diff --git a/android/app/build.gradle b/android/app/build.gradle index 5a0e7d86..cf69cbe8 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -86,6 +86,8 @@ dependencies { implementation(libs.jackson.annotations) implementation(libs.jackson.databind) + implementation(libs.libvlc.all) + // http implementation(libs.okhttp) @@ -115,5 +117,5 @@ dependencies { implementation libs.androidx.room.runtime annotationProcessor libs.androidx.room.compiler - debugImplementation("com.reqable.android:user-certificate-trust:1.0.0") + debugImplementation(libs.user.certificate.trust) } \ No newline at end of file diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/App.java b/android/app/src/main/java/top/ourfor/app/iplayx/App.java index b7ba4eea..f6e3b8bf 100644 --- a/android/app/src/main/java/top/ourfor/app/iplayx/App.java +++ b/android/app/src/main/java/top/ourfor/app/iplayx/App.java @@ -7,8 +7,6 @@ import androidx.appcompat.app.AppCompatDelegate; -import java.io.IOException; -import java.util.List; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; @@ -18,10 +16,8 @@ import lombok.val; import top.ourfor.app.iplayx.bean.JSONAdapter; import top.ourfor.app.iplayx.bean.KVStorage; -import top.ourfor.app.iplayx.common.annotation.ViewController; import top.ourfor.app.iplayx.config.AppSetting; import top.ourfor.app.iplayx.module.FontModule; -import top.ourfor.app.iplayx.util.AnnotationUtil; import top.ourfor.app.iplayx.util.DeviceUtil; import top.ourfor.app.iplayx.util.JacksonJsonAdapter; import top.ourfor.app.iplayx.util.MMKVStorage; @@ -39,11 +35,7 @@ public void onCreate() { val crashManager = new CrashManager(this); Thread.setDefaultUncaughtExceptionHandler(crashManager); val context = getApplicationContext(); - try { - FontModule.scanExternalFont(context); - } catch (Exception e) { - log.error("failed scan external font", e); - } + FontModule.initFont(context); DeviceUtil.init(context); XSET(Application.class, this); XSET(Context.class, context); diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/api/emby/EmbyApi.java b/android/app/src/main/java/top/ourfor/app/iplayx/api/emby/EmbyApi.java index f96c5a59..f09e7ef8 100644 --- a/android/app/src/main/java/top/ourfor/app/iplayx/api/emby/EmbyApi.java +++ b/android/app/src/main/java/top/ourfor/app/iplayx/api/emby/EmbyApi.java @@ -18,10 +18,12 @@ import lombok.EqualsAndHashCode; import lombok.Setter; import lombok.With; +import lombok.extern.slf4j.Slf4j; import lombok.val; import top.ourfor.app.iplayx.bean.JSONAdapter; import top.ourfor.app.iplayx.common.api.EmbyLikeApi; import top.ourfor.app.iplayx.common.type.MediaPlayState; +import top.ourfor.app.iplayx.model.EmbySiteInfo; import top.ourfor.app.iplayx.store.GlobalStore; import top.ourfor.app.iplayx.util.HTTPUtil; import top.ourfor.app.iplayx.util.HTTPModel; @@ -35,8 +37,9 @@ import top.ourfor.app.iplayx.common.model.SiteEndpointModel; import top.ourfor.app.iplayx.model.SiteModel; -@Builder @With +@Slf4j +@Builder @EqualsAndHashCode public class EmbyApi implements EmbyLikeApi { private static final String kDeviceProfile = "{\"DeviceProfile\":{\"MaxStaticBitrate\":140000000,\"MaxStreamingBitrate\":140000000,\"MusicStreamingTranscodingBitrate\":192000,\"DirectPlayProfiles\":[{\"Container\":\"mp4,m4v\",\"Type\":\"Video\",\"VideoCodec\":\"h264,h265,hevc,vp8,vp9\",\"AudioCodec\":\"mp3,aac,opus,flac,vorbis\"},{\"Container\":\"mkv\",\"Type\":\"Video\",\"VideoCodec\":\"h264,h265,hevc,vp8,vp9\",\"AudioCodec\":\"mp3,aac,opus,flac,vorbis\"},{\"Container\":\"flv\",\"Type\":\"Video\",\"VideoCodec\":\"h264\",\"AudioCodec\":\"aac,mp3\"},{\"Container\":\"mov\",\"Type\":\"Video\",\"VideoCodec\":\"h264\",\"AudioCodec\":\"mp3,aac,opus,flac,vorbis\"},{\"Container\":\"opus\",\"Type\":\"Audio\"},{\"Container\":\"mp3\",\"Type\":\"Audio\",\"AudioCodec\":\"mp3\"},{\"Container\":\"mp2,mp3\",\"Type\":\"Audio\",\"AudioCodec\":\"mp2\"},{\"Container\":\"aac\",\"Type\":\"Audio\",\"AudioCodec\":\"aac\"},{\"Container\":\"m4a\",\"AudioCodec\":\"aac\",\"Type\":\"Audio\"},{\"Container\":\"mp4\",\"AudioCodec\":\"aac\",\"Type\":\"Audio\"},{\"Container\":\"flac\",\"Type\":\"Audio\"},{\"Container\":\"webma,webm\",\"Type\":\"Audio\"},{\"Container\":\"wav\",\"Type\":\"Audio\",\"AudioCodec\":\"PCM_S16LE,PCM_S24LE\"},{\"Container\":\"ogg\",\"Type\":\"Audio\"},{\"Container\":\"webm\",\"Type\":\"Video\",\"AudioCodec\":\"vorbis,opus\",\"VideoCodec\":\"VP8,VP9\"}],\"TranscodingProfiles\":[{\"Container\":\"aac\",\"Type\":\"Audio\",\"AudioCodec\":\"aac\",\"Context\":\"Streaming\",\"Protocol\":\"hls\",\"MaxAudioChannels\":\"2\",\"MinSegments\":\"2\",\"BreakOnNonKeyFrames\":true},{\"Container\":\"aac\",\"Type\":\"Audio\",\"AudioCodec\":\"aac\",\"Context\":\"Streaming\",\"Protocol\":\"http\",\"MaxAudioChannels\":\"2\"},{\"Container\":\"mp3\",\"Type\":\"Audio\",\"AudioCodec\":\"mp3\",\"Context\":\"Streaming\",\"Protocol\":\"http\",\"MaxAudioChannels\":\"2\"},{\"Container\":\"opus\",\"Type\":\"Audio\",\"AudioCodec\":\"opus\",\"Context\":\"Streaming\",\"Protocol\":\"http\",\"MaxAudioChannels\":\"2\"},{\"Container\":\"wav\",\"Type\":\"Audio\",\"AudioCodec\":\"wav\",\"Context\":\"Streaming\",\"Protocol\":\"http\",\"MaxAudioChannels\":\"2\"},{\"Container\":\"opus\",\"Type\":\"Audio\",\"AudioCodec\":\"opus\",\"Context\":\"Static\",\"Protocol\":\"http\",\"MaxAudioChannels\":\"2\"},{\"Container\":\"mp3\",\"Type\":\"Audio\",\"AudioCodec\":\"mp3\",\"Context\":\"Static\",\"Protocol\":\"http\",\"MaxAudioChannels\":\"2\"},{\"Container\":\"aac\",\"Type\":\"Audio\",\"AudioCodec\":\"aac\",\"Context\":\"Static\",\"Protocol\":\"http\",\"MaxAudioChannels\":\"2\"},{\"Container\":\"wav\",\"Type\":\"Audio\",\"AudioCodec\":\"wav\",\"Context\":\"Static\",\"Protocol\":\"http\",\"MaxAudioChannels\":\"2\"},{\"Container\":\"mkv\",\"Type\":\"Video\",\"AudioCodec\":\"mp3,aac,opus,flac,vorbis\",\"VideoCodec\":\"h264,h265,hevc,vp8,vp9\",\"Context\":\"Static\",\"MaxAudioChannels\":\"2\",\"CopyTimestamps\":true},{\"Container\":\"m4s,ts\",\"Type\":\"Video\",\"AudioCodec\":\"mp3,aac\",\"VideoCodec\":\"h264,h265,hevc\",\"Context\":\"Streaming\",\"Protocol\":\"hls\",\"MaxAudioChannels\":\"2\",\"MinSegments\":\"2\",\"BreakOnNonKeyFrames\":true,\"ManifestSubtitles\":\"vtt\"},{\"Container\":\"webm\",\"Type\":\"Video\",\"AudioCodec\":\"vorbis\",\"VideoCodec\":\"vpx\",\"Context\":\"Streaming\",\"Protocol\":\"http\",\"MaxAudioChannels\":\"2\"},{\"Container\":\"mp4\",\"Type\":\"Video\",\"AudioCodec\":\"mp3,aac,opus,flac,vorbis\",\"VideoCodec\":\"h264\",\"Context\":\"Static\",\"Protocol\":\"http\"}],\"ContainerProfiles\":[],\"CodecProfiles\":[{\"Type\":\"VideoAudio\",\"Codec\":\"aac\",\"Conditions\":[{\"Condition\":\"Equals\",\"Property\":\"IsSecondaryAudio\",\"Value\":\"false\",\"IsRequired\":\"false\"}]},{\"Type\":\"VideoAudio\",\"Conditions\":[{\"Condition\":\"Equals\",\"Property\":\"IsSecondaryAudio\",\"Value\":\"false\",\"IsRequired\":\"false\"}]},{\"Type\":\"Video\",\"Codec\":\"h264\",\"Conditions\":[{\"Condition\":\"EqualsAny\",\"Property\":\"VideoProfile\",\"Value\":\"high|main|baseline|constrained baseline|high 10\",\"IsRequired\":false},{\"Condition\":\"LessThanEqual\",\"Property\":\"VideoLevel\",\"Value\":\"62\",\"IsRequired\":false}]},{\"Type\":\"Video\",\"Codec\":\"hevc\",\"Conditions\":[{\"Condition\":\"EqualsAny\",\"Property\":\"VideoCodecTag\",\"Value\":\"hvc1\",\"IsRequired\":false}]}],\"SubtitleProfiles\":[{\"Format\":\"vtt\",\"Method\":\"Hls\"},{\"Format\":\"eia_608\",\"Method\":\"VideoSideData\",\"Protocol\":\"hls\"},{\"Format\":\"eia_708\",\"Method\":\"VideoSideData\",\"Protocol\":\"hls\"},{\"Format\":\"vtt\",\"Method\":\"External\"},{\"Format\":\"ass\",\"Method\":\"External\"},{\"Format\":\"ssa\",\"Method\":\"External\"}],\"ResponseProfiles\":[{\"Type\":\"Video\",\"Container\":\"m4v\",\"MimeType\":\"video/mp4\"}]}}\n"; @@ -80,7 +83,8 @@ public static void login(String server, String username, String password, Consum return; } } catch (MalformedURLException | NullPointerException e) { - e.printStackTrace(); + log.error("Failed to parse server url: {}", server); + log.error("Error: ", e); } completion.accept(null); }); @@ -568,4 +572,30 @@ public void trackPlay(MediaPlayState state, EmbyPlaybackData data, Consumer completion) { + if (site == null || + site.getEndpoint() == null || + site.getUser() == null) return; + + // /emby/system/info/public + HTTPModel model = HTTPModel.builder() + .url(site.getEndpoint().getBaseUrl() + "emby/system/info/public") + .method("GET") + .modelClass(EmbySiteInfo.class) + .build(); + + HTTPUtil.request(model, response -> { + if (Objects.isNull(response)) { + completion.accept(null); + return; + } + if (response instanceof EmbySiteInfo) { + completion.accept(response); + return; + } + completion.accept(null); + }); + } } diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/api/jellyfin/JellyfinApi.java b/android/app/src/main/java/top/ourfor/app/iplayx/api/jellyfin/JellyfinApi.java index a945f1da..32f2223d 100644 --- a/android/app/src/main/java/top/ourfor/app/iplayx/api/jellyfin/JellyfinApi.java +++ b/android/app/src/main/java/top/ourfor/app/iplayx/api/jellyfin/JellyfinApi.java @@ -28,6 +28,7 @@ import top.ourfor.app.iplayx.model.EmbyPageableModel; import top.ourfor.app.iplayx.model.EmbyPlaybackData; import top.ourfor.app.iplayx.model.EmbyPlaybackModel; +import top.ourfor.app.iplayx.model.EmbySiteInfo; import top.ourfor.app.iplayx.model.EmbyUserData; import top.ourfor.app.iplayx.model.EmbyUserModel; import top.ourfor.app.iplayx.model.ImageType; @@ -36,6 +37,7 @@ import top.ourfor.app.iplayx.util.HTTPModel; import top.ourfor.app.iplayx.util.HTTPUtil; +@Setter @Builder @With @EqualsAndHashCode @@ -45,7 +47,6 @@ public class JellyfinApi implements EmbyLikeApi { private static final String authHeaderValue = String.format("MediaBrowser Client=\"%s\", Device=\"%s\", DeviceId=\"%s\", Version=\"%s\"", "iPlay", "Android", "9999999", "v1.0.0"); - @Setter SiteModel site; public static void login(String server, String username, String password, Consumer completion) { @@ -579,4 +580,30 @@ public void getSimilar(String id, Consumer completion) { completion.accept(null); }); } + + @Override + public void getSiteInfo(Consumer completion) { + if (site == null || + site.getEndpoint() == null || + site.getUser() == null) return; + + // /emby/system/info/public + HTTPModel model = HTTPModel.builder() + .url(site.getEndpoint().getBaseUrl() + "system/info/public") + .method("GET") + .modelClass(EmbySiteInfo.class) + .build(); + + HTTPUtil.request(model, response -> { + if (Objects.isNull(response)) { + completion.accept(null); + return; + } + if (response instanceof EmbySiteInfo) { + completion.accept(response); + return; + } + completion.accept(null); + }); + } } diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/common/api/EmbyLikeApi.java b/android/app/src/main/java/top/ourfor/app/iplayx/common/api/EmbyLikeApi.java index 47348710..3ca5d682 100644 --- a/android/app/src/main/java/top/ourfor/app/iplayx/common/api/EmbyLikeApi.java +++ b/android/app/src/main/java/top/ourfor/app/iplayx/common/api/EmbyLikeApi.java @@ -13,6 +13,8 @@ default void setSiteModel(SiteModel site) { } static void login(String server, String username, String password, Consumer completion) { } + default void getSiteInfo(Consumer completion) { } + default void getAlbums(Consumer completion) { } default void getAlbumLatestMedias(String id, Consumer completion) { } diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/common/type/PlayerKernelType.java b/android/app/src/main/java/top/ourfor/app/iplayx/common/type/PlayerKernelType.java new file mode 100644 index 00000000..bf842e68 --- /dev/null +++ b/android/app/src/main/java/top/ourfor/app/iplayx/common/type/PlayerKernelType.java @@ -0,0 +1,7 @@ +package top.ourfor.app.iplayx.common.type; + +public enum PlayerKernelType { + MPV, + EXO, + VLC, +} diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/config/AppSetting.java b/android/app/src/main/java/top/ourfor/app/iplayx/config/AppSetting.java index 61843522..099a728e 100644 --- a/android/app/src/main/java/top/ourfor/app/iplayx/config/AppSetting.java +++ b/android/app/src/main/java/top/ourfor/app/iplayx/config/AppSetting.java @@ -17,6 +17,7 @@ import lombok.val; import top.ourfor.app.iplayx.bean.KVStorage; import top.ourfor.app.iplayx.common.type.LayoutType; +import top.ourfor.app.iplayx.common.type.PlayerKernelType; import top.ourfor.app.iplayx.common.type.VideoDecodeType; import top.ourfor.app.iplayx.common.type.PictureQuality; import top.ourfor.app.iplayx.page.setting.theme.ThemeColorModel; @@ -42,6 +43,7 @@ public class AppSetting { public PictureQuality pictureQuality; public boolean usePictureMultiThread; public boolean useExoPlayer; + public PlayerKernelType playerKernel; public String mpvConfig; public String fontFamily; public String webHomePage; @@ -58,11 +60,11 @@ static AppSetting getShared() { instance.turnOffAutoUpgrade = DeviceUtil.isTV || DeviceUtil.isDebugPackage; instance.webHomePage = "https://bing.com"; instance.pictureQuality = PictureQuality.Auto; - instance.useExoPlayer = false; + instance.playerKernel = PlayerKernelType.MPV; instance.layoutType = LayoutType.Auto; if (DeviceUtil.isTV) { instance.videoDecodeType = VideoDecodeType.Hardware; - instance.useExoPlayer = true; + instance.playerKernel = PlayerKernelType.EXO; } } return instance; diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/model/EmbySiteInfo.java b/android/app/src/main/java/top/ourfor/app/iplayx/model/EmbySiteInfo.java new file mode 100644 index 00000000..f1918b35 --- /dev/null +++ b/android/app/src/main/java/top/ourfor/app/iplayx/model/EmbySiteInfo.java @@ -0,0 +1,25 @@ +package top.ourfor.app.iplayx.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.With; + +@Data +@With +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode +public class EmbySiteInfo { + @JsonProperty("ServerName") + String ServerName; + @JsonProperty("Version") + String Version; + @JsonProperty("Id") + String Id; +} diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/module/FontModule.java b/android/app/src/main/java/top/ourfor/app/iplayx/module/FontModule.java index d1016dd5..c484da07 100644 --- a/android/app/src/main/java/top/ourfor/app/iplayx/module/FontModule.java +++ b/android/app/src/main/java/top/ourfor/app/iplayx/module/FontModule.java @@ -10,11 +10,11 @@ import java.util.List; import java.util.Map; +import lombok.extern.slf4j.Slf4j; import top.ourfor.app.iplayx.config.AppSetting; +@Slf4j public class FontModule { - static private String moduleName = "FontModule"; - static private Map systemFontMap; static Map getSystemFontMap() throws NoSuchFieldException, IllegalAccessException { @@ -28,10 +28,8 @@ static Map getSystemFontMap() throws NoSuchFieldException, Ill static public void obtainSystemFont() { try { systemFontMap = getSystemFontMap(); - } catch (NoSuchFieldException e) { - Log.d(moduleName, e.toString()); - } catch (IllegalAccessException e) { - Log.d(moduleName, e.toString()); + } catch (NoSuchFieldException | IllegalAccessException e) { + log.error("obtain system font failed", e); } } @@ -58,15 +56,23 @@ public static String getFontPath(Context context) { return fontDir.getPath(); } + public static void initFont(Context context) { + try { + scanExternalFont(context); + } catch (Exception e) { + log.error("scan external font failed", e); + } + } + public static void scanExternalFont(Context context) throws IllegalAccessException, NoSuchMethodException, NoSuchFieldException { File filesDir = context.getExternalFilesDir(""); File fontDir = new File(filesDir, "font"); if (!fontDir.exists()) { boolean created = fontDir.mkdirs(); if (created) { - Log.d(moduleName, "create fontDir success: " + fontDir.getPath()); + log.debug("create fontDir success: {}", fontDir.getPath()); } else { - Log.d(moduleName, "create fontDir failed: " + fontDir.getPath()); + log.debug("create fontDir failed: {}", fontDir.getPath()); } } diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/page/Page.java b/android/app/src/main/java/top/ourfor/app/iplayx/page/Page.java index b7be8738..61c588a0 100644 --- a/android/app/src/main/java/top/ourfor/app/iplayx/page/Page.java +++ b/android/app/src/main/java/top/ourfor/app/iplayx/page/Page.java @@ -7,6 +7,7 @@ public interface Page { default int id() { return -1; } + default String title() { return null; } default void create(Context context, Map params) {} default void destroy() {} diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/page/Router.java b/android/app/src/main/java/top/ourfor/app/iplayx/page/Router.java index c59fef5d..15b6f8bb 100644 --- a/android/app/src/main/java/top/ourfor/app/iplayx/page/Router.java +++ b/android/app/src/main/java/top/ourfor/app/iplayx/page/Router.java @@ -61,6 +61,8 @@ public class Router implements Navigator { pageType.put(R.id.audioPage, PageType.AUDIO_CONFIG); pageType.put(R.id.playerPage, PageType.PLAYER); pageType.put(R.id.musicPage, PageType.MUSIC); + pageType.put(R.id.cachePage, PageType.CACHE); + pageType.put(R.id.picturePage, PageType.IMAGE_CONFIG); pageType.put(R.id.musicPlayerPage, PageType.MUSIC_PLAYER); pageType.put(R.id.webPage, PageType.WEB); } @@ -156,9 +158,9 @@ public void pushPage(String name, Map params) { public void pushPage(Page newPage, Map params) { val pages = navigators.computeIfAbsent(stackId, k -> new Stack<>()); if (pages.isEmpty()) return; - val page = pages.peek(); - page.viewWillDisappear(); - pageId.put(newPage, page.id()); + val oldPage = pages.peek(); + oldPage.viewWillDisappear(); + pageId.put(newPage, oldPage.id()); newPage.create(container.getContext(), params); pages.push(newPage); val view = newPage.view(); @@ -168,8 +170,8 @@ public void pushPage(Page newPage, Map params) { if (view instanceof PageLifecycle lifecycle) { lifecycle.onAttach(); } - pushPageAnimation(page, newPage); - onNavigateChange(page.id()); + pushPageAnimation(oldPage, newPage); + onNavigateChange(newPage.id()); } @Override diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/page/login/LoginPage.java b/android/app/src/main/java/top/ourfor/app/iplayx/page/login/LoginPage.java index f50ed971..943837fb 100644 --- a/android/app/src/main/java/top/ourfor/app/iplayx/page/login/LoginPage.java +++ b/android/app/src/main/java/top/ourfor/app/iplayx/page/login/LoginPage.java @@ -108,7 +108,9 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c actionBar.setDisplayHomeAsUpEnabled(true); XGET(BottomNavigationView.class).setVisibility(View.GONE); } - val context = container == null ? XGET(Activity.class) : container.getContext(); + if (context == null) { + context = container == null ? XGET(Activity.class) : container.getContext(); + } setupUI(context); refreshUI(); XWATCH(OneDriveAction.class, this); @@ -125,7 +127,9 @@ void setup(LayoutInflater inflater, ViewGroup container) { actionBar.setDisplayHomeAsUpEnabled(true); XGET(BottomNavigationView.class).setVisibility(View.GONE); } - val context = container == null ? XGET(Activity.class) : container.getContext(); + if (context == null) { + context = container == null ? XGET(Activity.class) : container.getContext(); + } setupUI(context); refreshUI(); XWATCH(OneDriveAction.class, this); @@ -158,7 +162,6 @@ public void onStart() { @SuppressLint("NewApi") void setupUI(Context context) { - KVStorage kv = XGET(KVStorage.class); if (siteModel != null) { binding.remarkInput.setText(siteModel.getEndpoint().getRemark()); binding.serverInput.setText(siteModel.getEndpoint().getBaseUrl()); @@ -183,77 +186,9 @@ void setupUI(Context context) { // do login log.info("remake: {}, server: {}, username: {}, password: {}", remake, server, username, password); if (serverType == ServerType.Emby) { - EmbyApi.login(server, username, password, (response) -> { - log.info("login response: {}", response); - if (response != null) { - kv.set(remarkKey, remake); - kv.set(serverKey, server); - kv.set(usernameKey, username); - kv.set(passwordKey, password); - - val site = (SiteModel) response; - site.setSync(binding.allowSyncSwitch.isChecked()); - site.setShowSensitive(binding.showSensitiveSwitch.isChecked()); - site.setRemark(remake); - site.setServerType(ServerType.Emby); - XGET(GlobalStore.class).addNewSite(site); - XGET(DispatchAction.class).runOnUiThread(() -> { - val action = XGET(SiteUpdateAction.class); - action.onSiteUpdate(); - Toast.makeText(context, "Login success", Toast.LENGTH_SHORT).show(); - if (isFragment) { - XGET(Navigator.class).popPage(); - } else { - dismiss(); - } - }); - } else { - XGET(Activity.class).runOnUiThread(() -> - Toast.makeText(context, "Login failed", Toast.LENGTH_SHORT).show() - ); - } - - binding.loginButton.post(() -> { - binding.loginButton.setEnabled(true); - binding.loginButton.setText(R.string.login); - }); - }); + loginToEmby(remake, server, username, password); } else if (serverType == ServerType.Jellyfin) { - JellyfinApi.login(server, username, password, (response) -> { - log.info("login response: {}", response); - if (response != null) { - kv.set(remarkKey, remake); - kv.set(serverKey, server); - kv.set(usernameKey, username); - kv.set(passwordKey, password); - - val site = (SiteModel) response; - site.setSync(binding.allowSyncSwitch.isChecked()); - site.setShowSensitive(binding.showSensitiveSwitch.isChecked()); - site.setRemark(remake); - site.setServerType(ServerType.Jellyfin); - XGET(GlobalStore.class).addNewSite(site); - getActivity().runOnUiThread(() -> { - val action = XGET(SiteUpdateAction.class); - action.onSiteUpdate(); - Toast.makeText(context, "Login success", Toast.LENGTH_SHORT).show(); - if (isFragment) { - XGET(Navigator.class).popPage(); - } else { - dismiss(); - } - }); - } else { - getActivity().runOnUiThread(() -> - Toast.makeText(context, "Login failed", Toast.LENGTH_SHORT).show() - ); - } - - binding.loginButton.post(() -> { - binding.loginButton.setEnabled(true); - binding.loginButton.setText(R.string.login); - }); - }); + loginToJellyfin(remake, server, username, password); } else if (serverType == ServerType.Cloud189) { loginTo189(remake, username, password); } else if (serverType == ServerType.OneDrive) { @@ -347,6 +282,86 @@ public void onedriveReadyUpdate(OneDriveAuth auth) { }); } + private void loginToEmby(String remake, String server, String username, String password) { + EmbyApi.login(server, username, password, (response) -> { + log.info("login response: {}", response); + if (response != null) { + var kv = XGET(KVStorage.class); + assert kv != null; + kv.set(remarkKey, remake); + kv.set(serverKey, server); + kv.set(usernameKey, username); + kv.set(passwordKey, password); + + val site = (SiteModel) response; + site.setSync(binding.allowSyncSwitch.isChecked()); + site.setShowSensitive(binding.showSensitiveSwitch.isChecked()); + site.setRemark(remake); + site.setServerType(ServerType.Emby); + XGET(GlobalStore.class).addNewSite(site); + XGET(Activity.class).runOnUiThread(() -> { + val action = XGET(SiteUpdateAction.class); + action.onSiteUpdate(); + Toast.makeText(context, "Login success", Toast.LENGTH_SHORT).show(); + if (isFragment) { + XGET(Navigator.class).popPage(); + } else { + dismiss(); + } + }); + } else { + XGET(Activity.class).runOnUiThread(() -> + Toast.makeText(context, "Login failed", Toast.LENGTH_SHORT).show() + ); + } + + binding.loginButton.post(() -> { + binding.loginButton.setEnabled(true); + binding.loginButton.setText(R.string.login); + }); + }); + } + + private void loginToJellyfin(String remake, String server, String username, String password) { + JellyfinApi.login(server, username, password, (response) -> { + log.info("login response: {}", response); + if (response != null) { + var kv = XGET(KVStorage.class); + assert kv != null; + kv.set(remarkKey, remake); + kv.set(serverKey, server); + kv.set(usernameKey, username); + kv.set(passwordKey, password); + + val site = (SiteModel) response; + site.setSync(binding.allowSyncSwitch.isChecked()); + site.setShowSensitive(binding.showSensitiveSwitch.isChecked()); + site.setRemark(remake); + site.setServerType(ServerType.Jellyfin); + XGET(GlobalStore.class).addNewSite(site); + getActivity().runOnUiThread(() -> { + val action = XGET(SiteUpdateAction.class); + action.onSiteUpdate(); + Toast.makeText(context, "Login success", Toast.LENGTH_SHORT).show(); + if (isFragment) { + XGET(Navigator.class).popPage(); + } else { + dismiss(); + } + }); + } else { + getActivity().runOnUiThread(() -> + Toast.makeText(context, "Login failed", Toast.LENGTH_SHORT).show() + ); + } + + binding.loginButton.post(() -> { + binding.loginButton.setEnabled(true); + binding.loginButton.setText(R.string.login); + }); + }); + } + private void loginToWebDAV(String remake, String server, String username, String password) { binding.loginButton.setEnabled(false); new WebDavFileApi(server, username, password).login(success -> { diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/page/login/SiteViewCell.java b/android/app/src/main/java/top/ourfor/app/iplayx/page/login/SiteViewCell.java index f14c3c42..0e3a18d8 100644 --- a/android/app/src/main/java/top/ourfor/app/iplayx/page/login/SiteViewCell.java +++ b/android/app/src/main/java/top/ourfor/app/iplayx/page/login/SiteViewCell.java @@ -9,6 +9,7 @@ import androidx.annotation.NonNull; import androidx.constraintlayout.widget.ConstraintLayout; +import lombok.extern.slf4j.Slf4j; import lombok.val; import top.ourfor.app.iplayx.R; import top.ourfor.app.iplayx.action.DispatchAction; @@ -21,6 +22,7 @@ import top.ourfor.app.iplayx.model.SiteModel; import top.ourfor.app.iplayx.store.GlobalStore; +@Slf4j public class SiteViewCell extends ConstraintLayout implements UpdateModelAction { private SiteModel model; SiteCellBinding binding = null; @@ -75,13 +77,13 @@ void setupUI(Context context) { void bind() { binding.content.setOnClickListener(v -> callOnClick()); - binding.delete.setOnClickListener(v -> XGET(GlobalStore.class).removeSite(model)); - boolean allowModify = XGET(Navigator.class).getCurrentPageId() == R.id.sitePage; + val currentPageId = XGET(Navigator.class).getCurrentPageId(); + boolean allowModify = currentPageId == R.id.sitePage || currentPageId == R.id.settingPage; binding.modify.setVisibility(allowModify ? VISIBLE : GONE); binding.modify.setOnClickListener(v -> { - val dst = XGET(Navigator.class).getCurrentPageId(); - if (dst == R.id.sitePage) { + val pageId = XGET(Navigator.class).getCurrentPageId(); + if (pageId == R.id.sitePage || pageId == R.id.settingPage) { val action = XGET(SiteUpdateAction.class); if (action == null) return; XGET(DispatchAction.class).runOnUiThread(() -> action.onSiteModify(model)); diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/page/media/MediaPage.java b/android/app/src/main/java/top/ourfor/app/iplayx/page/media/MediaPage.java index 38855916..2e3c8208 100644 --- a/android/app/src/main/java/top/ourfor/app/iplayx/page/media/MediaPage.java +++ b/android/app/src/main/java/top/ourfor/app/iplayx/page/media/MediaPage.java @@ -16,6 +16,8 @@ import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams; import com.google.android.flexbox.FlexboxLayout; +import com.google.android.material.bottomsheet.BottomSheetBehavior; +import com.google.android.material.bottomsheet.BottomSheetDialog; import java.util.HashMap; import java.util.Map; @@ -39,6 +41,7 @@ import top.ourfor.app.iplayx.module.GlideApp; import top.ourfor.app.iplayx.page.Page; import top.ourfor.app.iplayx.page.home.MediaViewCell; +import top.ourfor.app.iplayx.page.web.ScriptManageView; import top.ourfor.app.iplayx.store.GlobalStore; import top.ourfor.app.iplayx.util.DeviceUtil; import top.ourfor.app.iplayx.util.LayoutUtil; @@ -182,8 +185,7 @@ void bind() { if (model == null) return; var backdrop = model.getImage().getBackdrop(); - if (model instanceof EmbyMediaModel) { - val media = (EmbyMediaModel)model; + if (model instanceof EmbyMediaModel media) { backdrop = media.isEpisode() ? media.getImage().getPrimary() : media.getImage().getBackdrop(); } GlideApp.with(context) @@ -278,6 +280,24 @@ void bind() { binding.actorList.setVisibility(View.GONE); binding.actorLabel.setVisibility(View.GONE); } + + var isPlayable = model instanceof EmbyMediaModel media && (media.isEpisode() || media.isMovie()); + binding.playerConfig.setVisibility(isPlayable ? View.VISIBLE : View.GONE); + binding.playerConfig.setOnClickListener(v -> { + showPlayConfigPanel(); + }); + } + + void showPlayConfigPanel() { + var dialog = new BottomSheetDialog(getContext(), R.style.SiteBottomSheetDialog); + dialog.setOnDismissListener(dlg -> { }); + var view = new PlayerConfigPanelView(context, (EmbyMediaModel)model); + view.setOnPlayButtonClick(v -> dialog.dismiss()); + dialog.setContentView(view); + var behavior = BottomSheetBehavior.from((View) view.getParent()); + val height = (int) (DeviceUtil.screenSize(getContext()).getHeight() * 0.6); + behavior.setPeekHeight(height); + dialog.show(); } void showSimilarList() { diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/page/media/PlayerConfigPanelView.java b/android/app/src/main/java/top/ourfor/app/iplayx/page/media/PlayerConfigPanelView.java new file mode 100644 index 00000000..36e5c714 --- /dev/null +++ b/android/app/src/main/java/top/ourfor/app/iplayx/page/media/PlayerConfigPanelView.java @@ -0,0 +1,120 @@ +package top.ourfor.app.iplayx.page.media; + +import static top.ourfor.app.iplayx.module.Bean.XGET; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Spinner; + +import androidx.annotation.NonNull; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import top.ourfor.app.iplayx.bean.Navigator; +import top.ourfor.app.iplayx.databinding.PlayerConfigPanelBinding; +import top.ourfor.app.iplayx.model.EmbyMediaModel; +import top.ourfor.app.iplayx.view.LifecycleHolder; +import top.ourfor.app.iplayx.view.video.PlayerSourceModel; + +@Slf4j +@SuppressLint("ViewConstructor") +public class PlayerConfigPanelView extends LifecycleHolder { + PlayerConfigPanelBinding binding; + @Getter + PlayerConfigPanelViewModel viewModel = new PlayerConfigPanelViewModel(); + + @Getter @Setter + Consumer onPlayButtonClick; + + PlayerSourceModel videoSource; + PlayerSourceModel audioSource; + PlayerSourceModel subtitleSource; + + public PlayerConfigPanelView(@NonNull Context context, EmbyMediaModel media) { + super(context); + viewModel.getMedia().setValue(media); + binding = PlayerConfigPanelBinding.inflate(LayoutInflater.from(context), this, true); + val layout = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + setLayoutParams(layout); + bind(); + } + + private void bind() { + Consumer onMediaSourceUpdate = (model) -> { + if (model == null) return; + log.info("media source model: {}", model); + post(() -> { + configSource(binding.videoSource, model.getVideos(), null); + configSource(binding.audioSource, model.getAudios(), null); + configSource(binding.subtitleSource, model.getSubtitles(), null); + binding.loading.pauseAnimation(); + binding.loading.setVisibility(View.GONE); + }); + }; + viewModel.getMediaSource().observe(this, onMediaSourceUpdate::accept); + + viewModel.fetchMediaSource(onMediaSourceUpdate); + + binding.play.setOnClickListener(v -> { + if (onPlayButtonClick != null) { + onPlayButtonClick.accept(v); + } + + var navigator = XGET(Navigator.class); + assert navigator != null; + navigator.pushPage("movie_player_page", Map.of( + "source", PlayerConfigPanelViewModel.MediaSourceModel.builder() + .media(Objects.requireNonNull(viewModel.getMedia().getValue())) + .video(videoSource) + .audio(audioSource) + .subtitle(subtitleSource) + .build() + )); + }); + } + + void configSource(Spinner spinner, List sourceModels, PlayerSourceModel model) { + val options = sourceModels.stream().map(PlayerSourceModel::getName).collect(Collectors.toList()); + if (options.isEmpty()) { + return; + } + val value = model == null ? options.get(0) : model.getName(); + val adapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_item, options); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spinner.setAdapter(adapter); + spinner.setSelection(options.indexOf(value)); + spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + if (options.size() <= position) return; + val model = sourceModels.get(position); + val newPlaySource = PlayerSourceModel.builder().name(model.getName()).url(model.getUrl()).build(); + if (spinner == binding.videoSource) { + videoSource = newPlaySource; + } else if (spinner == binding.audioSource) { + audioSource = newPlaySource; + } else if (spinner == binding.subtitleSource) { + subtitleSource = newPlaySource; + } + } + + @Override + public void onNothingSelected(AdapterView parent) { + + } + }); + } +} diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/page/media/PlayerConfigPanelViewModel.java b/android/app/src/main/java/top/ourfor/app/iplayx/page/media/PlayerConfigPanelViewModel.java new file mode 100644 index 00000000..884fb6a5 --- /dev/null +++ b/android/app/src/main/java/top/ourfor/app/iplayx/page/media/PlayerConfigPanelViewModel.java @@ -0,0 +1,79 @@ +package top.ourfor.app.iplayx.page.media; + +import static top.ourfor.app.iplayx.module.Bean.XGET; + +import androidx.lifecycle.MutableLiveData; + +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.With; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import top.ourfor.app.iplayx.model.EmbyMediaModel; +import top.ourfor.app.iplayx.store.GlobalStore; +import top.ourfor.app.iplayx.view.video.PlayerSourceModel; + +@Slf4j +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class PlayerConfigPanelViewModel { + private final MutableLiveData media = new MutableLiveData<>(null); + private final MutableLiveData mediaSource = new MutableLiveData<>(null); + + String value; + + Consumer onClick; + + public void fetchMediaSource(Consumer callback) { + if (this.media.getValue() == null) return; + + val media = this.media.getValue(); + val store = XGET(GlobalStore.class); + assert store != null; + store.getPlayback(media.getId(), playback -> { + if (playback == null) return; + val sources = store.getPlaySources(media, playback); + val videos = sources.stream().filter(v -> v.getType() == PlayerSourceModel.PlayerSourceType.Video).collect(Collectors.toList()); + val audios = sources.stream().filter(v -> v.getType() == PlayerSourceModel.PlayerSourceType.Audio).collect(Collectors.toList()); + val subtitles = sources.stream().filter(v -> v.getType() == PlayerSourceModel.PlayerSourceType.Subtitle).collect(Collectors.toList()); + + val model = MediaSourceConfigModel.builder() + .videos(videos) + .audios(audios) + .subtitles(subtitles) + .build(); + mediaSource.postValue(model); + log.info("media source model: {}", model); + if (callback != null) callback.accept(model); + }); + } + + @Data + @With + @Builder + public static class MediaSourceConfigModel { + List videos; + List audios; + List subtitles; + } + + @Data + @With + @Builder + public static class MediaSourceModel { + EmbyMediaModel media; + + PlayerSourceModel video; + PlayerSourceModel audio; + PlayerSourceModel subtitle; + } + +} diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/page/player/MoviePlayerPage.java b/android/app/src/main/java/top/ourfor/app/iplayx/page/player/MoviePlayerPage.java index 88ca4497..b60562d4 100644 --- a/android/app/src/main/java/top/ourfor/app/iplayx/page/player/MoviePlayerPage.java +++ b/android/app/src/main/java/top/ourfor/app/iplayx/page/player/MoviePlayerPage.java @@ -61,6 +61,7 @@ import top.ourfor.app.iplayx.model.EmbyMediaModel; import top.ourfor.app.iplayx.page.Page; import top.ourfor.app.iplayx.page.home.MediaViewCell; +import top.ourfor.app.iplayx.page.media.PlayerConfigPanelViewModel; import top.ourfor.app.iplayx.util.DeviceUtil; import top.ourfor.app.iplayx.util.IntervalCaller; import top.ourfor.app.iplayx.util.WindowUtil; @@ -80,13 +81,14 @@ public class MoviePlayerPage implements Page { private String id = null; private String url = null; private String title = null; + private PlayerConfigPanelViewModel.MediaSourceModel source = null; private EmbyPlaybackData playbackData = null; private IntervalCaller caller; @Getter Context context; - HashMap params; + Map params; public void onCreate(@Nullable Bundle savedInstanceState) { @@ -95,6 +97,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { id = (String) args.getOrDefault("id", null); url = (String) args.getOrDefault("url", null); title = (String) args.getOrDefault("title", null); + source = (PlayerConfigPanelViewModel.MediaSourceModel) args.getOrDefault("source", null); setupUI(getContext()); bind(); WindowUtil.enterFullscreen(); @@ -119,13 +122,19 @@ void setupUI(Context context) { } void bind() { - if (id != null) { + if (source != null) { + setupWithSource(source); + } else if (id != null) { setupWithId(id); } else if (url != null) { setupWithUrl(url); } } + private void setupWithSource(PlayerConfigPanelViewModel.MediaSourceModel source) { + playEmbyMediaWithId(source.getMedia().getId()); + } + void setupWithUrl(String url) { playerView.post(() -> { playerView.setOption(AppSetting.shared.getPlayerConfig()); @@ -149,7 +158,12 @@ void setupWithUrl(String url) { } void setupWithId(String id) { + playEmbyMediaWithId(id); + } + + void playEmbyMediaWithId(String id) { val store = XGET(GlobalStore.class); + assert store != null; val media = store.getDataSource().getMediaMap().get(id); if (media == null) return; val name = media.getSeriesName() != null ? media.getSeriesName() : media.getName(); @@ -178,7 +192,7 @@ void setupWithId(String id) { store.getPlayback(media.getId(), playback -> { if (playback == null) return; val sources = store.getPlaySources(media, playback); - val video = sources.stream().filter(v -> v.getType() == PlayerSourceModel.PlayerSourceType.Video).findFirst().get(); + val video = source != null && source.getVideo() != null ? source.getVideo() : sources.stream().filter(v -> v.getType() == PlayerSourceModel.PlayerSourceType.Video).findFirst().get(); playbackData = EmbyPlaybackData.builder() .playSessionId(playback.getSessionId()) .isMuted(false) @@ -190,7 +204,7 @@ void setupWithId(String id) { .nowPlayingQueue(List.of(new EmbyPlayingQueue("", "playlistItem0"))) .build(); XGET(GlobalStore.class).trackPlay(MediaPlayState.OPENING, playbackData); - val url = store.getPlayUrl(playback); + val url = source != null ? video.getUrl() : store.getPlayUrl(playback); log.debug("video url: {}", url); if (url == null) return; this.playerView.post(() -> { @@ -359,7 +373,7 @@ public void viewDidDisappear() { @Override public void create(Context context, Map params) { this.context = context; - this.params = (HashMap)params; + this.params = (Map)params; onCreate(null); onCreateView(LayoutInflater.from(context), null, null); } @@ -368,5 +382,10 @@ public void create(Context context, Map params) { public View view() { return contentView; } + + @Override + public int id() { + return R.id.playerPage; + } } diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/SettingPage.java b/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/SettingPage.java index 2d4e26e8..bcc60782 100644 --- a/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/SettingPage.java +++ b/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/SettingPage.java @@ -61,17 +61,18 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c void setupUI(Context context) { listView.viewModel.viewCell = SettingItemViewCell.class; listView.viewModel.onClick = (ListItemClickEvent event) -> { - val navController = XGET(Navigator.class); + val navigator = XGET(Navigator.class); + assert navigator != null; val type = event.getModel().type; switch (type) { - case Theme -> navController.pushPage(R.id.themePage, null); - case Video -> navController.pushPage(R.id.videoPage, null); - case Audio -> navController.pushPage(R.id.audioPage, null); - case Site -> navController.pushPage(R.id.sitePage, null); - case About -> navController.pushPage(R.id.aboutPage, null); - case Cloud -> navController.pushPage(R.id.cloudPage, null); - case Cache -> navController.pushPage(R.id.cachePage, null); - case Picture -> navController.pushPage(R.id.picturePage, null); + case Theme -> navigator.pushPage("theme_page", null); + case Video -> navigator.pushPage("video_page", null); + case Audio -> navigator.pushPage("audio_page", null); + case Site -> navigator.pushPage("site_page", null); + case About -> navigator.pushPage("about_page", null); + case Cloud -> navigator.pushPage("cloud_page", null); + case Cache -> navigator.pushPage("cache_page", null); + case Picture -> navigator.pushPage("picture_page", null); default -> Toast.makeText(context, R.string.not_implementation, Toast.LENGTH_SHORT).show(); } }; @@ -94,4 +95,9 @@ public void create(Context context, Map params) { public View view() { return binding.getRoot(); } + + @Override + public int id() { + return R.id.settingPage; + } } diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/about/AboutPage.java b/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/about/AboutPage.java index 0eabe299..ec3b5366 100644 --- a/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/about/AboutPage.java +++ b/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/about/AboutPage.java @@ -133,4 +133,5 @@ public void create(Context context, Map params) { this.context = context; onCreateView(LayoutInflater.from(context), null, null); } + } diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/audio/AudioPage.java b/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/audio/AudioPage.java index e11f41d5..5cac95d7 100644 --- a/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/audio/AudioPage.java +++ b/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/audio/AudioPage.java @@ -81,4 +81,9 @@ public void create(Context context, Map params) { onCreate(null); onCreateView(LayoutInflater.from(context), null, null); } + + @Override + public int id() { + return R.id.audioPage; + } } diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/cache/CachePage.java b/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/cache/CachePage.java index 176e2a13..f70bfb6f 100644 --- a/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/cache/CachePage.java +++ b/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/cache/CachePage.java @@ -90,4 +90,9 @@ public void create(Context context, Map params) { public View view() { return contentView; } + + @Override + public int id() { + return R.id.cachePage; + } } diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/cloud/CloudPage.java b/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/cloud/CloudPage.java index 065265ca..ec1298df 100644 --- a/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/cloud/CloudPage.java +++ b/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/cloud/CloudPage.java @@ -247,4 +247,9 @@ public void create(Context context, Map params) { public View view() { return binding.getRoot(); } + + @Override + public int id() { + return R.id.cloudPage; + } } diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/common/ScriptManageViewModel.java b/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/common/ScriptManageViewModel.java deleted file mode 100644 index 703f5be3..00000000 --- a/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/common/ScriptManageViewModel.java +++ /dev/null @@ -1,33 +0,0 @@ -package top.ourfor.app.iplayx.page.setting.common; - -import android.content.Context; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.LayoutInflater; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.constraintlayout.widget.ConstraintLayout; - -import java.util.function.Consumer; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.val; -import top.ourfor.app.iplayx.R; -import top.ourfor.app.iplayx.action.UpdateModelAction; -import top.ourfor.app.iplayx.databinding.ScriptManageBinding; -import top.ourfor.app.iplayx.util.DeviceUtil; - -@NoArgsConstructor -@AllArgsConstructor -public class ScriptManageViewModel { - @Getter @Setter - String value; - - @Getter @Setter - Consumer onClick; - -} diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/picture/PicturePage.java b/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/picture/PicturePage.java index 86ff63cd..03d9ad4f 100644 --- a/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/picture/PicturePage.java +++ b/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/picture/PicturePage.java @@ -109,4 +109,9 @@ public void create(Context context, Map params) { onCreate(null); onCreateView(LayoutInflater.from(context), null, null); } + + @Override + public int id() { + return R.id.picturePage; + } } diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/site/SitePage.java b/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/site/SitePage.java index 6bf291b2..be12811d 100644 --- a/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/site/SitePage.java +++ b/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/site/SitePage.java @@ -32,6 +32,7 @@ import top.ourfor.app.iplayx.action.SiteUpdateAction; import top.ourfor.app.iplayx.common.annotation.ViewController; import top.ourfor.app.iplayx.databinding.SitePageBinding; +import top.ourfor.app.iplayx.model.EmbySiteInfo; import top.ourfor.app.iplayx.model.SiteModel; import top.ourfor.app.iplayx.page.Activity; import top.ourfor.app.iplayx.page.Page; @@ -113,7 +114,19 @@ void addSite() { @Override public void updateSiteList() { val store = XGET(GlobalStore.class); - listView.viewModel.isSelected = (model) -> model.getUser().equals(store.getSite().getUser()) && model.getId().equals(store.getSite().getId()); + assert store != null; + val currentSite = store.getSite(); + if (currentSite.getRemark() == null || currentSite.getRemark().isEmpty()) { + val api = store.getApi(); + if (api == null) return; + api.getSiteInfo((info) -> { + if (!(info instanceof EmbySiteInfo embyInfo)) return; + currentSite.setRemark(embyInfo.getServerName()); + store.save(); + updateSiteList(); + }); + } + listView.viewModel.isSelected = (model) -> model.getUser().equals(currentSite.getUser()) && model.getId().equals(currentSite.getId()); listView.setItems(store.getSites()); } @@ -161,4 +174,9 @@ public void create(Context context, Map params) { public View view() { return binding.getRoot(); } + + @Override + public int id() { + return R.id.sitePage; + } } diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/theme/ThemePage.java b/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/theme/ThemePage.java index cf1b8e93..ca8feba0 100644 --- a/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/theme/ThemePage.java +++ b/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/theme/ThemePage.java @@ -151,4 +151,9 @@ public void create(Context context, Map params) { public View view() { return contentView; } + + @Override + public int id() { + return R.id.themePage; + } } diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/video/PlayerKernelModel.java b/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/video/PlayerKernelModel.java new file mode 100644 index 00000000..e73b9b04 --- /dev/null +++ b/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/video/PlayerKernelModel.java @@ -0,0 +1,27 @@ +package top.ourfor.app.iplayx.page.setting.video; + +import androidx.annotation.NonNull; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.With; +import top.ourfor.app.iplayx.common.type.PlayerKernelType; + +@Builder +@With +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PlayerKernelModel { + + @Builder.Default + PlayerKernelType type = PlayerKernelType.MPV; + String title; + + @NonNull + public String toString() { + return title != null ? title : super.toString(); + } +} diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/video/VideoPage.java b/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/video/VideoPage.java index 5da7b418..36c7060f 100644 --- a/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/video/VideoPage.java +++ b/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/video/VideoPage.java @@ -18,6 +18,7 @@ import lombok.val; import top.ourfor.app.iplayx.R; import top.ourfor.app.iplayx.common.annotation.ViewController; +import top.ourfor.app.iplayx.common.type.PlayerKernelType; import top.ourfor.app.iplayx.config.AppSetting; import top.ourfor.app.iplayx.common.type.VideoDecodeType; import top.ourfor.app.iplayx.page.Page; @@ -51,15 +52,28 @@ public void onCreate(@Nullable Bundle savedInstanceState) { break; } } + var kernelOptions = List.of( + new OptionModel<>(PlayerKernelType.MPV, getContext().getString(R.string.player_mpv)), + new OptionModel<>(PlayerKernelType.EXO, getContext().getString(R.string.player_exo)), + new OptionModel<>(PlayerKernelType.VLC, getContext().getString(R.string.player_vlc)) + ); + var defaultPlayerKernelOption = kernelOptions.get(0); + for (val option : kernelOptions) { + if (option.value == AppSetting.shared.playerKernel) { + defaultPlayerKernelOption = option; + break; + } + } settingModels = List.of( SettingModel.builder() - .title(getContext().getString(R.string.use_exo_player)) - .type(SettingType.SWITCH) - .value(AppSetting.shared.useExoPlayer) + .title(getContext().getString(R.string.select_player_kernel)) + .type(SettingType.SELECT) + .value(defaultPlayerKernelOption) + .options(kernelOptions) .onClick(object -> { - if (!(object instanceof Boolean)) return; - val value = (Boolean) object; - AppSetting.shared.useExoPlayer = value; + if (!(object instanceof OptionModel)) return; + val value = (OptionModel) object; + AppSetting.shared.playerKernel = value.value; AppSetting.shared.save(); }) .build(), @@ -140,4 +154,9 @@ public void create(Context context, Map params) { onCreate(null); onCreateView(LayoutInflater.from(context), null, null); } + + @Override + public int id() { + return R.id.videoPage; + } } diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/common/ScriptManageView.java b/android/app/src/main/java/top/ourfor/app/iplayx/page/web/ScriptManageView.java similarity index 78% rename from android/app/src/main/java/top/ourfor/app/iplayx/page/setting/common/ScriptManageView.java rename to android/app/src/main/java/top/ourfor/app/iplayx/page/web/ScriptManageView.java index db6b16c6..04fdaef8 100644 --- a/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/common/ScriptManageView.java +++ b/android/app/src/main/java/top/ourfor/app/iplayx/page/web/ScriptManageView.java @@ -1,41 +1,36 @@ -package top.ourfor.app.iplayx.page.setting.common; +package top.ourfor.app.iplayx.page.web; import static top.ourfor.app.iplayx.module.Bean.XGET; +import android.app.Dialog; import android.content.Context; import android.text.Editable; import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.Spinner; import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; import androidx.constraintlayout.widget.ConstraintLayout; -import com.google.android.flexbox.FlexboxLayout; - -import java.util.ArrayList; -import java.util.List; +import java.util.function.Consumer; import lombok.Getter; +import lombok.Setter; import lombok.val; import top.ourfor.app.iplayx.R; -import top.ourfor.app.iplayx.action.UpdateModelAction; import top.ourfor.app.iplayx.bean.KVStorage; import top.ourfor.app.iplayx.databinding.ScriptManageBinding; -import top.ourfor.app.iplayx.databinding.SettingCellBinding; import top.ourfor.app.iplayx.util.DeviceUtil; -import top.ourfor.app.iplayx.view.TagView; public class ScriptManageView extends ConstraintLayout { ScriptManageBinding binding = null; @Getter ScriptManageViewModel viewModel = new ScriptManageViewModel(); + @Getter @Setter + Consumer onSaveButtonClick; + public ScriptManageView(@NonNull Context context) { super(context); binding = ScriptManageBinding.inflate(LayoutInflater.from(context), this, true); @@ -48,8 +43,14 @@ private void bind() { viewModel.value = XGET(KVStorage.class).get("@script"); setupTextArea(); - binding.settingButton.setOnClickListener(v -> { - XGET(KVStorage.class).set("@script", viewModel.value); + binding.saveButton.setOnClickListener(v -> { + val kv = XGET(KVStorage.class); + if (kv != null) { + kv.set("@script", viewModel.value); + } + if (onSaveButtonClick != null) { + onSaveButtonClick.accept(v); + } }); if (!DeviceUtil.isTV) return; diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/page/web/ScriptManageViewModel.java b/android/app/src/main/java/top/ourfor/app/iplayx/page/web/ScriptManageViewModel.java new file mode 100644 index 00000000..8ea07140 --- /dev/null +++ b/android/app/src/main/java/top/ourfor/app/iplayx/page/web/ScriptManageViewModel.java @@ -0,0 +1,19 @@ +package top.ourfor.app.iplayx.page.web; + +import java.util.function.Consumer; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@NoArgsConstructor +@AllArgsConstructor +public class ScriptManageViewModel { + @Getter @Setter + String value; + + @Getter @Setter + Consumer onClick; + +} diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/page/web/WebPage.java b/android/app/src/main/java/top/ourfor/app/iplayx/page/web/WebPage.java index 7ce67238..615063c7 100644 --- a/android/app/src/main/java/top/ourfor/app/iplayx/page/web/WebPage.java +++ b/android/app/src/main/java/top/ourfor/app/iplayx/page/web/WebPage.java @@ -13,15 +13,12 @@ import android.view.ViewGroup; import android.webkit.JavascriptInterface; import android.webkit.WebResourceRequest; -import android.webkit.WebResourceResponse; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; -import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.constraintlayout.widget.ConstraintLayout; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import com.airbnb.lottie.LottieAnimationView; @@ -45,15 +42,10 @@ import top.ourfor.app.iplayx.databinding.WebPageBinding; import top.ourfor.app.iplayx.page.Activity; import top.ourfor.app.iplayx.page.Page; -import top.ourfor.app.iplayx.page.setting.common.ScriptManageView; -import top.ourfor.app.iplayx.page.setting.common.WebScriptMessage; import top.ourfor.app.iplayx.store.GlobalStore; import top.ourfor.app.iplayx.util.AnimationUtil; import top.ourfor.app.iplayx.util.DeviceUtil; import top.ourfor.app.iplayx.util.WindowUtil; -import top.ourfor.app.iplayx.view.infra.Button; -import top.ourfor.app.iplayx.view.infra.EditText; -import top.ourfor.app.iplayx.view.infra.TextView; import top.ourfor.app.iplayx.view.infra.Toolbar; import top.ourfor.app.iplayx.view.infra.ToolbarAction; @@ -211,11 +203,10 @@ void bind() { } void showScriptPanel() { - val drive = store.getDrive(); dialog = new BottomSheetDialog(getContext(), R.style.SiteBottomSheetDialog); - dialog.setOnDismissListener(dlg -> { - }); + dialog.setOnDismissListener(dlg -> { }); var view = new ScriptManageView(context); + view.setOnSaveButtonClick(v -> dialog.dismiss()); dialog.setContentView(view); var behavior = BottomSheetBehavior.from((View) view.getParent()); val height = (int) (DeviceUtil.screenSize(getContext()).getHeight() * 0.6); diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/common/WebScriptMessage.java b/android/app/src/main/java/top/ourfor/app/iplayx/page/web/WebScriptMessage.java similarity index 83% rename from android/app/src/main/java/top/ourfor/app/iplayx/page/setting/common/WebScriptMessage.java rename to android/app/src/main/java/top/ourfor/app/iplayx/page/web/WebScriptMessage.java index 08694b59..475d5513 100644 --- a/android/app/src/main/java/top/ourfor/app/iplayx/page/setting/common/WebScriptMessage.java +++ b/android/app/src/main/java/top/ourfor/app/iplayx/page/web/WebScriptMessage.java @@ -1,4 +1,4 @@ -package top.ourfor.app.iplayx.page.setting.common; +package top.ourfor.app.iplayx.page.web; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/util/FontUtil.java b/android/app/src/main/java/top/ourfor/app/iplayx/util/FontUtil.java new file mode 100644 index 00000000..59e76740 --- /dev/null +++ b/android/app/src/main/java/top/ourfor/app/iplayx/util/FontUtil.java @@ -0,0 +1,31 @@ +package top.ourfor.app.iplayx.util; + +import android.content.Context; +import android.graphics.Typeface; +import android.os.Build; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.style.TypefaceSpan; + +import androidx.core.content.res.ResourcesCompat; + +import lombok.val; +import top.ourfor.app.iplayx.R; + +public class FontUtil { + + public static SpannableString applyEmojiFont(Context context, String text, int fontResId) { + Typeface emojiTypeface = ResourcesCompat.getFont(context, fontResId); + SpannableString spannableString = new SpannableString(text); + for (int i = 0; i < text.length(); i++) { + if (Character.isSurrogate(text.charAt(i))) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + assert emojiTypeface != null; + spannableString.setSpan(new TypefaceSpan(emojiTypeface), i, i + 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + i++; + } + } + return spannableString; + } +} diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/view/player/Player.java b/android/app/src/main/java/top/ourfor/app/iplayx/view/player/Player.java index eeb05184..00967919 100644 --- a/android/app/src/main/java/top/ourfor/app/iplayx/view/player/Player.java +++ b/android/app/src/main/java/top/ourfor/app/iplayx/view/player/Player.java @@ -7,6 +7,7 @@ import top.ourfor.app.iplayx.view.video.PlayerEventListener; import top.ourfor.app.iplayx.view.video.PlayerSourceModel; +import top.ourfor.lib.mpv.TrackItem; public interface Player { default void setDelegate(PlayerEventListener delegate) {} @@ -28,24 +29,26 @@ default void jumpBackward(int seconds) {} default void jumpForward(int seconds) {} default void stop() {} default void resize(String newSize) {} - default List audios() { return null; } - default List subtitles() { return null; } + default List audios() { return null; } + default List subtitles() { return null; } default void loadSubtitle(List subtitles) {} default String currentSubtitleId() { return "no"; } default String currentAudioId() { return "no"; } default void setSubtitleFontName(String subtitleFontName) {} default void setSubtitleFontDirectory(String directory) {} + default void setSubtitleDelay(double delay) {} + default void setSubtitlePosition(double position) {} default void destroy() {} - void useSubtitle(String id); - void useAudio(String id); + default void useSubtitle(String id) {}; + default void useAudio(String id) {}; + default void useVideo(String id) {}; default void applyOption(Map option) {}; default void speedUp(float speed) {} default void speedDown(float speed) {} - default void useVideo(String id) {}; default void setLastWatchPosition(long lastWatchPosition) {}; diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/view/video/MPVPlayerViewModel.java b/android/app/src/main/java/top/ourfor/app/iplayx/view/video/MPVPlayerViewModel.java index 988c02aa..ef1134ca 100644 --- a/android/app/src/main/java/top/ourfor/app/iplayx/view/video/MPVPlayerViewModel.java +++ b/android/app/src/main/java/top/ourfor/app/iplayx/view/video/MPVPlayerViewModel.java @@ -337,6 +337,18 @@ public void seekRelative(int seconds) { mpv.command("seek", String.valueOf(seconds), "relative+exact"); } + @Override + public void setSubtitleDelay(double delay) { + if (mpv == null) return; + mpv.setOptionString("sub-delay", String.valueOf(delay)); + } + + @Override + public void setSubtitlePosition(double position) { + if (mpv == null) return; + mpv.setOptionString("sub-pos", String.valueOf(position)); + } + @Override public void destroy() { if (mpv == null) return; diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerAdvanceConfigView.java b/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerAdvanceConfigView.java new file mode 100644 index 00000000..9b5c16c9 --- /dev/null +++ b/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerAdvanceConfigView.java @@ -0,0 +1,70 @@ +package top.ourfor.app.iplayx.view.video; + +import android.content.Context; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; + +import androidx.annotation.NonNull; +import androidx.constraintlayout.widget.ConstraintLayout; + +import top.ourfor.app.iplayx.databinding.PlayerAdvanceConfigBinding; +import top.ourfor.app.iplayx.view.player.Player; + +public class PlayerAdvanceConfigView extends ConstraintLayout { + PlayerAdvanceConfigBinding binding; + Player player; + + public PlayerAdvanceConfigView(@NonNull Context context) { + super(context); + binding = PlayerAdvanceConfigBinding.inflate(LayoutInflater.from(context), this, true); + setup(); + bind(); + } + + void setup() { + + } + + void bind() { + binding.subDelay.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { + + } + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { + + } + + @Override + public void afterTextChanged(Editable editable) { + if (player != null) { + double delay = Double.parseDouble(editable.toString()); + player.setSubtitleDelay(delay); + } + } + }); + + binding.subPos.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { + + } + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { + + } + + @Override + public void afterTextChanged(Editable editable) { + if (player != null) { + double position = Double.parseDouble(editable.toString()); + player.setSubtitlePosition(position); + } + } + }); + } +} diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerContentView.java b/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerContentView.java index 414039b8..a36bfefc 100644 --- a/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerContentView.java +++ b/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerContentView.java @@ -22,7 +22,20 @@ public PlayerContentView(Context context) { } public void initialize(String configDir, String cacheDir, String fontDir) { - viewModel = AppSetting.shared.useExoPlayer ? new ExoPlayerViewModel(this) : new MPVPlayerViewModel(configDir, cacheDir, fontDir); + switch (AppSetting.shared.playerKernel) { + case MPV: + viewModel = new MPVPlayerViewModel(configDir, cacheDir, fontDir); + break; + case EXO: + viewModel = new ExoPlayerViewModel(this); + break; + case VLC: + viewModel = new VLCPlayerViewModel(this); + break; + default: + log.info("Using default player: MPV"); + viewModel = new MPVPlayerViewModel(configDir, cacheDir, fontDir); + } setZOrderMediaOverlay(false); // we need to call write-watch-later manually getHolder().addCallback(this); diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerControlView.java b/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerControlView.java index d8f50654..f9e5a068 100644 --- a/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerControlView.java +++ b/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerControlView.java @@ -53,6 +53,7 @@ public class PlayerControlView extends ConstraintLayout implements PlayerEventLi public PlayerControlItemView orientationButton; public PlayerControlItemView speedButton; public PlayerControlItemView commentButton; + public PlayerControlItemView advanceConfigButton; public TextView timeLabel; public TextView titleLabel; public PlayerSlider progressBar; @@ -133,6 +134,7 @@ private void setupUI() { orientationButton = binding.orientation; speedButton = binding.speed; commentButton = binding.comment; + advanceConfigButton = binding.advanceConfig; updatePlayerSlider(); } @@ -184,6 +186,7 @@ public void onStopTrackingTouch(SeekBar seekBar) { }); popup.show(); }); + binding.advanceConfig.setOnClickListener(v -> delegate.onAdvanceConfig()); binding.pipEnter.setOnClickListener(v -> delegate.onPipEnter()); binding.playlist.setOnClickListener(v -> delegate.onTapPlaylist()); binding.comment.setOnClickListener(v -> delegate.onTapComment()); diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerEventListener.java b/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerEventListener.java index 4f91ca46..a39754ed 100644 --- a/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerEventListener.java +++ b/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerEventListener.java @@ -12,4 +12,5 @@ default void onTapPlaylist() {} default void onTapComment() {} default void onOrientationChange() {}; + default void onAdvanceConfig() {} } diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerEventView.java b/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerEventView.java index 2d8af712..dcb65375 100644 --- a/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerEventView.java +++ b/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerEventView.java @@ -23,6 +23,8 @@ import lombok.Setter; import lombok.extern.slf4j.Slf4j; import lombok.val; +import top.ourfor.app.iplayx.R; +import top.ourfor.app.iplayx.util.DeviceUtil; import top.ourfor.app.iplayx.util.LayoutUtil; import top.ourfor.app.iplayx.view.infra.RotateGestureDetector; import top.ourfor.lib.mpv.TrackItem; @@ -189,12 +191,20 @@ public void showSelectView(List items) { } public void showSelectView(List items, String currentId) { + showSelectView(items, currentId, ""); + } + + public void showSelectView(List items, String currentId, String prefixText) { if (selectView != null) return; Context context = getContext(); - List subtitles = items.stream() - .map(item -> new PlayerSelectModel(item, String.valueOf(item.id).equals(currentId))) + List tracks = items.stream() + .map(item -> { + item.setPrefix(prefixText); + return new PlayerSelectModel(item, String.valueOf(item.id).equals(currentId), prefixText); + }) .collect(Collectors.toList()); - selectView = new PlayerSelectView(context, subtitles); + selectView = new PlayerSelectView(context, tracks); + selectView.setBackgroundResource(R.drawable.dialog_alpha_bg); selectView.setDelegate(this); LayoutParams layout = new LayoutParams(0, 0); layout.leftToLeft = LayoutParams.PARENT_ID; @@ -202,9 +212,9 @@ public void showSelectView(List items, String currentId) { layout.rightToRight = LayoutParams.PARENT_ID; layout.bottomToBottom = LayoutParams.PARENT_ID; layout.matchConstraintPercentHeight = 0.75f; - layout.matchConstraintMaxHeight = 640; + layout.matchConstraintMaxHeight = (int) (DeviceUtil.screenSize(context).getHeight() * 0.65); layout.matchConstraintPercentWidth = 0.5f; - layout.matchConstraintMaxWidth = 800; + layout.matchConstraintMaxWidth = (int) (DeviceUtil.screenSize(context).getWidth() * 0.5); post(() -> { addView(selectView, layout); requestLayout(); diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerSelectAdapter.java b/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerSelectAdapter.java index 49b04d44..6c80ca4e 100644 --- a/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerSelectAdapter.java +++ b/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerSelectAdapter.java @@ -1,12 +1,19 @@ package top.ourfor.app.iplayx.view.video; +import android.annotation.SuppressLint; import android.graphics.Color; +import android.graphics.Typeface; +import android.os.Build; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.style.TypefaceSpan; import android.util.Log; import android.view.ViewGroup; import android.widget.LinearLayout; import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.core.content.res.ResourcesCompat; import androidx.recyclerview.widget.RecyclerView; import java.util.List; @@ -14,6 +21,9 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; +import lombok.val; +import top.ourfor.app.iplayx.R; +import top.ourfor.app.iplayx.util.FontUtil; @Getter @Setter @@ -28,7 +38,6 @@ public class PlayerSelectAdapter extends RecyclerView.Adapter model = localDataSet.get(position); T text = localDataSet.get(position).item; - holder.getTextView() - .setText(text.toString()); + val textView = holder.getTextView(); + val spannableString = FontUtil.applyEmojiFont(textView.getContext(), text.toString(), R.font.twemoji_mozilla); + textView.setText(spannableString); holder.rootView.setOnClickListener(v -> { PlayerSelectModel item = localDataSet.get(position); if (!multiSelectSupport) { @@ -56,7 +67,6 @@ public void onBindViewHolder(@NonNull ViewHolder holder, int position) { else delegate.onDeselect(item); } notifyDataSetChanged(); - Log.d(TAG, "position: " + position + " data: " + item); }); holder.rootView.setIsSelected(model.isSelected); } diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerSelectItemView.java b/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerSelectItemView.java index 691bc4fb..a51bcbfa 100644 --- a/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerSelectItemView.java +++ b/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerSelectItemView.java @@ -5,37 +5,32 @@ import android.content.res.ColorStateList; import android.graphics.Color; import android.graphics.drawable.GradientDrawable; -import android.view.ViewGroup; -import android.widget.ImageView; +import android.view.LayoutInflater; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.constraintlayout.widget.ConstraintLayout; import lombok.Getter; -import top.ourfor.app.iplayx.R; +import top.ourfor.app.iplayx.databinding.PlayerSelectItemBinding; import top.ourfor.app.iplayx.util.DeviceUtil; @Getter public class PlayerSelectItemView extends ConstraintLayout { - private TextView textView; - private ImageView iconView; - private static int iconSize = 36; - private static int normalColor = Color.parseColor("#506200EE"); - private static int focusColor = Color.parseColor("#FF6200EE"); + static int iconSize = 36; + static int normalColor = Color.parseColor("#506200EE"); + static int focusColor = Color.parseColor("#FF6200EE"); + + PlayerSelectItemBinding binding; @SuppressLint("ResourceType") public PlayerSelectItemView(@NonNull Context context) { super(context); - iconView = new ImageView(context); - textView = new TextView(context); + binding = PlayerSelectItemBinding.inflate(LayoutInflater.from(context), this, true); setupUI(context); } private void setupUI(Context context) { - addView(iconView, iconViewLayout()); - addView(textView, textViewLayout()); - GradientDrawable gradientDrawable = new GradientDrawable(); gradientDrawable.setColor(normalColor); gradientDrawable.setCornerRadius(DeviceUtil.dpToPx(5)); @@ -53,30 +48,19 @@ private void setupUI(Context context) { public void setIsSelected(boolean flag) { int icon = NO_ID; if (flag) { - icon = R.drawable.checkmark; + icon = com.microsoft.fluent.mobile.icons.R.drawable.ic_fluent_checkbox_checked_24_filled; + } else { + icon = com.microsoft.fluent.mobile.icons.R.drawable.ic_fluent_checkbox_unchecked_24_filled; } - iconView.setImageResource(icon); - iconView.setImageTintList(ColorStateList.valueOf(Color.WHITE)); + binding.itemIcon.setImageResource(icon); + binding.itemIcon.setImageTintList(ColorStateList.valueOf(Color.WHITE)); } - private LayoutParams textViewLayout() { - LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); - params.topMargin = 5; - params.bottomMargin = 5; - params.leftMargin = iconSize + 30; - params.topToTop = LayoutParams.PARENT_ID; - params.bottomToBottom = LayoutParams.PARENT_ID; - return params; + public void setText(String text) { + binding.nameLabel.setText(text); } - private LayoutParams iconViewLayout() { - LayoutParams params = new LayoutParams(iconSize, iconSize); - params.topMargin = 5; - params.bottomMargin = 5; - params.topToTop = LayoutParams.PARENT_ID; - params.leftMargin = 20; - params.bottomToBottom = LayoutParams.PARENT_ID; - return params; + public TextView getTextView() { + return binding.nameLabel; } - } diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerSelectModel.java b/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerSelectModel.java index ff9f680c..f4be4f02 100644 --- a/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerSelectModel.java +++ b/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerSelectModel.java @@ -16,4 +16,5 @@ public class PlayerSelectModel { T item; boolean isSelected; + String prefixText; } diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerSelectView.java b/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerSelectView.java index 66fbf86e..821eb8b8 100644 --- a/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerSelectView.java +++ b/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerSelectView.java @@ -1,6 +1,7 @@ package top.ourfor.app.iplayx.view.video; +import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Color; import android.graphics.drawable.GradientDrawable; @@ -15,11 +16,11 @@ import lombok.Setter; +@SuppressLint("ViewConstructor") public class PlayerSelectView extends ConstraintLayout implements PlayerSelectDelegate> { - private static int CLOSE_ICON_SIZE = 56; + RecyclerView listView; + PlayerSelectAdapter listViewModel; - private RecyclerView listView; - private PlayerSelectAdapter listViewModel; @Setter private List> datasource; @Setter diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerView.java b/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerView.java index fc12abb8..e2d9606f 100644 --- a/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerView.java +++ b/android/app/src/main/java/top/ourfor/app/iplayx/view/video/PlayerView.java @@ -8,16 +8,19 @@ import android.animation.ObjectAnimator; import android.app.Activity; +import android.app.Dialog; import android.content.Context; import android.content.pm.ActivityInfo; import android.content.res.AssetManager; import android.graphics.Color; import android.media.AudioManager; import android.os.Build; +import android.util.Size; import android.view.KeyEvent; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; +import android.widget.Button; import android.widget.ImageView; import androidx.annotation.NonNull; @@ -212,7 +215,8 @@ void setupUI(Context context, String url) throws IOException { controlView.pipButton, controlView.playlistButton, controlView.orientationButton, - controlView.speedButton + controlView.speedButton, + controlView.advanceConfigButton ); eventView.delegate = this; eventView.trackSelectDelegate = this; @@ -408,13 +412,31 @@ public void onPipEnter() { XGET(Activity.class).enterPictureInPictureMode(); } + @Override + public void onAdvanceConfig() { + var dialog = new Dialog(getContext(), R.style.PlayerAdvanceConfigDialog); + var contentView = new PlayerAdvanceConfigView(getContext()); + contentView.player = this.controlView.player; + dialog.setContentView(contentView); + Window window = dialog.getWindow(); + if (window != null) { + Size size = DeviceUtil.screenSize(getContext()); + WindowManager.LayoutParams dialogLayoutParams = new WindowManager.LayoutParams(); + dialogLayoutParams.copyFrom(window.getAttributes()); + dialogLayoutParams.width = (int) (size.getWidth() * 0.5); + dialogLayoutParams.height = (int) (size.getHeight() * 0.8); + window.setAttributes(dialogLayoutParams); + } + dialog.show(); + } + @Override public void onSelectSubtitle() { var player = contentView.viewModel; var currentSubtitleId = player.currentSubtitleId(); var subtitles = (List)player.subtitles(); controlView.updateControlVisible(false); - eventView.showSelectView(subtitles, currentSubtitleId); + eventView.showSelectView(subtitles, currentSubtitleId, "\uD83D\uDCD1 "); } @Override @@ -428,7 +450,7 @@ public void onSelectVideo() { .type(VideoTrackName) .build()).collect(Collectors.toList()); controlView.updateControlVisible(false); - eventView.showSelectView(videos, currentSubtitleId); + eventView.showSelectView(videos, currentSubtitleId, "\uD83C\uDFA5 "); } @Override @@ -437,7 +459,7 @@ public void onSelectAudio() { var currentAudioId = player.currentAudioId(); var audios = (List)player.audios(); controlView.updateControlVisible(false); - eventView.showSelectView(audios, currentAudioId); + eventView.showSelectView(audios, currentAudioId, "\uD83D\uDD0A "); } void copySubtitleFont(String configDir) throws IOException { diff --git a/android/app/src/main/java/top/ourfor/app/iplayx/view/video/VLCPlayerViewModel.java b/android/app/src/main/java/top/ourfor/app/iplayx/view/video/VLCPlayerViewModel.java new file mode 100644 index 00000000..c067f974 --- /dev/null +++ b/android/app/src/main/java/top/ourfor/app/iplayx/view/video/VLCPlayerViewModel.java @@ -0,0 +1,234 @@ +package top.ourfor.app.iplayx.view.video; + +import static top.ourfor.lib.mpv.TrackItem.AudioTrackName; +import static top.ourfor.lib.mpv.TrackItem.SubtitleTrackName; + +import android.net.Uri; +import android.util.Size; +import android.view.SurfaceHolder; +import android.view.View; + +import org.videolan.libvlc.LibVLC; +import org.videolan.libvlc.Media; +import org.videolan.libvlc.MediaPlayer; +import org.videolan.libvlc.interfaces.IMedia; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import top.ourfor.app.iplayx.view.player.Player; +import top.ourfor.lib.mpv.TrackItem; + +@Slf4j +public class VLCPlayerViewModel implements Player { + LibVLC libVLC; + MediaPlayer mediaPlayer; + PlayerEventListener delegate; + + public VLCPlayerViewModel(View view) { + var options = List.of("--avcodec-hw=any"); + var context = view.getContext(); + libVLC = new LibVLC(context, options); + mediaPlayer = new MediaPlayer(libVLC); + } + + @Override + public void setDelegate(PlayerEventListener delegate) { + this.delegate = delegate; + mediaPlayer.setEventListener(event -> { + switch (event.type) { + case MediaPlayer.Event.EndReached: + delegate.onPropertyChange(PlayerPropertyType.EofReached, true); + break; + case MediaPlayer.Event.Playing: + delegate.onPropertyChange(PlayerPropertyType.PausedForCache, false); + break; + case MediaPlayer.Event.Paused: + delegate.onPropertyChange(PlayerPropertyType.Pause, true); + break; + case MediaPlayer.Event.TimeChanged: + if (isPlaying()) { + delegate.onPropertyChange(PlayerPropertyType.TimePos, mediaPlayer.getTime() / 1000.0); + delegate.onPropertyChange(PlayerPropertyType.PausedForCache, false); + } + break; + case MediaPlayer.Event.Buffering: + delegate.onPropertyChange(PlayerPropertyType.PausedForCache, true); + break; + case MediaPlayer.Event.LengthChanged: + if (isPlaying()) delegate.onPropertyChange(PlayerPropertyType.Duration, mediaPlayer.getLength() / 1000.0); + break; + } + }); + } + + @Override + public void attach(SurfaceHolder holder) { + mediaPlayer.getVLCVout().setVideoSurface(holder.getSurface(), holder); + val size = new Size(holder.getSurfaceFrame().width(), holder.getSurfaceFrame().height()); + mediaPlayer.getVLCVout().setWindowSize(size.getWidth(), size.getHeight()); + mediaPlayer.getVLCVout().attachViews(); + } + + @Override + public void resize(String newSize) { + // size: width x height + var size = newSize.split("x"); + mediaPlayer.getVLCVout().setWindowSize(Integer.parseInt(size[0]), Integer.parseInt(size[1])); + } + + @Override + public void detach() { + mediaPlayer.getVLCVout().detachViews(); + } + + @Override + public boolean isPlaying() { + return mediaPlayer.isPlaying(); + } + + @Override + public void resume() { + mediaPlayer.play(); + } + + @Override + public void pause() { + mediaPlayer.pause(); + } + + @Override + public Double progress() { + return (double) mediaPlayer.getTime() / 1000.0; + } + + @Override + public Double duration() { + return (double) mediaPlayer.getLength() / 1000.0; + } + + @Override + public void loadVideo(String url) { + log.info("load video: {}", url); + var media = new Media(libVLC, Uri.parse(url)); + mediaPlayer.setMedia(media); + media.release(); + mediaPlayer.play(); + } + + @Override + public void loadVideo(String url, String audioUrl) { + log.info("load video: {}, audio: {}", url, audioUrl); + var media = new Media(libVLC, Uri.parse(url)); + mediaPlayer.setMedia(media); + media.release(); + mediaPlayer.play(); + } + + @Override + public void jumpForward(int seconds) { + mediaPlayer.setTime(mediaPlayer.getTime() + seconds * 1000L); + } + + @Override + public void jumpBackward(int seconds) { + mediaPlayer.setTime(mediaPlayer.getTime() - seconds * 1000L); + } + + @Override + public void speed(float speed) { + mediaPlayer.setRate(speed); + } + + @Override + public void speedDown(float speed) { + mediaPlayer.setRate(mediaPlayer.getRate() - speed); + } + + @Override + public void speedUp(float speed) { + mediaPlayer.setRate(mediaPlayer.getRate() + speed); + } + + @Override + public String currentSubtitleId() { + val selectedTrack = mediaPlayer.getSelectedTrack(IMedia.Track.Type.Text); + return selectedTrack != null ? selectedTrack.id : ""; + } + + @Override + public String currentAudioId() { + val selectedTrack = mediaPlayer.getSelectedTrack(IMedia.Track.Type.Audio); + return selectedTrack != null ? selectedTrack.id : ""; + } + + @Override + public void useSubtitle(String id) { + mediaPlayer.selectTracks(IMedia.Track.Type.Text, id); + } + + @Override + public void useAudio(String id) { + mediaPlayer.selectTracks(IMedia.Track.Type.Audio, id); + } + + @Override + public void useVideo(String id) { + mediaPlayer.selectTracks(IMedia.Track.Type.Video, id); + } + + @Override + public void seek(long timeInSeconds) { + mediaPlayer.setTime(timeInSeconds * 1000); + } + + @Override + public List subtitles() { + val tracks = mediaPlayer.getTracks(IMedia.Track.Type.Text); + if (tracks == null) return List.of(); + return Arrays.stream(tracks).map(track -> { + val builder = TrackItem.builder(); + builder.id(String.valueOf(track.id)); + builder.title(track.name); + builder.type(SubtitleTrackName); + builder.lang(track.language); + return builder.build(); + }).collect(Collectors.toList()); + } + + @Override + public List audios() { + val tracks = mediaPlayer.getTracks(IMedia.Track.Type.Audio); + if (tracks == null) return List.of(); + return Arrays.stream(tracks).map(track -> { + val builder = TrackItem.builder(); + builder.id(String.valueOf(track.id)); + builder.title(track.name); + builder.type(AudioTrackName); + builder.lang(track.language); + return builder.build(); + }).collect(Collectors.toList()); + } + + + @Override + public void destroy() { + try { + if (mediaPlayer != null && !mediaPlayer.isReleased()) { + mediaPlayer.stop(); + mediaPlayer.getVLCVout().detachViews(); + mediaPlayer.release(); + mediaPlayer = null; + } + if (libVLC != null && !libVLC.isReleased()) { + libVLC.release(); + libVLC = null; + } + } catch (Exception e) { + log.error("destroy error", e); + } + } +} diff --git a/android/app/src/main/java/top/ourfor/lib/mpv/TrackItem.java b/android/app/src/main/java/top/ourfor/lib/mpv/TrackItem.java index 8a5ace7d..99118416 100644 --- a/android/app/src/main/java/top/ourfor/lib/mpv/TrackItem.java +++ b/android/app/src/main/java/top/ourfor/lib/mpv/TrackItem.java @@ -14,18 +14,20 @@ @NoArgsConstructor @AllArgsConstructor public class TrackItem { + public static String VideoTrackName = "video"; + public static String AudioTrackName = "audio"; + public static String SubtitleTrackName = "sub"; + public String lang; public String type; public String title; public String id; + public String prefix; - public static String VideoTrackName = "video"; - public static String AudioTrackName = "audio"; - public static String SubtitleTrackName = "sub"; @NonNull @Override public String toString() { - return lang + " " + title; + return prefix + lang + " " + title; } } diff --git a/android/app/src/main/res/drawable/bg_spinner.xml b/android/app/src/main/res/drawable/bg_spinner.xml new file mode 100644 index 00000000..bc596820 --- /dev/null +++ b/android/app/src/main/res/drawable/bg_spinner.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/dialog_alpha_bg.xml b/android/app/src/main/res/drawable/dialog_alpha_bg.xml new file mode 100644 index 00000000..8c57d6af --- /dev/null +++ b/android/app/src/main/res/drawable/dialog_alpha_bg.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/dialog_bg.xml b/android/app/src/main/res/drawable/dialog_bg.xml index 252dd011..c0a2f20a 100644 --- a/android/app/src/main/res/drawable/dialog_bg.xml +++ b/android/app/src/main/res/drawable/dialog_bg.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/android/app/src/main/res/layout/media_page.xml b/android/app/src/main/res/layout/media_page.xml index d5252960..cf25db3a 100644 --- a/android/app/src/main/res/layout/media_page.xml +++ b/android/app/src/main/res/layout/media_page.xml @@ -1,179 +1,186 @@ - - - - - - - - - - + android:layout_height="match_parent" + android:background="@drawable/bg" + android:scrollbars="none"> - - - - - - - - - - - - - - - + android:layout_height="wrap_content"> + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/player_advance_config.xml b/android/app/src/main/res/layout/player_advance_config.xml new file mode 100644 index 00000000..1e1962de --- /dev/null +++ b/android/app/src/main/res/layout/player_advance_config.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/player_config_panel.xml b/android/app/src/main/res/layout/player_config_panel.xml new file mode 100644 index 00000000..f2493359 --- /dev/null +++ b/android/app/src/main/res/layout/player_config_panel.xml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/player_control.xml b/android/app/src/main/res/layout/player_control.xml index 4cfd7c5b..ae01c7a3 100644 --- a/android/app/src/main/res/layout/player_control.xml +++ b/android/app/src/main/res/layout/player_control.xml @@ -88,6 +88,19 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" /> + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/script_manage.xml b/android/app/src/main/res/layout/script_manage.xml index e2332499..59bc1636 100644 --- a/android/app/src/main/res/layout/script_manage.xml +++ b/android/app/src/main/res/layout/script_manage.xml @@ -37,7 +37,7 @@ /> #93000A #FFDAD6 #10140F + #8010140F #E0E4DB #10140F #E0E4DB diff --git a/android/app/src/main/res/values-night/strings.xml b/android/app/src/main/res/values-night/strings.xml index 3a47b61d..bd131e03 100644 --- a/android/app/src/main/res/values-night/strings.xml +++ b/android/app/src/main/res/values-night/strings.xml @@ -126,4 +126,15 @@ 类似的影片 Web主页地址 注入JS代码 + 播放 + 视频源 + 音频源 + 字幕源 + 调整字幕延时 + 字幕显示位置偏移 + 对于部分播放器实现,某些配置可能无效 + 播放器实现 + VLC + MPV + EXO \ No newline at end of file diff --git a/android/app/src/main/res/values-television-night/strings.xml b/android/app/src/main/res/values-television-night/strings.xml index b21299be..4a5cc0d9 100644 --- a/android/app/src/main/res/values-television-night/strings.xml +++ b/android/app/src/main/res/values-television-night/strings.xml @@ -81,4 +81,15 @@ 类似的影片 Web主页地址 注入JS代码 + 播放 + 视频源 + 音频源 + 字幕源 + 调整字幕延时 + 字幕显示位置偏移 + 对于部分播放器实现,某些配置可能无效 + 播放器实现 + VLC + MPV + EXO \ No newline at end of file diff --git a/android/app/src/main/res/values-television/strings.xml b/android/app/src/main/res/values-television/strings.xml index b21299be..4a5cc0d9 100644 --- a/android/app/src/main/res/values-television/strings.xml +++ b/android/app/src/main/res/values-television/strings.xml @@ -81,4 +81,15 @@ 类似的影片 Web主页地址 注入JS代码 + 播放 + 视频源 + 音频源 + 字幕源 + 调整字幕延时 + 字幕显示位置偏移 + 对于部分播放器实现,某些配置可能无效 + 播放器实现 + VLC + MPV + EXO \ No newline at end of file diff --git a/android/app/src/main/res/values-zh/strings.xml b/android/app/src/main/res/values-zh/strings.xml index 0414a099..8139612a 100644 --- a/android/app/src/main/res/values-zh/strings.xml +++ b/android/app/src/main/res/values-zh/strings.xml @@ -136,4 +136,15 @@ 类似的影片 Web主页地址 注入JS代码 + 播放 + 视频源 + 音频源 + 字幕源 + 调整字幕延时 + 字幕显示位置偏移 + 对于部分播放器实现,某些配置可能无效 + 播放器实现 + VLC + MPV + EXO \ No newline at end of file diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml index 9b74f034..7ef32cb1 100644 --- a/android/app/src/main/res/values/colors.xml +++ b/android/app/src/main/res/values/colors.xml @@ -32,6 +32,7 @@ #FFDAD6 #410002 #F7FBF1 + #80F7FBF1 #181D17 #F7FBF1 #181D17 diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index f3d7eae3..2d842930 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -136,4 +136,15 @@ 类似的影片 Web主页地址 注入JS代码 + 播放 + 视频源 + 音频源 + 字幕源 + 调整字幕延时 + 字幕显示位置偏移 + 对于部分播放器实现,某些配置可能无效 + 播放器实现 + VLC + MPV + EXO \ No newline at end of file diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index ad569c25..9a071ba4 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -37,6 +37,13 @@ adjustResize + +