Compare commits

..

11 Commits

Author SHA1 Message Date
Kelvin b7c123c281 Refs 2025-08-19 16:45:57 +02:00
Kelvin 9481bbf3f1 Vod chat button fix, default settings in devportal 2025-08-19 16:42:17 +02:00
Kelvin 43ec7e821b Refs 2025-08-18 21:31:49 +02:00
Kelvin ca3454afbe Login warning fixes, uimod (disabled) 2025-08-18 19:35:12 +02:00
Kelvin 1edc8aabf8 Fix login dialog 2025-08-15 21:20:23 +02:00
Kelvin 91060faac9 VOD chat 2025-08-15 16:36:38 +02:00
Kelvin 17027ba364 Remote history sync on toggle 2025-08-14 21:03:39 +02:00
Kelvin 8569eaa5db Hide DevSubmit filter 2025-08-14 20:36:56 +02:00
Kelvin d32d817e0a Merge branch 'shorts-improv' into 'master'
Fix background play, disable artwork on background till improved, renamed...

See merge request videostreaming/grayjay!140
2025-08-14 11:26:47 +00:00
Kelvin a0f4cc760c Fix background play, disable artwork on background till improved, renamed variable that caused confusion 2025-08-14 12:35:46 +02:00
Kelvin 5247997ea5 Set plugin install request timeouts, fix messaging surrounding downloading icons 2025-08-13 19:36:26 +02:00
17 changed files with 189 additions and 44 deletions
+22 -2
View File
@@ -1022,15 +1022,35 @@
return x.value
});
let settingsToUse = __DEV_SETTINGS ?? {};
if (true) {
for (let setting of this.Plugin?.currentPlugin?.settings) {
if (typeof settingsToUse[setting.variable] == "undefined") {
switch (setting?.type?.toLowerCase()) {
case "boolean":
settingsToUse[setting.variable] = setting.default === 'true';
break;
case "dropdown":
let dropDownIndex = parseInt(setting.default);
if (dropDownIndex) {
settingsToUse[setting.variable] = setting.options[dropDownIndex];
}
break;
}
}
}
}
if(name == "enable") {
if(parameterVals.length > 0)
parameterVals[0] = this.Plugin.currentPlugin;
else
parameterVals.push(this.Plugin.currentPlugin);
if(parameterVals.length > 1)
parameterVals[1] = __DEV_SETTINGS;
parameterVals[1] = settingsToUse;
else
parameterVals.push(__DEV_SETTINGS);
parameterVals.push(settingsToUse);
}
const func = source[name];
@@ -76,6 +76,10 @@ class LoginActivity : AppCompatActivity() {
};
var isFirstLoad = true;
val loginWarnings = authConfig.loginWarnings?.toMutableList() ?: mutableListOf<SourcePluginAuthConfig.Warning>();
val uiMods = authConfig.uiMods?.toMutableList() ?: mutableListOf<SourcePluginAuthConfig.UIMod>();
var currentScale = 100;
var currentDesktop = false;
_webView.setInitialScale(50);
webViewClient.onPageLoaded.subscribe { view, url ->
_textUrl.setText(url ?: "");
@@ -89,9 +93,9 @@ class LoginActivity : AppCompatActivity() {
view?.evaluateJavascript("setTimeout(()=> document.querySelector(\"${authConfig.loginButton}\")?.click(), 1000)", {});
}
if(loginWarnings.size > 0) {
if(loginWarnings.size > 0 && url != null) {
synchronized(loginWarnings) {
val warning = loginWarnings.find { it.url.matches(it.getRegex()) };
val warning = loginWarnings.find { url.matches(it.getRegex()) };
if(warning != null) {
if(warning.once == true)
loginWarnings.remove(warning);
@@ -101,6 +105,35 @@ class LoginActivity : AppCompatActivity() {
}
}
}
/*
var specifiedScale = false;
var specifiedDesktop = false;
if(uiMods.size > 0 && url != null) {
synchronized(uiMods) {
val uimod = uiMods.find { url.matches(it.getRegex()) };
if(uimod != null) {
if(uimod.scale != null) {
currentScale =(uimod.scale * 100).toInt();
_webView.setInitialScale(currentScale);
specifiedScale = true;
}
if(uimod.desktop != null && uimod.desktop) {
_webView.settings.useWideViewPort = true;
specifiedDesktop = true;
}
}
}
}
if(!specifiedScale && currentScale != 100) {
currentScale = (100).toInt();
_webView.setInitialScale(currentScale);
}
if(!specifiedDesktop && currentDesktop) {
_webView.settings.useWideViewPort = false;
currentDesktop = false;
}
*/
}
_webView.settings.domStorageEnabled = true;
@@ -16,7 +16,8 @@ class SourcePluginAuthConfig(
val loginButton: String? = null,
val domainHeadersToFind: Map<String, List<String>>? = null,
val loginWarning: String? = null,
val loginWarnings: List<Warning>? = null
val loginWarnings: List<Warning>? = null,
val uiMods: List<UIMod>? = null
) {
@Serializable
@@ -29,6 +30,23 @@ class SourcePluginAuthConfig(
@Contextual
private var _regex: Regex? = null;
fun getRegex(): Regex {
return _regex ?: url.let {
val reg = Regex(it);
_regex = reg;
return reg;
}
}
}
@Serializable
class UIMod(
val url: String,
val scale: Float?,
val desktop: Boolean?
) {
@Contextual
private var _regex: Regex? = null;
fun getRegex(): Regex {
return _regex ?: url.let {
val reg = Regex(it);
@@ -194,7 +194,11 @@ class PackageBridge : V8Package {
val stackTrace = Thread.currentThread().stackTrace;
val callerMethod = stackTrace.findLast {
it.className == JSClient::class.java.name
it.className == JSClient::class.java.name &&
it.methodName != "isBusy" &&
it.methodName != "busy" &&
it.methodName != "getCopy" &&
it.methodName != "isBusyWith"
}?.methodName ?: "";
val session = StateApp.instance.sessionId;
val pluginId = _plugin.config.id;
@@ -25,6 +25,7 @@ import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateDeveloper
import com.futo.platformplayer.states.StateHistory
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePlugins
import com.futo.platformplayer.views.buttons.BigButton
@@ -152,11 +153,50 @@ class SourceDetailFragment : MainFragment() {
if(field is View)
field.isVisible = false;
}
if(!source.capabilities.hasGetUserHistory) {
if(!source.capabilities.hasGetUserHistory || !source.isLoggedIn) {
val field = _settingsAppForm.findField("sync");
if(field is View)
field.isVisible = false;
}
else {
val field = _settingsAppForm.findField("syncHistory");
field?.onChanged?.subscribe { field, new, old ->
if(old != new && new == true && StatePlatform.instance.isClientEnabled(config.id)) {
UIDialogs.showDialog(context, R.drawable.ic_sources, "Would you like to sync now?",
"This will attempt to update your history from the platform, when this setting is enabled, it is done during startup.", null, 0,
UIDialogs.Action("No", {
}),
UIDialogs.Action("Yes", {
UIDialogs.showDialogProgress(context, {
it.setText("Importing history..");
fragment.lifecycleScope.launch(Dispatchers.IO) {
try {
val client = StatePlatform.instance.getClient(config.id);
if (client != null && client is JSClient) {
val count = StateHistory.instance.syncRemoteHistory(client);
withContext(Dispatchers.Main) {
it.hide();
if(count > 0)
UIDialogs.showDialogOk(context, R.drawable.ic_pair_success, "Imported ${count} history items");
else
UIDialogs.showDialogOk(context, R.drawable.ic_help, "Imported no history items");
}
}
}
catch(ex: Throwable) {
withContext(Dispatchers.Main) {
UIDialogs.appToast("Sync History failed due to:\n" + ex.message);
it.hide();
}
}
}
});
}, UIDialogs.ActionStyle.PRIMARY));
}
}
}
_settingsAppForm.onChanged.clear();
_settingsAppForm.onChanged.subscribe { field, value ->
_settingsAppChanged = true;
@@ -437,7 +437,7 @@ class VideoDetailFragment() : MainFragment() {
fun onUserLeaveHint() {
val viewDetail = _viewDetail;
Logger.i(TAG, "onUserLeaveHint preventPictureInPicture=${viewDetail?.preventPictureInPicture} isCasting=${StateCasting.instance.isCasting} isBackgroundPictureInPicture=${Settings.instance.playback.isBackgroundPictureInPicture()} allowBackground=${viewDetail?.allowBackground}");
Logger.i(TAG, "onUserLeaveHint preventPictureInPicture=${viewDetail?.preventPictureInPicture} isCasting=${StateCasting.instance.isCasting} isBackgroundPictureInPicture=${Settings.instance.playback.isBackgroundPictureInPicture()} allowBackground=${viewDetail?.isAudioOnlyUserAction}");
if (viewDetail === null) {
return
@@ -446,7 +446,7 @@ class VideoDetailFragment() : MainFragment() {
if (viewDetail.shouldEnterPictureInPicture) {
_leavingPiP = false
}
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.S && viewDetail.preventPictureInPicture == false && !StateCasting.instance.isCasting && Settings.instance.playback.isBackgroundPictureInPicture() && !viewDetail.allowBackground) {
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.S && viewDetail.preventPictureInPicture == false && !StateCasting.instance.isCasting && Settings.instance.playback.isBackgroundPictureInPicture() && !viewDetail.isAudioOnlyUserAction) {
val params = _viewDetail?.getPictureInPictureParams();
if(params != null) {
Logger.i(TAG, "enterPictureInPictureMode")
@@ -526,7 +526,7 @@ class VideoDetailFragment() : MainFragment() {
private fun stopIfRequired() {
var shouldStop = true;
if (_viewDetail?.allowBackground == true) {
if (_viewDetail?.isAudioOnlyUserAction == true) {
shouldStop = false;
} else if (Settings.instance.playback.isBackgroundPictureInPicture() && !_leavingPiP) {
shouldStop = false;
@@ -10,7 +10,6 @@ import android.content.Intent
import android.content.res.Configuration
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.Rect
import android.graphics.drawable.Animatable
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
@@ -51,7 +50,6 @@ import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.UISlideOverlays
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.activities.SyncShowPairingCodeActivity.Companion.activity
import com.futo.platformplayer.api.media.IPluginSourced
import com.futo.platformplayer.api.media.LiveChatManager
import com.futo.platformplayer.api.media.PlatformID
@@ -82,7 +80,6 @@ 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.JSVideo
import com.futo.platformplayer.api.media.platforms.js.models.JSVideoDetails
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
import com.futo.platformplayer.api.media.structures.IPager
@@ -326,7 +323,7 @@ class VideoDetailView : ConstraintLayout {
val onEnterPictureInPicture = Event0();
val onVideoChanged = Event2<Int, Int>()
var allowBackground: Boolean = false
var isAudioOnlyUserAction: Boolean = false
private set(value) {
if (field != value) {
field = value
@@ -338,7 +335,7 @@ class VideoDetailView : ConstraintLayout {
get() = !preventPictureInPicture &&
!StateCasting.instance.isCasting &&
Settings.instance.playback.isBackgroundPictureInPicture() &&
!allowBackground &&
!isAudioOnlyUserAction &&
isPlaying
val onShouldEnterPictureInPictureChanged = Event0();
@@ -764,7 +761,7 @@ class VideoDetailView : ConstraintLayout {
MediaControlReceiver.onBackgroundReceived.subscribe(this) {
Logger.i(TAG, "MediaControlReceiver.onBackgroundReceived")
_player.switchToAudioMode(video);
allowBackground = true;
isAudioOnlyUserAction = true;
StateApp.instance.contextOrNull?.let {
try {
if (it is MainActivity) {
@@ -1008,15 +1005,26 @@ class VideoDetailView : ConstraintLayout {
}
}
_slideUpOverlay?.hide();
} else if(video is JSVideoDetails && (video as JSVideoDetails).hasVODEvents())
RoundButton(context, R.drawable.ic_chat, context.getString(R.string.vod_chat), TAG_VODCHAT) {
video?.let {
try {
loadVODChat(it);
}
catch(ex: Throwable) {
Logger.e(TAG, "Failed to reopen vod chat", ex);
}
}
_slideUpOverlay?.hide();
} else null,
if (!isLimitedVersion) RoundButton(context, R.drawable.ic_screen_share, if (allowBackground) context.getString(R.string.background_revert) else context.getString(R.string.background), TAG_BACKGROUND) {
if (!allowBackground) {
if (!isLimitedVersion) RoundButton(context, R.drawable.ic_screen_share, if (isAudioOnlyUserAction) context.getString(R.string.background_revert) else context.getString(R.string.background), TAG_BACKGROUND) {
if (!isAudioOnlyUserAction) {
_player.switchToAudioMode(video);
allowBackground = true;
isAudioOnlyUserAction = true;
it.text.text = resources.getString(R.string.background_revert);
} else {
_player.switchToVideoMode();
allowBackground = false;
isAudioOnlyUserAction = false;
it.text.text = resources.getString(R.string.background);
}
_slideUpOverlay?.hide();
@@ -1132,10 +1140,14 @@ class VideoDetailView : ConstraintLayout {
//Lifecycle
var isLoginStop = false; //TODO: This is a bit jank, but easiest solution for now without reworking flow. (Alternatively, fix MainActivity getting stopped/disposing video)
fun onResume() {
Logger.v(TAG, "onResume");
_onPauseCalled = false;
val wasLoginCall = isLoginStop;
isLoginStop = false;
Logger.i(TAG, "_video: ${video?.name ?: "no video"}");
Logger.i(TAG, "_didStop: $_didStop");
@@ -1144,7 +1156,7 @@ class VideoDetailView : ConstraintLayout {
val t = (lastPositionMilliseconds / 1000.0f).roundToLong();
if(_searchVideo != null)
setVideoOverview(_searchVideo!!, true, t);
else if(_url != null)
else if(_url != null && !wasLoginCall)
setVideo(_url!!, t, _playWhenReady);
}
else if(_didStop) {
@@ -1156,11 +1168,14 @@ class VideoDetailView : ConstraintLayout {
if(_player.isAudioMode) {
//Requested behavior to leave it in audio mode. leaving it commented if it causes issues, revert?
if(!allowBackground) {
if(!isAudioOnlyUserAction) {
_player.switchToVideoMode();
allowBackground = false;
isAudioOnlyUserAction = false;
_buttonPins.getButtonByTag(TAG_BACKGROUND)?.text?.text = resources.getString(R.string.background);
}
else {
_buttonPins.getButtonByTag(TAG_BACKGROUND)?.text?.text = resources.getString(R.string.video);
}
}
if(!_player.isFitMode && !_player.isFullScreen && !fragment.isInPictureInPicture)
_player.fitHeight();
@@ -1176,7 +1191,7 @@ class VideoDetailView : ConstraintLayout {
if(StateCasting.instance.isCasting)
return;
if(allowBackground)
if(isAudioOnlyUserAction)
StatePlayer.instance.startOrUpdateMediaSession(context, video);
else {
when (Settings.instance.playback.backgroundPlay) {
@@ -1184,7 +1199,6 @@ class VideoDetailView : ConstraintLayout {
1 -> {
if(!(video?.isLive ?: false)) {
_player.switchToAudioMode(video);
allowBackground = true;
}
StatePlayer.instance.startOrUpdateMediaSession(context, video);
}
@@ -1972,10 +1986,10 @@ class VideoDetailView : ConstraintLayout {
if (isLimitedVersion && _player.isAudioMode) {
_player.switchToVideoMode()
allowBackground = false;
isAudioOnlyUserAction = false;
} else {
val thumbnail = video.thumbnails.getHQThumbnail();
if ((videoSource == null || _player.isAudioMode) && !thumbnail.isNullOrBlank())
if ((videoSource == null) && !thumbnail.isNullOrBlank()) // || _player.isAudioMode
Glide.with(context).asBitmap().load(thumbnail)
.into(object: CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
@@ -3265,8 +3279,13 @@ class VideoDetailView : ConstraintLayout {
val id = e.config.let { if(it is SourcePluginConfig) it.id else null };
val didLogin = if(id == null)
false
else StatePlugins.instance.loginPlugin(context, id) {
fetchVideo();
else {
isLoginStop = true;
StatePlugins.instance.loginPlugin(context, id) {
fragment.lifecycleScope.launch(Dispatchers.Main) {
fetchVideo();
}
}
}
if(!didLogin)
UIDialogs.showDialogOk(context, R.drawable.ic_error_pred, "Failed to login");
@@ -3444,6 +3463,7 @@ class VideoDetailView : ConstraintLayout {
const val TAG_SHARE = "share";
const val TAG_OVERLAY = "overlay";
const val TAG_LIVECHAT = "livechat";
const val TAG_VODCHAT = "vodchat";
const val TAG_CHAPTERS = "chapters";
const val TAG_OPEN = "open";
const val TAG_SEND_TO_DEVICE = "send_to_device";
@@ -194,17 +194,18 @@ class StateHistory {
_remoteHistoryDatesStore.save();
}
fun syncRemoteHistory(plugin: JSClient) {
fun syncRemoteHistory(plugin: JSClient): Int {
if (plugin.capabilities.hasGetUserHistory &&
plugin.isLoggedIn) {
Logger.i(TAG, "Syncing remote history for plugin [${plugin.name}]");
val hist = StatePlatform.instance.getUserHistory(plugin.id);
syncRemoteHistory(plugin.id, hist, 100, 3);
return syncRemoteHistory(plugin.id, hist, 100, 3);
}
return 0;
}
fun syncRemoteHistory(pluginId: String, videos: IPager<IPlatformContent>, maxVideos: Int, maxPages: Int) {
fun syncRemoteHistory(pluginId: String, videos: IPager<IPlatformContent>, maxVideos: Int, maxPages: Int): Int {
val lastDate = _remoteHistoryDatesStore.get(pluginId) ?: OffsetDateTime.MIN;
val maxVideosCount = if(maxVideos <= 0) 500 else maxVideos;
val maxPageCount = if(maxPages <= 0) 3 else maxPages;
@@ -272,12 +273,14 @@ class StateHistory {
}
catch(ex: Throwable){}
}
return updated;
}
}
catch(ex: Throwable) {
val plugin = if(pluginId != StateDeveloper.DEV_ID) StatePlugins.instance.getPlugin(pluginId) else null;
Logger.e(TAG, "Sync Remote History failed for [${plugin?.config?.name}] due to: " + ex.message)
}
return 0;
}
companion object {
@@ -179,8 +179,9 @@ class StatePlugins {
}
StateApp.instance.scope.launch(Dispatchers.IO) {
StatePlatform.instance.reloadClient(context, id);
afterLogin.invoke();
StatePlatform.instance.reloadClient(context, id) {
afterLogin.invoke();
}
}
};
return true;
@@ -475,6 +476,7 @@ class StatePlugins {
delay(500);
val client = ManagedHttpClient();
client.setTimeout(10000);
try {
withContext(Dispatchers.Main) {
onProgress.invoke("Validating script", 0.25);
@@ -489,14 +491,14 @@ class StatePlugins {
}
val icon = config.absoluteIconUrl?.let { absIconUrl ->
withContext(Dispatchers.Main) {
onProgress.invoke("Saving plugin", 0.75);
}
val iconResp = client.get(absIconUrl);
if (iconResp.isOk)
return@let iconResp.body?.byteStream()?.use { it.readBytes() };
return@let null;
}
withContext(Dispatchers.Main) {
onProgress.invoke("Saving plugin", 0.75);
}
val installEx = StatePlugins.instance.createPlugin(config, script, icon, true);
if (installEx != null)
throw installEx;
@@ -907,11 +907,14 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
override fun switchToVideoMode() {
super.switchToVideoMode()
setArtwork(null)
//setArtwork(null)
}
override fun switchToAudioMode(video: IPlatformVideoDetails?) {
super.switchToAudioMode(video)
//This causes issues, and is in general confusing, needs improvements
/*
val thumbnail = video?.thumbnails?.getHQThumbnail()
if (!thumbnail.isNullOrBlank()) {
Glide.with(context).asBitmap().load(thumbnail)
@@ -928,5 +931,6 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
}
})
}
*/
}
}
+1
View File
@@ -247,6 +247,7 @@
<string name="membership">Membership</string>
<string name="store">Store</string>
<string name="live_chat">Live Chat</string>
<string name="vod_chat">VOD Chat</string>
<string name="remove">Remove</string>
<string name="space_videos">Videos</string>
<string name="playlist">Playlist</string>