Compare commits

...

20 Commits

Author SHA1 Message Date
Kelvin 3a41b89e52 Requests 2024-11-13 18:39:01 +01:00
Kelvin 0a0c16524a Allow hiding privacy mode and FAQ without breaking existing orderings 2024-09-10 23:10:36 +02:00
Kelvin 9b843a155e Revert "Allow more tabs to be hidden."
This reverts commit 8c4e511883
2024-09-10 20:51:30 +00:00
Kelvin cb085acbff Submods 2024-09-10 21:06:30 +02:00
Kelvin c3d7df166b Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-09-10 20:40:17 +02:00
Kelvin d312062125 Fix content recommendations on offline videos 2024-09-10 20:40:14 +02:00
Koen J e2453192aa More gracefully handle failing to set plugin auth. 2024-09-10 17:27:00 +02:00
Koen J 0f4e4a7d97 Allow configuring stability threshold time and ensure there is no more than 1 job active at a time for SimpleOrientationListener. 2024-09-10 15:54:27 +02:00
Koen J f20a708b36 Check both length and null for 'No recommendations found' 2024-09-10 12:25:30 +02:00
Koen J 8c4e511883 Allow more tabs to be hidden. 2024-09-10 12:24:42 +02:00
Koen J a4a3b8d664 Implement full autorotate lock (default off). 2024-09-10 11:59:44 +02:00
Koen J bf6530ea81 Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay 2024-09-10 10:34:46 +02:00
Koen J 4a80c2aab1 Moved Autoplay button to top and load recommendations now appropriately uses 'StatePlatform.instance.getContentRecommendations(v.url)' for local videos. 2024-09-10 10:34:36 +02:00
Kelvin 527bbfe43f Fix watchlater re-downloading every time videos are reordered 2024-09-09 23:07:15 +02:00
Koen J d8e1edb60b Added autoplay icon. 2024-09-09 15:52:55 +02:00
Koen J 245b5f74c0 Increased scrubber size a bit and made add comment view invisible for platform comments. 2024-09-09 15:50:36 +02:00
Koen J e9a1f63415 Added autoplay setting. 2024-09-09 15:20:31 +02:00
Koen J ec370dd94b Added autoplay feature. 2024-09-09 14:58:08 +02:00
Koen J e39d862ef3 Added rotation zone setting allowing you to specify the rotation to be less sensitive (default 45 degrees). Added reverse portrait setting allowing you to allow reverse portrait (default off). Added setting to hide recommendations. 2024-09-09 12:41:16 +02:00
Koen J 7b065654aa Updated submodules. 2024-09-09 10:52:52 +02:00
21 changed files with 432 additions and 165 deletions
@@ -471,14 +471,30 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.full_screen_portrait, FieldForm.TOGGLE, R.string.allow_full_screen_portrait, 13)
var fullscreenPortrait: Boolean = false;
@FormField(R.string.reverse_portrait, FieldForm.TOGGLE, R.string.reverse_portrait_description, 14)
var reversePortrait: Boolean = false;
@FormField(R.string.prefer_webm, FieldForm.TOGGLE, R.string.prefer_webm_description, 14)
@FormField(R.string.rotation_zone, FieldForm.DROPDOWN, R.string.rotation_zone_description, 15)
@DropdownFieldOptionsId(R.array.rotation_zone)
var rotationZone: Int = 2;
@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, 15)
@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, 16)
@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, 21)
var autoplay: Boolean = false;
}
@FormField(R.string.comments, "group", R.string.comments_description, 6)
@@ -494,6 +510,9 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.default_recommendations, FieldForm.TOGGLE, R.string.default_recommendations_description, 0)
var recommendationsDefault: Boolean = false;
@FormField(R.string.hide_recommendations, FieldForm.TOGGLE, R.string.hide_recommendations_description, 0)
var hideRecommendations: Boolean = false;
@FormField(R.string.bad_reputation_comments_fading, FieldForm.TOGGLE, R.string.bad_reputation_comments_fading_description, 0)
var badReputationCommentsFading: Boolean = true;
@@ -8,6 +8,7 @@ import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.logging.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -17,24 +18,43 @@ class SimpleOrientationListener(
) {
private var lastOrientation: Int = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
private var lastStableOrientation: Int = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
private val stabilityThresholdTime = 500L
private var _currentJob: Job? = null
val onOrientationChanged = Event1<Int>()
private val orientationListener = object : OrientationEventListener(activity, SensorManager.SENSOR_DELAY_UI) {
override fun onOrientationChanged(orientation: Int) {
//val rotationZone = 45
val stabilityThresholdTime = when (Settings.instance.playback.stabilityThresholdTime) {
0 -> 100L
1 -> 500L
2 -> 750L
3 -> 1000L
4 -> 1500L
5 -> 2000L
else -> 500L
}
val rotationZone = when (Settings.instance.playback.rotationZone) {
0 -> 15
1 -> 30
2 -> 45
else -> 45
}
val newOrientation = when {
orientation in 45..134 -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
orientation in 135..224 -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
orientation in 225..314 -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
orientation in 315..360 || orientation in 0..44 -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
orientation in (90 - rotationZone)..(90 + rotationZone - 1) -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
orientation in (180 - rotationZone)..(180 + rotationZone - 1) -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
orientation in (270 - rotationZone)..(270 + rotationZone - 1) -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
orientation in (360 - rotationZone)..(360 + rotationZone - 1) || orientation in 0..(rotationZone - 1) -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
else -> lastOrientation
}
if (newOrientation != lastStableOrientation) {
lastStableOrientation = newOrientation
lifecycleScope.launch(Dispatchers.Main) {
_currentJob?.cancel()
_currentJob = lifecycleScope.launch(Dispatchers.Main) {
try {
delay(stabilityThresholdTime)
if (newOrientation == lastStableOrientation) {
@@ -55,6 +75,8 @@ class SimpleOrientationListener(
}
fun stopListening() {
_currentJob?.cancel()
_currentJob = null
orientationListener.disable()
}
@@ -25,6 +25,7 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
import com.futo.platformplayer.downloads.VideoLocal
@@ -879,6 +880,12 @@ class UISlideOverlays {
val items = arrayListOf<View>();
val lastUpdated = StatePlaylists.instance.getLastUpdatedPlaylist();
val isLimited = video?.url != null && StatePlatform.instance.getContentClientOrNull(video!!.url)?.let {
if (it is JSClient)
return@let it.config.reduceFunctionsInLimitedVersion && BuildConfig.IS_PLAYSTORE_BUILD
else false;
} ?: false;
if (lastUpdated != null) {
items.add(
SlideUpMenuGroup(container.context, container.context.getString(R.string.recently_used_playlist), "recentlyusedplaylist",
@@ -899,17 +906,18 @@ class UISlideOverlays {
val watchLater = StatePlaylists.instance.getWatchLater();
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
(listOf(
SlideUpMenuItem(
container.context,
R.drawable.ic_download,
container.context.getString(R.string.download),
container.context.getString(R.string.download_the_video),
tag = "download",
call = {
showDownloadVideoOverlay(video, container, true);
},
invokeParent = false
),
if(!isLimited)
SlideUpMenuItem(
container.context,
R.drawable.ic_download,
container.context.getString(R.string.download),
container.context.getString(R.string.download_the_video),
tag = "download",
call = {
showDownloadVideoOverlay(video, container, true);
},
invokeParent = false
) else null,
SlideUpMenuItem(
container.context,
R.drawable.ic_share,
@@ -936,7 +944,7 @@ class UISlideOverlays {
StateMeta.instance.addHiddenCreator(video.author.url);
UIDialogs.toast(container.context, "[${video.author.name}] hidden, you may need to reload home");
}))
+ actions)
+ actions).filterNotNull()
));
items.add(
SlideUpMenuGroup(container.context, container.context.getString(R.string.add_to), "addto",
@@ -1033,15 +1041,7 @@ class UISlideOverlays {
"${watchLater.size} " + container.context.getString(R.string.videos),
tag = "watch later",
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video)); }),
SlideUpMenuItem(
container.context,
R.drawable.ic_download,
container.context.getString(R.string.download),
container.context.getString(R.string.download_the_video),
tag = container.context.getString(R.string.download),
call = { showDownloadVideoOverlay(video, container, true); },
invokeParent = false
))
)
);
val playlistItems = arrayListOf<SlideUpMenuItem>();
@@ -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 {
@@ -50,7 +50,8 @@ class SourcePluginConfig(
var primaryClaimFieldType: Int? = null,
var developerSubmitUrl: String? = null,
var allowAllHttpHeaderAccess: Boolean = false,
var maxDownloadParallelism: Int = 0
var maxDownloadParallelism: Int = 0,
var reduceFunctionsInLimitedVersion: Boolean = false,
) : IV8PluginConfig {
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
@@ -209,26 +209,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) {
@@ -310,19 +314,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<BuyFragment>() }))
}
newCurrentButtonDefinitions.add(ButtonDefinition(97, R.drawable.ic_quiz, R.drawable.ic_quiz_fill, R.string.faq, canToggle = false, { false }, {
it.navigate<BrowserFragment>(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
@@ -395,6 +386,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<BrowserFragment>(Settings.URL_FAQ);
})
//96 is reserved for privacy button
//98 is reserved for buy button
@@ -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) {
@@ -97,36 +97,56 @@ class VideoDetailFragment : MainFragment {
val isMaximized = state == State.MAXIMIZED
val isFullScreenPortraitAllowed = Settings.instance.playback.fullscreenPortrait;
val bypassRotationPrevention = Settings.instance.other.bypassRotationPrevention;
val fullAutorotateLock = Settings.instance.playback.fullAutorotateLock
val currentRequestedOrientation = a.requestedOrientation
val currentOrientation = if (_currentOrientation == -1) currentRequestedOrientation else _currentOrientation
var currentOrientation = if (_currentOrientation == -1) currentRequestedOrientation else _currentOrientation
if (currentOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT && !Settings.instance.playback.reversePortrait)
currentOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
val isAutoRotate = Settings.instance.playback.isAutoRotate()
val isFs = isFullscreen
if (isFs && isMaximized) {
if (isFullScreenPortraitAllowed) {
if (isAutoRotate) {
a.requestedOrientation = currentOrientation
}
} else if (currentOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || currentOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) {
if (isAutoRotate) {
a.requestedOrientation = currentOrientation
if (fullAutorotateLock) {
if (isFs && isMaximized) {
if (isFullScreenPortraitAllowed) {
if (isAutoRotate) {
a.requestedOrientation = currentOrientation
}
} else if (currentOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || currentOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) {
if (isAutoRotate || currentOrientation != currentRequestedOrientation && (currentRequestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || currentRequestedOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT)) {
a.requestedOrientation = currentOrientation
}
} else {
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
}
} else if (bypassRotationPrevention) {
a.requestedOrientation = currentOrientation
} else if (currentOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || currentOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT) {
a.requestedOrientation = currentOrientation
} else {
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
}
} else if (bypassRotationPrevention) {
if (isAutoRotate) {
a.requestedOrientation = currentOrientation
}
} else if (currentOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || currentOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT) {
if (isAutoRotate) {
a.requestedOrientation = currentOrientation
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
} else {
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
if (isFs && isMaximized) {
if (isFullScreenPortraitAllowed) {
a.requestedOrientation = currentOrientation
} else if (currentOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || currentOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) {
a.requestedOrientation = currentOrientation
} else if (currentRequestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || currentRequestedOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) {
//Don't change anything
} else {
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
}
} else if (bypassRotationPrevention) {
a.requestedOrientation = currentOrientation
} else if (currentOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || currentOrientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT) {
a.requestedOrientation = currentOrientation
} else {
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
}
Log.i(TAG, "updateOrientation (isFs = ${isFs}, currentOrientation = ${currentOrientation}, currentRequestedOrientation = ${currentRequestedOrientation}, isMaximized = ${isMaximized}, isAutoRotate = ${isAutoRotate}, isFullScreenPortraitAllowed = ${isFullScreenPortraitAllowed}) resulted in requested orientation ${activity?.requestedOrientation}");
Log.i(TAG, "updateOrientation (isFs = ${isFs}, currentOrientation = ${currentOrientation}, fullAutorotateLock = ${fullAutorotateLock}, currentRequestedOrientation = ${currentRequestedOrientation}, isMaximized = ${isMaximized}, isAutoRotate = ${isAutoRotate}, isFullScreenPortraitAllowed = ${isFullScreenPortraitAllowed}) resulted in requested orientation ${activity?.requestedOrientation}");
}
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
@@ -40,6 +40,7 @@ import androidx.media3.ui.TimeBar
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import com.futo.platformplayer.BuildConfig
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
@@ -72,6 +73,7 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.platforms.js.models.JSVideoDetails
import com.futo.platformplayer.api.media.structures.IPager
@@ -290,6 +292,7 @@ class VideoDetailView : ConstraintLayout {
private var _commentsCount = 0;
private var _polycentricProfile: PolycentricCache.CachedPolycentricProfile? = null;
private var _slideUpOverlay: SlideUpMenuOverlay? = null;
private var _autoplayVideo: IPlatformVideo? = null
//Events
val onMinimize = Event0();
@@ -720,6 +723,17 @@ class VideoDetailView : ConstraintLayout {
fragment.activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
};
StatePlayer.instance.autoplayChanged.subscribe(this) {
if (it) {
val url = _url
val autoPlayVideo = _autoplayVideo
if (url != null && autoPlayVideo == null) {
_taskLoadRecommendations.cancel()
_taskLoadRecommendations.run(url)
}
}
}
_layoutResume.setOnClickListener {
handleSeek(_historicalPosition * 1000);
@@ -807,6 +821,11 @@ class VideoDetailView : ConstraintLayout {
}
fun updateMoreButtons() {
val isLimitedVersion = video?.url != null && StatePlatform.instance.getContentClientOrNull(video!!.url)?.let {
if (it is JSClient)
return@let it.config.reduceFunctionsInLimitedVersion && BuildConfig.IS_PLAYSTORE_BUILD
else false;
} ?: false;
val buttons = listOf(RoundButton(context, R.drawable.ic_add, context.getString(R.string.add), TAG_ADD) {
(video ?: _searchVideo)?.let {
_slideUpOverlay = UISlideOverlays.showAddToOverlay(it, _overlayContainer) {
@@ -826,38 +845,44 @@ class VideoDetailView : ConstraintLayout {
}
_slideUpOverlay?.hide();
} else null,
RoundButton(context, R.drawable.ic_screen_share, context.getString(R.string.background), TAG_BACKGROUND) {
if(!allowBackground) {
_player.switchToAudioMode();
allowBackground = true;
it.text.text = resources.getString(R.string.background_revert);
if(!isLimitedVersion)
RoundButton(context, R.drawable.ic_screen_share, context.getString(R.string.background), TAG_BACKGROUND) {
if(!allowBackground) {
_player.switchToAudioMode();
allowBackground = true;
it.text.text = resources.getString(R.string.background_revert);
}
else {
_player.switchToVideoMode();
allowBackground = false;
it.text.text = resources.getString(R.string.background);
}
_slideUpOverlay?.hide();
}
else {
_player.switchToVideoMode();
allowBackground = false;
it.text.text = resources.getString(R.string.background);
else null,
if(!isLimitedVersion)
RoundButton(context, R.drawable.ic_download, context.getString(R.string.download), TAG_DOWNLOAD) {
video?.let {
_slideUpOverlay = UISlideOverlays.showDownloadVideoOverlay(it, _overlayContainer, context.contentResolver);
};
}
_slideUpOverlay?.hide();
},
RoundButton(context, R.drawable.ic_download, context.getString(R.string.download), TAG_DOWNLOAD) {
video?.let {
_slideUpOverlay = UISlideOverlays.showDownloadVideoOverlay(it, _overlayContainer, context.contentResolver);
};
},
RoundButton(context, R.drawable.ic_share, context.getString(R.string.share), TAG_SHARE) {
video?.let {
Logger.i(TAG, "Share preventPictureInPicture = true");
preventPictureInPicture = true;
shareVideo();
};
_slideUpOverlay?.hide();
},
RoundButton(context, R.drawable.ic_screen_share, context.getString(R.string.overlay), TAG_OVERLAY) {
this.startPictureInPicture();
fragment.forcePictureInPicture();
//PiPActivity.startPiP(context);
_slideUpOverlay?.hide();
},
else null,
RoundButton(context, R.drawable.ic_share, context.getString(R.string.share), TAG_SHARE) {
video?.let {
Logger.i(TAG, "Share preventPictureInPicture = true");
preventPictureInPicture = true;
shareVideo();
};
_slideUpOverlay?.hide();
},
if(!isLimitedVersion)
RoundButton(context, R.drawable.ic_screen_share, context.getString(R.string.overlay), TAG_OVERLAY) {
this.startPictureInPicture();
fragment.forcePictureInPicture();
//PiPActivity.startPiP(context);
_slideUpOverlay?.hide();
}
else null,
RoundButton(context, R.drawable.ic_export, context.getString(R.string.page), TAG_OPEN) {
video?.let {
val url = video?.shareUrl ?: _searchVideo?.shareUrl ?: _url;
@@ -1006,6 +1031,7 @@ class VideoDetailView : ConstraintLayout {
_container_content_queue.cleanup();
_container_content_description.cleanup();
_container_content_support.cleanup();
StatePlayer.instance.autoplayChanged.remove(this)
StateCasting.instance.onActiveDevicePlayChanged.remove(this);
StateCasting.instance.onActiveDeviceTimeChanged.remove(this);
StateCasting.instance.onActiveDeviceConnectionStateChanged.remove(this);
@@ -1102,6 +1128,8 @@ class VideoDetailView : ConstraintLayout {
this.video = null;
cleanupPlaybackTracker();
_searchVideo = video;
_autoplayVideo = null
Logger.i(TAG, "Autoplay video cleared (setVideoOverview)")
_videoResumePositionMilliseconds = resumeSeconds * 1000;
setLastPositionMilliseconds(_videoResumePositionMilliseconds, false);
_addCommentView.setContext(null, null);
@@ -1191,6 +1219,8 @@ class VideoDetailView : ConstraintLayout {
Logger.i(TAG, "setVideoDetails (${videoDetail.name})")
_didTriggerDatasourceErrroCount = 0;
_didTriggerDatasourceError = false;
_autoplayVideo = null
Logger.i(TAG, "Autoplay video cleared (setVideoDetails)")
if(newVideo && this.video?.url == videoDetail.url)
return;
@@ -1297,7 +1327,7 @@ class VideoDetailView : ConstraintLayout {
if (video is TutorialFragment.TutorialVideo) {
setTabIndex(0, true)
} else {
if (Settings.instance.comments.recommendationsDefault) {
if (Settings.instance.comments.recommendationsDefault && !Settings.instance.comments.hideRecommendations) {
setTabIndex(2, true)
} else {
when(Settings.instance.comments.defaultCommentSection) {
@@ -1511,6 +1541,11 @@ class VideoDetailView : ConstraintLayout {
_layoutRating.visibility = View.VISIBLE
_layoutChangeBottomSection.visibility = View.VISIBLE
}
if (StatePlayer.instance.autoplay) {
_taskLoadRecommendations.cancel()
_taskLoadRecommendations.run(videoDetail.url)
}
}
fun loadLiveChat(video: IPlatformVideoDetails) {
_liveChat?.stop();
@@ -1779,6 +1814,14 @@ class VideoDetailView : ConstraintLayout {
fun nextVideo(forceLoop: Boolean = false, withoutRemoval: Boolean = false, bypassVideoLoop: Boolean = false): Boolean {
Logger.i(TAG, "nextVideo")
var next = StatePlayer.instance.nextQueueItem(withoutRemoval || _player.duration < 100 || (_player.position.toFloat() / _player.duration) < 0.9, bypassVideoLoop);
val autoplayVideo = _autoplayVideo
if (next == null && autoplayVideo != null && StatePlayer.instance.autoplay) {
Logger.i(TAG, "Found autoplay video!")
StatePlayer.instance.setAutoplayed(autoplayVideo.url)
next = autoplayVideo
}
_autoplayVideo = null
Logger.i(TAG, "Autoplay video cleared (nextVideo)")
if(next == null && forceLoop)
next = StatePlayer.instance.restartQueue();
if(next != null) {
@@ -2303,6 +2346,9 @@ class VideoDetailView : ConstraintLayout {
return
}
val recommendationsHidden = Settings.instance.comments.hideRecommendations
_buttonRecommended.visibility = if (recommendationsHidden) View.GONE else View.VISIBLE
_taskLoadRecommendations.cancel()
_tabIndex = index
_buttonRecommended.setTextColor(resources.getColor(if (index == 2) R.color.white else R.color.gray_ac))
@@ -2319,7 +2365,7 @@ class VideoDetailView : ConstraintLayout {
_layoutRecommended.visibility = View.GONE
fetchPolycentricComments()
} else if (index == 1) {
_addCommentView.visibility = View.VISIBLE
_addCommentView.visibility = View.GONE
_layoutRecommended.visibility = View.GONE
fetchComments()
} else if (index == 2) {
@@ -2327,60 +2373,57 @@ 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)
}
}
private fun setRecommendations(pager: IPager<IPlatformContent>?, message: String? = null) {
_layoutRecommended.removeAllViews()
if (pager == null) {
_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))
}
textAlignment = TEXT_ALIGNMENT_CENTER
textSize = 14.0f
text = message
})
return
private fun setRecommendations(results: List<IPlatformVideo>?, message: String? = null) {
if (results != null && StatePlayer.instance.autoplay) {
_autoplayVideo = results.firstOrNull { !StatePlayer.instance.wasAutoplayed(it.url) }
Logger.i(TAG, "Autoplay video set (url = ${_autoplayVideo?.url})")
}
val results = pager.getResults().filter { it is IPlatformVideo }
for (result in results) {
_layoutRecommended.addView(PreviewVideoView(context, FeedStyle.THUMBNAIL, null, false).apply {
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
bind(result)
hideAddTo()
onVideoClicked.subscribe { video, _ ->
fragment.navigate<VideoDetailFragment>(video).maximizeVideoDetail()
}
onChannelClicked.subscribe {
fragment.navigate<ChannelFragment>(it)
}
onAddToWatchLaterClicked.subscribe(this) {
if(it is IPlatformVideo) {
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it));
UIDialogs.toast("Added to watch later\n[${it.name}]");
if (_tabIndex == 2) {
_layoutRecommended.removeAllViews()
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))
}
}
})
textAlignment = TEXT_ALIGNMENT_CENTER
textSize = 14.0f
text = message
})
return
}
for (result in results) {
_layoutRecommended.addView(PreviewVideoView(context, FeedStyle.THUMBNAIL, null, false).apply {
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
bind(result)
hideAddTo()
onVideoClicked.subscribe { video, _ ->
fragment.navigate<VideoDetailFragment>(video).maximizeVideoDetail()
}
onChannelClicked.subscribe {
fragment.navigate<ChannelFragment>(it)
}
onAddToWatchLaterClicked.subscribe(this) {
if(it is IPlatformVideo) {
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it));
UIDialogs.toast("Added to watch later\n[${it.name}]");
}
}
})
}
}
}
@@ -2728,8 +2771,16 @@ class VideoDetailView : ConstraintLayout {
}
} else TaskHandler(IPlatformVideoDetails::class.java, {fragment.lifecycleScope});
private val _taskLoadRecommendations = TaskHandler<String, IPager<IPlatformContent>?>(StateApp.instance.scopeGetter, { video?.getContentRecommendations(StatePlatform.instance.getContentClient(it)) })
.success { setRecommendations(it, "No recommendations found") }
private val _taskLoadRecommendations = TaskHandler<String?, IPager<IPlatformContent>?>(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<Throwable> {
setRecommendations(null, it.message)
Logger.w(TAG, "Failed to load recommendations.", it);
@@ -8,6 +8,7 @@ import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.upstream.DefaultAllocator
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
@@ -45,6 +46,33 @@ class StatePlayer {
onRotationLockChanged.emit(value)
}
val onRotationLockChanged = Event1<Boolean>()
var autoplay: Boolean = Settings.instance.playback.autoplay
get() = field
set(value) {
if (field != value)
_autoplayed.clear()
field = value
autoplayChanged.emit(value)
}
private val _autoplayed = hashSetOf<String>()
fun wasAutoplayed(url: String?): Boolean {
if (url == null) {
return false
}
synchronized(_autoplayed) {
return _autoplayed.contains(url)
}
}
fun setAutoplayed(url: String?) {
if (url == null) {
return
}
synchronized(_autoplayed) {
_autoplayed.add(url)
}
}
val autoplayChanged = Event1<Boolean>()
var loopVideo : Boolean = false;
val isPlaying: Boolean get() = _exoplayer?.player?.playWhenReady ?: false;
@@ -138,6 +166,12 @@ class StatePlayer {
}
}
fun isUrlInQueue(url : String) : Boolean {
synchronized(_queue) {
return _queue.any { it.url == url };
}
}
fun getQueueType() : String {
return _queueType;
}
@@ -61,8 +61,17 @@ class StatePlaylists {
}
fun updateWatchLater(updated: List<SerializedPlatformVideo>) {
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();
}
@@ -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);
@@ -18,6 +18,7 @@ import android.widget.ImageButton
import android.widget.TextView
import androidx.annotation.OptIn
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.core.view.setMargins
import androidx.media3.common.C
import androidx.media3.common.PlaybackParameters
@@ -74,6 +75,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
//Custom buttons
private val _control_fullscreen: ImageButton;
private val _control_autoplay: ImageButton;
private val _control_videosettings: ImageButton;
private val _control_minimize: ImageButton;
private val _control_rotate_lock: ImageButton;
@@ -92,6 +94,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
private val _control_videosettings_fullscreen: ImageButton;
private val _control_minimize_fullscreen: ImageButton;
private val _control_rotate_lock_fullscreen: ImageButton;
private val _control_autoplay_fullscreen: ImageButton;
private val _control_loop_fullscreen: ImageButton;
private val _control_cast_fullscreen: ImageButton;
private val _control_play_fullscreen: ImageButton;
@@ -149,6 +152,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
videoControls = findViewById(R.id.video_player_controller);
_control_fullscreen = videoControls.findViewById(R.id.button_fullscreen);
_control_autoplay = videoControls.findViewById(R.id.button_autoplay);
_control_videosettings = videoControls.findViewById(R.id.button_settings);
_control_minimize = videoControls.findViewById(R.id.button_minimize);
_control_rotate_lock = videoControls.findViewById(R.id.button_rotate_lock);
@@ -164,6 +168,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
_control_duration = videoControls.findViewById(R.id.text_duration);
_videoControls_fullscreen = findViewById(R.id.video_player_controller_fullscreen);
_control_autoplay_fullscreen = _videoControls_fullscreen.findViewById(R.id.button_autoplay);
_control_fullscreen_fullscreen = _videoControls_fullscreen.findViewById(R.id.button_fullscreen);
_control_minimize_fullscreen = _videoControls_fullscreen.findViewById(R.id.button_minimize);
_control_videosettings_fullscreen = _videoControls_fullscreen.findViewById(R.id.button_settings);
@@ -386,6 +391,18 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
UIDialogs.showCastingDialog(context);
};
_control_autoplay.setOnClickListener {
StatePlayer.instance.autoplay = !StatePlayer.instance.autoplay;
updateAutoplayButton()
}
updateAutoplayButton()
_control_autoplay_fullscreen.setOnClickListener {
StatePlayer.instance.autoplay = !StatePlayer.instance.autoplay;
updateAutoplayButton()
}
updateAutoplayButton()
val progressUpdateListener = { position: Long, bufferedPosition: Long ->
val currentTime = position.formatDuration()
val currentDuration = duration.formatDuration()
@@ -433,6 +450,11 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
}
}
private fun updateAutoplayButton() {
_control_autoplay.setColorFilter(ContextCompat.getColor(context, if (StatePlayer.instance.autoplay) com.futo.futopay.R.color.primary else R.color.white))
_control_autoplay_fullscreen.setColorFilter(ContextCompat.getColor(context, if (StatePlayer.instance.autoplay) com.futo.futopay.R.color.primary else R.color.white))
}
private fun setSystemBrightness(brightness: Float) {
Log.i(TAG, "setSystemBrightness $brightness")
if (android.provider.Settings.System.canWrite(context)) {
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M400,623.08L400,336.92L623.08,480L400,623.08ZM480,880Q363.54,880 269.42,820.12Q175.31,760.23 120,651.23L120,800L80,800L80,584.62L294.62,584.62L294.62,624.62L152,624.62Q198.38,724.23 285.73,782.12Q373.08,840 480,840Q598.85,840 693.5,769Q788.15,698 823.08,584.38L862.62,592.38Q825.31,721.46 719.54,800.73Q613.77,880 480,880ZM82,440Q89.77,377.62 110.15,328.04Q130.54,278.46 169.92,227.23L199.23,255Q167.23,296.77 149.54,339.04Q131.85,381.31 122.23,440L82,440ZM255.23,199.77L227.46,170.46Q275.85,132.62 329.54,110.58Q383.23,88.54 440,83.54L440,123.54Q391.31,128.54 344.15,148.15Q297,167.77 255.23,199.77ZM703.46,199.77Q667.08,169.31 616.73,148.54Q566.38,127.77 520,123.54L520,83.54Q577,88.77 630.81,111.08Q684.62,133.38 732,171.23L703.46,199.77ZM836.46,440Q829.92,384.38 810.31,338.65Q790.69,292.92 758.69,255.77L787.23,227.23Q825.85,273.08 848.15,326.88Q870.46,380.69 876.46,440L836.46,440Z"/>
</vector>
@@ -112,7 +112,8 @@
app:scrubber_drawable="@drawable/player_thumb"
app:bar_height="2dp"
app:scrubber_disabled_size="0dp"
app:scrubber_enabled_size="12dp"
app:scrubber_enabled_size="16dp"
app:scrubber_dragged_size="20dp"
app:played_color="@color/colorPrimary"
app:buffered_color="#DDEEEEEE"
app:unplayed_color="#55EEEEEE" />
+10 -1
View File
@@ -29,6 +29,14 @@
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent">
<ImageButton
android:id="@+id/button_autoplay"
android:layout_width="50dp"
android:layout_height="50dp"
android:scaleType="fitCenter"
android:clickable="true"
android:padding="12dp"
app:srcCompat="@drawable/autoplay_24px" />
<ImageButton
android:id="@+id/button_cast"
android:layout_width="50dp"
@@ -205,7 +213,8 @@
app:bar_height="2dp"
app:scrubber_drawable="@drawable/player_thumb"
app:scrubber_disabled_size="0dp"
app:scrubber_enabled_size="12dp"
app:scrubber_enabled_size="16dp"
app:scrubber_dragged_size="20dp"
app:played_color="@color/transparent"
app:buffered_color="@color/transparent"
app:unplayed_color="@color/transparent"
@@ -57,6 +57,14 @@
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent">
<ImageButton
android:id="@+id/button_autoplay"
android:layout_width="50dp"
android:layout_height="50dp"
android:scaleType="fitCenter"
android:clickable="true"
android:padding="12dp"
app:srcCompat="@drawable/autoplay_24px" />
<ImageButton
android:id="@+id/button_cast"
android:layout_width="50dp"
@@ -237,7 +245,8 @@
app:scrubber_drawable="@drawable/player_thumb"
app:bar_height="2dp"
app:scrubber_disabled_size="0dp"
app:scrubber_enabled_size="12dp"
app:scrubber_enabled_size="16dp"
app:scrubber_dragged_size="20dp"
app:played_color="@color/colorPrimary"
app:buffered_color="#AAEEEEEE"
app:unplayed_color="#88EEEEEE"
+2 -1
View File
@@ -178,7 +178,8 @@
app:bar_height="2dp"
app:scrubber_drawable="@drawable/player_thumb"
app:scrubber_disabled_size="0dp"
app:scrubber_enabled_size="12dp"
app:scrubber_enabled_size="16dp"
app:scrubber_dragged_size="20dp"
app:played_color="@color/colorPrimary"
app:buffered_color="@color/transparent"
app:unplayed_color="#7F7F7F"
+25
View File
@@ -373,12 +373,22 @@
<string name="system_volume_descr">Gesture controls adjust system volume</string>
<string name="live_chat_webview">Live Chat Webview</string>
<string name="full_screen_portrait">Fullscreen portrait</string>
<string name="reverse_portrait">Allow reverse portrait</string>
<string name="reverse_portrait_description">Allow app to flip into reverse portrait</string>
<string name="rotation_zone">Rotation zone</string>
<string name="rotation_zone_description">Specify the sensitivity of rotation zones (decrease to make less sensitive)</string>
<string name="stability_threshold_time">Stability threshold time</string>
<string name="stability_threshold_time_description">Specify the duration the orientation needs to be the same to trigger a rotation</string>
<string name="prefer_webm">Prefer Webm Video Codecs</string>
<string name="prefer_webm_description">If player should prefer Webm codecs (vp9/opus) over mp4 codecs (h264/AAC), may result in worse compatibility.</string>
<string name="full_autorotate_lock">Full auto rotate lock</string>
<string name="full_autorotate_lock_description">Prevent any rotation while rotation lock is engaged (even flipping between landscape and landscape reverse).</string>
<string name="prefer_webm_audio">Prefer Webm Audio Codecs</string>
<string name="prefer_webm_audio_description">If player should prefer Webm codecs (opus) over mp4 codecs (AAC), may result in worse compatibility.</string>
<string name="allow_under_cutout">Allow video under cutout</string>
<string name="allow_under_cutout_description">Allow video to go underneath the screen cutout in full-screen.\nMay require restart</string>
<string name="autoplay">Enable autoplay by default</string>
<string name="autoplay_description">Autoplay will be enabled by default whenever you watch a video</string>
<string name="allow_full_screen_portrait">Allow fullscreen portrait</string>
<string name="background_switch_audio">Switch to Audio in Background</string>
<string name="background_switch_audio_description">Optimize bandwidth usage by switching to audio-only stream in background if available, may cause stutter</string>
@@ -412,6 +422,8 @@
<string name="preferred_preview_quality_description">Default quality while previewing a video in a feed</string>
<string name="primary_language">Primary Language</string>
<string name="default_comment_section">Default Comment Section</string>
<string name="hide_recommendations">Hide Recommendations</string>
<string name="hide_recommendations_description">Fully hide the recommendations tab.</string>
<string name="default_recommendations">Recommendations as Default</string>
<string name="default_recommendations_description">Show recommendations as default, instead of comments.</string>
<string name="bad_reputation_comments_fading">Bad Reputation Comment Fading</string>
@@ -953,4 +965,17 @@
<item>Within 30 seconds of loss</item>
<item>Always</item>
</string-array>
<string-array name="rotation_zone">
<item>15</item>
<item>30</item>
<item>45</item>
</string-array>
<string-array name="rotation_threshold_time">
<item>100</item>
<item>500</item>
<item>750</item>
<item>1000</item>
<item>1500</item>
<item>2000</item>
</string-array>
</resources>