diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt index 049f570e..f17ab36d 100644 --- a/app/src/main/java/com/futo/platformplayer/Settings.kt +++ b/app/src/main/java/com/futo/platformplayer/Settings.kt @@ -470,15 +470,22 @@ class Settings : FragmentedStorageFileJson() { @DropdownFieldOptionsId(R.array.rotation_zone) var rotationZone: Int = 2; - @FormField(R.string.prefer_webm, FieldForm.TOGGLE, R.string.prefer_webm_description, 16) + @FormField(R.string.stability_threshold_time, FieldForm.DROPDOWN, R.string.stability_threshold_time_description, 16) + @DropdownFieldOptionsId(R.array.rotation_threshold_time) + var stabilityThresholdTime: Int = 1; + + @FormField(R.string.full_autorotate_lock, FieldForm.TOGGLE, R.string.full_autorotate_lock_description, 17) + var fullAutorotateLock: Boolean = false; + + @FormField(R.string.prefer_webm, FieldForm.TOGGLE, R.string.prefer_webm_description, 18) var preferWebmVideo: Boolean = false; - @FormField(R.string.prefer_webm_audio, FieldForm.TOGGLE, R.string.prefer_webm_audio_description, 17) + @FormField(R.string.prefer_webm_audio, FieldForm.TOGGLE, R.string.prefer_webm_audio_description, 19) var preferWebmAudio: Boolean = false; - @FormField(R.string.allow_under_cutout, FieldForm.TOGGLE, R.string.allow_under_cutout_description, 18) + @FormField(R.string.allow_under_cutout, FieldForm.TOGGLE, R.string.allow_under_cutout_description, 20) var allowVideoToGoUnderCutout: Boolean = true; - @FormField(R.string.autoplay, FieldForm.TOGGLE, R.string.autoplay, 19) + @FormField(R.string.autoplay, FieldForm.TOGGLE, R.string.autoplay, 21) var autoplay: Boolean = false; } diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt index 8a6b40b0..2b6deaf8 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt @@ -237,7 +237,8 @@ open class JSClient : IPlatformClient { hasGetLiveChatWindow = plugin.executeBoolean("!!source.getLiveChatWindow") ?: false, hasGetContentChapters = plugin.executeBoolean("!!source.getContentChapters") ?: false, hasPeekChannelContents = plugin.executeBoolean("!!source.peekChannelContents") ?: false, - hasGetChannelPlaylists = plugin.executeBoolean("!!source.getChannelPlaylists") ?: false + hasGetChannelPlaylists = plugin.executeBoolean("!!source.getChannelPlaylists") ?: false, + hasGetContentRecommendations = plugin.executeBoolean("!!source.getContentRecommendations") ?: false ); try { diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt index 56b3de79..7c0fe870 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt @@ -216,26 +216,30 @@ class MenuBottomBarFragment : MainActivityFragment() { _moreButtons.clear(); _layoutMoreButtons.removeAllViews(); + var insertedButtons = 0; //Force buy to be on top for more buttons val buyIndex = buttons.indexOfFirst { b -> b.id == 98 }; if (buyIndex != -1) { val button = buttons[buyIndex] buttons.removeAt(buyIndex) buttons.add(0, button) + insertedButtons++; } //Force faq to be second val faqIndex = buttons.indexOfFirst { b -> b.id == 97 }; if (faqIndex != -1) { val button = buttons[faqIndex] buttons.removeAt(faqIndex) - buttons.add(if (buttons.size == 1) 1 else 0, button) + buttons.add(if (insertedButtons == 1) 1 else 0, button) + insertedButtons++; } //Force privacy to be third val privacyIndex = buttons.indexOfFirst { b -> b.id == 96 }; if (privacyIndex != -1) { val button = buttons[privacyIndex] buttons.removeAt(privacyIndex) - buttons.add(if (buttons.size == 2) 2 else 1, button) + buttons.add(if (insertedButtons == 2) 2 else (if(insertedButtons == 1) 1 else 0), button) + insertedButtons++; } for (data in buttons) { @@ -327,19 +331,6 @@ class MenuBottomBarFragment : MainActivityFragment() { if (!StatePayment.instance.hasPaid) { newCurrentButtonDefinitions.add(ButtonDefinition(98, R.drawable.ic_paid, R.drawable.ic_paid_filled, R.string.buy, canToggle = false, { it.currentMain is BuyFragment }, { it.navigate() })) } - newCurrentButtonDefinitions.add(ButtonDefinition(97, R.drawable.ic_quiz, R.drawable.ic_quiz_fill, R.string.faq, canToggle = false, { false }, { - it.navigate(Settings.URL_FAQ); - })) - newCurrentButtonDefinitions.add(ButtonDefinition(96, R.drawable.ic_disabled_visible, R.drawable.ic_disabled_visible, R.string.privacy_mode, canToggle = false, { false }, { - UIDialogs.showDialog(context, R.drawable.ic_disabled_visible_purple, "Privacy Mode", - "All requests will be processed anonymously (unauthenticated), playback and history tracking will be disabled.\n\nTap the icon to disable.", null, 0, - UIDialogs.Action("Cancel", { - StateApp.instance.setPrivacyMode(false); - }, UIDialogs.ActionStyle.NONE), - UIDialogs.Action("Enable", { - StateApp.instance.setPrivacyMode(true); - }, UIDialogs.ActionStyle.PRIMARY)); - })) //Add conditional buttons here, when you add a conditional button, be sure to add the register and unregister events for when the button needs to be updated @@ -412,6 +403,19 @@ class MenuBottomBarFragment : MainActivityFragment() { if (c is Activity) { c.overridePendingTransition(R.anim.slide_in_up, R.anim.slide_darken); } + }), + ButtonDefinition(96, R.drawable.ic_disabled_visible, R.drawable.ic_disabled_visible, R.string.privacy_mode, canToggle = true, { false }, { + UIDialogs.showDialog(it.context ?: return@ButtonDefinition, R.drawable.ic_disabled_visible_purple, "Privacy Mode", + "All requests will be processed anonymously (unauthenticated), playback and history tracking will be disabled.\n\nTap the icon to disable.", null, 0, + UIDialogs.Action("Cancel", { + StateApp.instance.setPrivacyMode(false); + }, UIDialogs.ActionStyle.NONE), + UIDialogs.Action("Enable", { + StateApp.instance.setPrivacyMode(true); + }, UIDialogs.ActionStyle.PRIMARY)); + }), + ButtonDefinition(97, R.drawable.ic_quiz, R.drawable.ic_quiz_fill, R.string.faq, canToggle = true, { false }, { + it.navigate(Settings.URL_FAQ); }) //96 is reserved for privacy button //98 is reserved for buy button diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SourceDetailFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SourceDetailFragment.kt index 7f4ea091..c5f3d430 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SourceDetailFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SourceDetailFragment.kt @@ -397,23 +397,43 @@ class SourceDetailFragment : MainFragment() { UIDialogs.Action("Cancel", {}, UIDialogs.ActionStyle.NONE), UIDialogs.Action("Login", { LoginActivity.showLogin(StateApp.instance.context, config) { - StatePlugins.instance.setPluginAuth(config.id, it); - reloadSource(config.id); + try { + StatePlugins.instance.setPluginAuth(config.id, it); + reloadSource(config.id); + } catch (e: Throwable) { + StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { + context?.let { c -> UIDialogs.showGeneralErrorDialog(c, "Failed to set plugin authentication (loginSource, loginWarning)", e) } + } + Logger.e(TAG, "Failed to set plugin authentication (loginSource, loginWarning)", e) + } }; }, UIDialogs.ActionStyle.PRIMARY)) } else LoginActivity.showLogin(StateApp.instance.context, config) { - StatePlugins.instance.setPluginAuth(config.id, it); - reloadSource(config.id); + try { + StatePlugins.instance.setPluginAuth(config.id, it); + reloadSource(config.id); + } catch (e: Throwable) { + StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { + context?.let { c -> UIDialogs.showGeneralErrorDialog(c, "Failed to set plugin authentication (loginSource)", e) } + } + Logger.e(TAG, "Failed to set plugin authentication (loginSource)", e) + } }; } private fun logoutSource(clear: Boolean = true) { val config = _config ?: return; - StatePlugins.instance.setPluginAuth(config.id, null); - reloadSource(config.id); - + try { + StatePlugins.instance.setPluginAuth(config.id, null); + reloadSource(config.id); + } catch (e: Throwable) { + StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { + context?.let { c -> UIDialogs.showGeneralErrorDialog(c, "Failed to clear plugin authentication", e) } + } + Logger.e(TAG, "Failed to clear plugin authentication", e) + } //TODO: Maybe add a dialog option.. if(Settings.instance.plugins.clearCookiesOnLogout && clear) { diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt index 14881672..6117b646 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt @@ -116,17 +116,18 @@ class VideoDetailFragment : MainFragment { fun updateOrientation() { val a = activity ?: return // only applies to small windows - val isFullScreenPortraitAllowed = Settings.instance.playback.fullscreenPortrait; - val isReversePortraitAllowed = Settings.instance.playback.reversePortrait; + val isFullScreenPortraitAllowed = Settings.instance.playback.fullscreenPortrait + val isReversePortraitAllowed = Settings.instance.playback.reversePortrait + val fullAutorotateLock = Settings.instance.playback.fullAutorotateLock val rotationLock = StatePlayer.instance.rotationLock // For small windows if the device isn't landscape right now and full screen portrait isn't allowed then we should force landscape if (isSmallWindow && isFullscreen && !isFullScreenPortraitAllowed && resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT && !rotationLock) { - activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE + a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE } // For small windows if the device isn't in a portrait orientation and we're in the maximized state then we should force portrait else if (isSmallWindow && !isMinimizing && !isFullscreen && state == State.MAXIMIZED && resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { - activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT + a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT } else if (rotationLock) { a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED } else { diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index f394693d..aaa60eb2 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -2364,20 +2364,11 @@ class VideoDetailView : ConstraintLayout { _layoutRecommended.visibility = View.VISIBLE _commentsList.clear() - val url = _url - if (url != null) { - _layoutRecommended.addView(LoaderView(context).apply { - layoutParams = LinearLayout.LayoutParams(60.dp(resources), 60.dp(resources)) - start() - }) - _taskLoadRecommendations.run(url) - } else { - _layoutRecommended.addView(TextView(context).apply { - layoutParams = LinearLayout.LayoutParams(60.dp(resources), 60.dp(resources)) - textSize = 12.0f - text = "No recommendations found" - }) - } + _layoutRecommended.addView(LoaderView(context).apply { + layoutParams = LinearLayout.LayoutParams(60.dp(resources), 60.dp(resources)) + start() + }) + _taskLoadRecommendations.run(null) } } @@ -2389,7 +2380,7 @@ class VideoDetailView : ConstraintLayout { if (_tabIndex == 2) { _layoutRecommended.removeAllViews() - if (results == null) { + if (results == null || results.isEmpty()) { _layoutRecommended.addView(TextView(context).apply { layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT).apply { setMargins(20.dp(resources), 20.dp(resources), 20.dp(resources), 20.dp(resources)) @@ -2787,7 +2778,15 @@ class VideoDetailView : ConstraintLayout { } } else TaskHandler(IPlatformVideoDetails::class.java, {fragment.lifecycleScope}); - private val _taskLoadRecommendations = TaskHandler?>(StateApp.instance.scopeGetter, { video?.getContentRecommendations(StatePlatform.instance.getContentClient(it)) }) + private val _taskLoadRecommendations = TaskHandler?>(StateApp.instance.scopeGetter, { + video?.let { v -> + if (v is VideoLocal) { + StatePlatform.instance.getContentRecommendations(v.url) + } else { + video?.getContentRecommendations(StatePlatform.instance.getContentClient(v.url)) + } + } + }) .success { setRecommendations(it?.getResults()?.filter { it is IPlatformVideo }?.map { it as IPlatformVideo }, "No recommendations found") } .exception { setRecommendations(null, it.message) diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt index 41de7c3c..d5f53af4 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt @@ -61,8 +61,17 @@ class StatePlaylists { } fun updateWatchLater(updated: List) { synchronized(_watchlistStore) { - _watchlistStore.deleteAll(); - _watchlistStore.saveAllAsync(updated); + //_watchlistStore.deleteAll(); + val existing = _watchlistStore.getItems(); + val toAdd = updated.filter { u -> !existing.any { u.url == it.url } }; + val toRemove = existing.filter { u -> !updated.any { u.url == it.url } }; + Logger.i(TAG, "WatchLater changed:\nTo Add:\n" + + (if(toAdd.size == 0) "None" else toAdd.map { " + " + it.name }.joinToString("\n")) + + "\nTo Remove:\n" + + (if(toRemove.size == 0) "None" else toRemove.map { " - " + it.name }.joinToString("\n"))); + for(remove in toRemove) + _watchlistStore.delete(remove); + _watchlistStore.saveAllAsync(toAdd); _watchlistOrderStore.set(*updated.map { it.url }.toTypedArray()); _watchlistOrderStore.save(); } diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt index 420a3721..71aa6d6f 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePlugins.kt @@ -10,6 +10,8 @@ import com.futo.platformplayer.api.media.platforms.js.SourceAuth import com.futo.platformplayer.api.media.platforms.js.SourceCaptchaData import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor +import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment +import com.futo.platformplayer.fragment.mainactivity.main.SourceDetailFragment.Companion import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.models.ImageVariable import com.futo.platformplayer.stores.FragmentedStorage @@ -128,7 +130,15 @@ class StatePlugins { return false; LoginActivity.showLogin(context, config) { - StatePlugins.instance.setPluginAuth(config.id, it); + try { + StatePlugins.instance.setPluginAuth(config.id, it); + } catch (e: Throwable) { + StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { + UIDialogs.showGeneralErrorDialog(context, "Failed to set plugin authentication (loginPlugin)", e) + } + Logger.e(SourceDetailFragment.TAG, "Failed to set plugin authentication (loginPlugin)", e) + return@showLogin + } StateApp.instance.scope.launch(Dispatchers.IO) { StatePlatform.instance.reloadClient(context, id); diff --git a/app/src/main/res/layout/video_player_ui.xml b/app/src/main/res/layout/video_player_ui.xml index 32a9f5cf..e849b942 100644 --- a/app/src/main/res/layout/video_player_ui.xml +++ b/app/src/main/res/layout/video_player_ui.xml @@ -29,6 +29,14 @@ android:layout_height="wrap_content" app:layout_constraintTop_toTopOf="parent" app:layout_constraintRight_toRightOf="parent"> + - - + - - Allow app to flip into reverse portrait Rotation zone Specify the sensitivity of rotation zones (decrease to make less sensitive) + Stability threshold time + Specify the duration the orientation needs to be the same to trigger a rotation Prefer Webm Video Codecs If player should prefer Webm codecs (vp9/opus) over mp4 codecs (h264/AAC), may result in worse compatibility. + Full auto rotate lock + Prevent any rotation while rotation lock is engaged (even flipping between landscape and landscape reverse). Prefer Webm Audio Codecs If player should prefer Webm codecs (opus) over mp4 codecs (AAC), may result in worse compatibility. Allow video under cutout @@ -966,4 +970,12 @@ 30 45 + + 100 + 500 + 750 + 1000 + 1500 + 2000 + \ No newline at end of file diff --git a/app/src/stable/assets/sources/youtube b/app/src/stable/assets/sources/youtube index 4cd358c2..35b56d38 160000 --- a/app/src/stable/assets/sources/youtube +++ b/app/src/stable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit 4cd358c2bd11e016bdfe1ed341fd5c7e49219758 +Subproject commit 35b56d380a9ae6ef85ba8ec16cecb0a86d4efa1d diff --git a/app/src/unstable/assets/sources/youtube b/app/src/unstable/assets/sources/youtube index 4cd358c2..35b56d38 160000 --- a/app/src/unstable/assets/sources/youtube +++ b/app/src/unstable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit 4cd358c2bd11e016bdfe1ed341fd5c7e49219758 +Subproject commit 35b56d380a9ae6ef85ba8ec16cecb0a86d4efa1d