From 8202513993aca4fdd4e720331a6167648243a6fa Mon Sep 17 00:00:00 2001 From: Kai Date: Thu, 22 May 2025 12:12:34 -0500 Subject: [PATCH 1/4] fix stutter when switching to background Changelog: changed --- .../main/java/com/futo/platformplayer/Settings.kt | 5 ----- .../fragment/mainactivity/main/VideoDetailView.kt | 2 +- .../views/video/FutoVideoPlayerBase.kt | 14 ++++++++++++-- app/src/main/res/values/strings.xml | 1 - 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt index 7f827f66..cc70191a 100644 --- a/app/src/main/java/com/futo/platformplayer/Settings.kt +++ b/app/src/main/java/com/futo/platformplayer/Settings.kt @@ -467,11 +467,6 @@ class Settings : FragmentedStorageFileJson() { @FormField(R.string.live_chat_webview, FieldForm.TOGGLE, R.string.use_the_live_chat_web_window_when_available_over_native_implementation, 9) var useLiveChatWindow: Boolean = true; - - - @FormField(R.string.background_switch_audio, FieldForm.TOGGLE, R.string.background_switch_audio_description, 10) - var backgroundSwitchToAudio: Boolean = true; - @FormField(R.string.restart_after_audio_focus_loss, FieldForm.DROPDOWN, R.string.restart_playback_when_gaining_audio_focus_after_a_loss, 11) @DropdownFieldOptionsId(R.array.restart_playback_after_loss) var restartPlaybackAfterLoss: Int = 1; 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 d1a3e41e..901a4898 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 @@ -1104,7 +1104,7 @@ class VideoDetailView : ConstraintLayout { when (Settings.instance.playback.backgroundPlay) { 0 -> handlePause(); 1 -> { - if(!(video?.isLive ?: false) && Settings.instance.playback.backgroundSwitchToAudio) + if(!(video?.isLive ?: false)) _player.switchToAudioMode(); StatePlayer.instance.startOrUpdateMediaSession(context, video); } diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt index c872ca02..60c5dbf2 100644 --- a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt +++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt @@ -252,12 +252,22 @@ abstract class FutoVideoPlayerBase : RelativeLayout { fun switchToVideoMode() { Logger.i(TAG, "Switching to Video Mode"); isAudioMode = false; - loadSelectedSources(playing, true); + val player = exoPlayer ?: return + player.player.trackSelectionParameters = + player.player.trackSelectionParameters + .buildUpon() + .setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, isAudioMode) + .build() } fun switchToAudioMode() { Logger.i(TAG, "Switching to Audio Mode"); isAudioMode = true; - loadSelectedSources(playing, true); + val player = exoPlayer ?: return + player.player.trackSelectionParameters = + player.player.trackSelectionParameters + .buildUpon() + .setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, isAudioMode) + .build() } fun seekTo(ms: Long) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f81f1642..85445796 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -427,7 +427,6 @@ Seek duration Fast-Forward / Fast-Rewind duration Switch to Audio in Background - Optimize bandwidth usage by switching to audio-only stream in background if available, may cause stutter Groups Show Subscription Groups Use Subscription Exchange (Experimental) From 29f1bef0997e4b0a17fb457643aff398b878fb76 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Wed, 4 Jun 2025 20:43:37 +0200 Subject: [PATCH 2/4] Advanced settings option, Playlist id saving for exports and backups, Sync synchronization to prevent dups --- .../java/com/futo/platformplayer/Settings.kt | 31 +++++++- .../platformplayer/states/StatePlaylists.kt | 14 +++- .../futo/platformplayer/states/StateSync.kt | 79 +++++++++++-------- .../views/fields/ButtonField.kt | 4 +- .../views/fields/DropdownField.kt | 7 +- .../futo/platformplayer/views/fields/Field.kt | 8 +- .../platformplayer/views/fields/FieldForm.kt | 37 +++++++-- .../platformplayer/views/fields/GroupField.kt | 3 +- .../views/fields/ReadOnlyTextField.kt | 3 +- .../views/fields/ToggleField.kt | 9 ++- app/src/main/res/values/strings.xml | 2 + 11 files changed, 146 insertions(+), 51 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt index 7f827f66..363eb917 100644 --- a/app/src/main/java/com/futo/platformplayer/Settings.kt +++ b/app/src/main/java/com/futo/platformplayer/Settings.kt @@ -29,6 +29,7 @@ import com.futo.platformplayer.states.StateUpdate import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorageFileJson import com.futo.platformplayer.views.FeedStyle +import com.futo.platformplayer.views.fields.AdvancedField import com.futo.platformplayer.views.fields.DropdownFieldOptionsId import com.futo.platformplayer.views.fields.FieldForm import com.futo.platformplayer.views.fields.FormField @@ -175,6 +176,10 @@ class Settings : FragmentedStorageFileJson() { } }*/ + + @FormField(R.string.advanced_settings, FieldForm.TOGGLE, R.string.advanced_settings_description, -1, "advancedSettings") + var advancedSettings: Boolean = false; + @FormField(R.string.language, "group", -1, 0) var language = LanguageSettings(); @Serializable @@ -224,7 +229,7 @@ class Settings : FragmentedStorageFileJson() { @FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6) var previewFeedItems: Boolean = true; - + @AdvancedField @FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6) var progressBar: Boolean = true; @@ -256,6 +261,7 @@ class Settings : FragmentedStorageFileJson() { @FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 5) var previewFeedItems: Boolean = true; + @AdvancedField @FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6) var progressBar: Boolean = true; @@ -277,6 +283,7 @@ class Settings : FragmentedStorageFileJson() { @Serializable class ChannelSettings { + @AdvancedField @FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6) var progressBar: Boolean = true; } @@ -302,16 +309,20 @@ class Settings : FragmentedStorageFileJson() { @FormField(R.string.use_subscription_exchange, FieldForm.TOGGLE, R.string.use_subscription_exchange_description, 6) var useSubscriptionExchange: Boolean = false; + @AdvancedField @FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6) var previewFeedItems: Boolean = true; + @AdvancedField @FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 7) var progressBar: Boolean = true; + @AdvancedField @FormField(R.string.fetch_on_app_boot, FieldForm.TOGGLE, R.string.shortly_after_opening_the_app_start_fetching_subscriptions, 8) @Serializable(with = FlexibleBooleanSerializer::class) var fetchOnAppBoot: Boolean = true; + @AdvancedField @FormField(R.string.fetch_on_tab_opened, FieldForm.TOGGLE, R.string.fetch_on_tab_opened_description, 9) var fetchOnTabOpen: Boolean = true; @@ -342,13 +353,16 @@ class Settings : FragmentedStorageFileJson() { @FormField(R.string.show_watch_metrics, FieldForm.TOGGLE, R.string.show_watch_metrics_description, 12) var showWatchMetrics: Boolean = false; + @AdvancedField @FormField(R.string.track_playtime_locally, FieldForm.TOGGLE, R.string.track_playtime_locally_description, 13) var allowPlaytimeTracking: Boolean = true; + @AdvancedField @FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 14) var alwaysReloadFromCache: Boolean = false; + @AdvancedField @FormField(R.string.peek_channel_contents, FieldForm.TOGGLE, R.string.peek_channel_contents_description, 15) var peekChannelContents: Boolean = false; @@ -425,9 +439,11 @@ class Settings : FragmentedStorageFileJson() { var preferredPreviewQuality: Int = 5; fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality); + @AdvancedField @FormField(R.string.simplify_sources, FieldForm.TOGGLE, R.string.simplify_sources_description, 4) var simplifySources: Boolean = true; + @AdvancedField @FormField(R.string.always_allow_reverse_landscape_auto_rotate, FieldForm.TOGGLE, R.string.always_allow_reverse_landscape_auto_rotate_description, 5) var alwaysAllowReverseLandscapeAutoRotate: Boolean = true @@ -438,6 +454,7 @@ class Settings : FragmentedStorageFileJson() { fun isBackgroundContinue() = backgroundPlay == 1; fun isBackgroundPictureInPicture() = backgroundPlay == 2; + @AdvancedField @FormField(R.string.resume_after_preview, FieldForm.DROPDOWN, R.string.when_watching_a_video_in_preview_mode_resume_at_the_position_when_opening_the_video_code, 7) @DropdownFieldOptionsId(R.array.resume_after_preview) var resumeAfterPreview: Int = 1; @@ -464,6 +481,7 @@ class Settings : FragmentedStorageFileJson() { }; } + @AdvancedField @FormField(R.string.live_chat_webview, FieldForm.TOGGLE, R.string.use_the_live_chat_web_window_when_available_over_native_implementation, 9) var useLiveChatWindow: Boolean = true; @@ -497,6 +515,7 @@ class Settings : FragmentedStorageFileJson() { @FormField(R.string.autoplay, FieldForm.TOGGLE, R.string.autoplay, 21) var autoplay: Boolean = false; + @AdvancedField @FormField(R.string.delete_watchlist_on_finish, FieldForm.TOGGLE, R.string.delete_watchlist_on_finish_description, 22) var deleteFromWatchLaterAuto: Boolean = true; @@ -530,6 +549,7 @@ class Settings : FragmentedStorageFileJson() { @FormField(R.string.default_recommendations, FieldForm.TOGGLE, R.string.default_recommendations_description, 0) var recommendationsDefault: Boolean = false; + @AdvancedField @FormField(R.string.hide_recommendations, FieldForm.TOGGLE, R.string.hide_recommendations_description, 0) var hideRecommendations: Boolean = false; @@ -566,10 +586,12 @@ class Settings : FragmentedStorageFileJson() { var preferredAudioQuality: Int = 1; fun isHighBitrateDefault(): Boolean = preferredAudioQuality > 0; + @AdvancedField @FormField(R.string.byte_range_download, FieldForm.TOGGLE, R.string.attempt_to_utilize_byte_ranges, 4) @Serializable(with = FlexibleBooleanSerializer::class) var byteRangeDownload: Boolean = true; + @AdvancedField @FormField(R.string.byte_range_concurrency, FieldForm.DROPDOWN, R.string.number_of_concurrent_threads_to_multiply_download_speeds_from_throttled_sources, 5) @DropdownFieldOptionsId(R.array.thread_count) var byteRangeConcurrency: Int = 3; @@ -599,11 +621,12 @@ class Settings : FragmentedStorageFileJson() { @Serializable(with = FlexibleBooleanSerializer::class) var keepScreenOn: Boolean = true; + @AdvancedField @FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 3) @Serializable(with = FlexibleBooleanSerializer::class) var alwaysProxyRequests: Boolean = false; - + @AdvancedField @FormField(R.string.allow_ipv6, FieldForm.TOGGLE, R.string.allow_ipv6_description, 4) @Serializable(with = FlexibleBooleanSerializer::class) var allowIpv6: Boolean = true; @@ -675,9 +698,11 @@ class Settings : FragmentedStorageFileJson() { @Serializable class Plugins { + @AdvancedField @FormField(R.string.check_disabled_plugin_updates, FieldForm.TOGGLE, R.string.check_disabled_plugin_updates_description, -1) var checkDisabledPluginsForUpdates: Boolean = false; + @AdvancedField @FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0) var clearCookiesOnLogout: Boolean = true; @@ -896,8 +921,10 @@ class Settings : FragmentedStorageFileJson() { var other = Other(); @Serializable class Other { + @AdvancedField @FormField(R.string.playlist_delete_confirmation, FieldForm.TOGGLE, R.string.playlist_delete_confirmation_description, 2) var playlistDeleteConfirmation: Boolean = true; + @AdvancedField @FormField(R.string.playlist_allow_dups, FieldForm.TOGGLE, R.string.playlist_allow_dups_description, 3) var playlistAllowDups: Boolean = true; 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 c20375f2..6ebf7be6 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt @@ -423,17 +423,25 @@ class StatePlaylists { class PlaylistBackup: ReconstructStore() { override fun toReconstruction(obj: Playlist): String { val items = ArrayList(); - items.add(obj.name); + items.add(obj.name + ":::" + obj.id); items.addAll(obj.videos.map { it.url }); return items.map { it.replace("\n","") }.joinToString("\n"); } override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder, importCache: ImportCache?): Playlist { + var idToUse = id; val items = backup.split("\n"); if(items.size <= 0) { throw IllegalStateException("Cannot reconstructor playlist ${id}"); } - val name = items[0]; + var name = items[0]; + if(name.contains(":::")){ + val splitIndex = name.indexOf(":::"); + val foundId = name.substring(splitIndex + 3); + if(!foundId.isNullOrEmpty()) + idToUse = foundId; + name = name.substring(0, splitIndex); + } val videos = items.drop(1).filter { it.isNotEmpty() }.map { try { val videoUrl = it; @@ -465,7 +473,7 @@ class StatePlaylists { throw ReconstructionException(name, "${name}:[${it}] ${ex.message}", ex); } }.filter { it != null }.map { it!! } - return Playlist(id, name, videos); + return Playlist(idToUse, name, videos); } } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/states/StateSync.kt b/app/src/main/java/com/futo/platformplayer/states/StateSync.kt index fd08165c..3cc52a2d 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateSync.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateSync.kt @@ -224,6 +224,11 @@ class StateSync { } } + private val _lockSubscriptions = Any(); + private val _lockSubscriptionGroups = Any(); + private val _lockPlaylists = Any(); + private val _lockWatchlater = Any(); + private fun handleData(session: SyncSession, opcode: UByte, subOpcode: UByte, data: ByteBuffer) { val remotePublicKey = session.remotePublicKey when (subOpcode) { @@ -307,7 +312,9 @@ class StateSync { data.get(dataBody); val json = String(dataBody, Charsets.UTF_8); val subPackage = Serializer.json.decodeFromString(json); - handleSyncSubscriptionPackage(session, subPackage); + synchronized(_lockSubscriptions) { + handleSyncSubscriptionPackage(session, subPackage); + } if(subPackage.subscriptions.size > 0) { val newestSub = subPackage.subscriptions.maxOf { it.creationTime }; @@ -327,21 +334,23 @@ class StateSync { val pack = Serializer.json.decodeFromString(json); var lastSubgroupChange = OffsetDateTime.MIN; - for(group in pack.groups){ - if(group.lastChange > lastSubgroupChange) - lastSubgroupChange = group.lastChange; + synchronized(_lockSubscriptionGroups) { + for(group in pack.groups){ + if(group.lastChange > lastSubgroupChange) + lastSubgroupChange = group.lastChange; - val existing = StateSubscriptionGroups.instance.getSubscriptionGroup(group.id); + val existing = StateSubscriptionGroups.instance.getSubscriptionGroup(group.id); - if(existing == null) - StateSubscriptionGroups.instance.updateSubscriptionGroup(group, false, true); - else if(existing.lastChange < group.lastChange) { - existing.name = group.name; - existing.urls = group.urls; - existing.image = group.image; - existing.priority = group.priority; - existing.lastChange = group.lastChange; - StateSubscriptionGroups.instance.updateSubscriptionGroup(existing, false, true); + if(existing == null) + StateSubscriptionGroups.instance.updateSubscriptionGroup(group, false, true); + else if(existing.lastChange < group.lastChange) { + existing.name = group.name; + existing.urls = group.urls; + existing.image = group.image; + existing.priority = group.priority; + existing.lastChange = group.lastChange; + StateSubscriptionGroups.instance.updateSubscriptionGroup(existing, false, true); + } } } for(removal in pack.groupRemovals) { @@ -358,18 +367,20 @@ class StateSync { val json = String(dataBody, Charsets.UTF_8); val pack = Serializer.json.decodeFromString(json); - for(playlist in pack.playlists) { - val existing = StatePlaylists.instance.getPlaylist(playlist.id); + synchronized(_lockPlaylists) { + for(playlist in pack.playlists) { + val existing = StatePlaylists.instance.getPlaylist(playlist.id); - if(existing == null) - StatePlaylists.instance.createOrUpdatePlaylist(playlist, false); - else if(existing.dateUpdate < playlist.dateUpdate) { - existing.dateUpdate = playlist.dateUpdate; - existing.name = playlist.name; - existing.videos = playlist.videos; - existing.dateCreation = playlist.dateCreation; - existing.datePlayed = playlist.datePlayed; - StatePlaylists.instance.createOrUpdatePlaylist(existing, false); + if(existing == null) + StatePlaylists.instance.createOrUpdatePlaylist(playlist, false); + else if(existing.dateUpdate < playlist.dateUpdate) { + existing.dateUpdate = playlist.dateUpdate; + existing.name = playlist.name; + existing.videos = playlist.videos; + existing.dateCreation = playlist.dateCreation; + existing.datePlayed = playlist.datePlayed; + StatePlaylists.instance.createOrUpdatePlaylist(existing, false); + } } } for(removal in pack.playlistRemovals) { @@ -390,14 +401,16 @@ class StateSync { Logger.i(TAG, "SyncWatchLater received ${pack.videos.size} (${pack.videoAdds?.size}, ${pack.videoRemovals?.size})"); val allExisting = StatePlaylists.instance.getWatchLater(); - for(video in pack.videos) { - val existing = allExisting.firstOrNull { it.url == video.url }; - val time = if(pack.videoAdds != null && pack.videoAdds.containsKey(video.url)) (pack.videoAdds[video.url] ?: 0).sToOffsetDateTimeUTC() else OffsetDateTime.MIN; - val removalTime = StatePlaylists.instance.getWatchLaterRemovalTime(video.url) ?: OffsetDateTime.MIN; - if(existing == null && time > removalTime) { - StatePlaylists.instance.addToWatchLater(video, false); - if(time > OffsetDateTime.MIN) - StatePlaylists.instance.setWatchLaterAddTime(video.url, time); + synchronized(_lockWatchlater) { + for(video in pack.videos) { + val existing = allExisting.firstOrNull { it.url == video.url }; + val time = if(pack.videoAdds != null && pack.videoAdds.containsKey(video.url)) (pack.videoAdds[video.url] ?: 0).sToOffsetDateTimeUTC() else OffsetDateTime.MIN; + val removalTime = StatePlaylists.instance.getWatchLaterRemovalTime(video.url) ?: OffsetDateTime.MIN; + if(existing == null && time > removalTime) { + StatePlaylists.instance.addToWatchLater(video, false); + if(time > OffsetDateTime.MIN) + StatePlaylists.instance.setWatchLaterAddTime(video.url, time); + } } } for(removal in pack.videoRemovals) { diff --git a/app/src/main/java/com/futo/platformplayer/views/fields/ButtonField.kt b/app/src/main/java/com/futo/platformplayer/views/fields/ButtonField.kt index 03af1ee8..a7e142f8 100644 --- a/app/src/main/java/com/futo/platformplayer/views/fields/ButtonField.kt +++ b/app/src/main/java/com/futo/platformplayer/views/fields/ButtonField.kt @@ -41,6 +41,8 @@ class ButtonField : BigButton, IField { return null; }; + override var isAdvanced: Boolean = false; + //private val _title : TextView; //private val _subtitle : TextView; @@ -89,7 +91,7 @@ class ButtonField : BigButton, IField { return this; } - override fun fromField(obj : Any, field : Field, formField: FormField?) : ButtonField { + override fun fromField(obj : Any, field : Field, formField: FormField?, advanced: Boolean) : ButtonField { throw IllegalStateException("ButtonField should only be used for methods"); } override fun setField() { diff --git a/app/src/main/java/com/futo/platformplayer/views/fields/DropdownField.kt b/app/src/main/java/com/futo/platformplayer/views/fields/DropdownField.kt index 590335d3..4f01d4d5 100644 --- a/app/src/main/java/com/futo/platformplayer/views/fields/DropdownField.kt +++ b/app/src/main/java/com/futo/platformplayer/views/fields/DropdownField.kt @@ -40,6 +40,8 @@ class DropdownField : TableRow, IField { override var reference: Any? = null; + override var isAdvanced: Boolean = false; + override val onChanged = Event3(); override val value: Any? get() = _selected; @@ -112,7 +114,7 @@ class DropdownField : TableRow, IField { return this; } - override fun fromField(obj: Any, field: Field, formField: FormField?) : DropdownField { + override fun fromField(obj: Any, field: Field, formField: FormField?, advanced: Boolean) : DropdownField { this._field = field; this._obj = obj; @@ -133,6 +135,9 @@ class DropdownField : TableRow, IField { _description.visibility = View.GONE; } + val advancedFieldAttr = field.getAnnotation(AdvancedField::class.java) + if(advancedFieldAttr != null || advanced) + isAdvanced = true; _options = (field.getAnnotation(DropdownFieldOptions::class.java)?.options ?: field.getAnnotation(DropdownFieldOptionsId::class.java)?.optionsId?.let { resources.getStringArray(it) } ?: diff --git a/app/src/main/java/com/futo/platformplayer/views/fields/Field.kt b/app/src/main/java/com/futo/platformplayer/views/fields/Field.kt index 9439d46a..b11ce641 100644 --- a/app/src/main/java/com/futo/platformplayer/views/fields/Field.kt +++ b/app/src/main/java/com/futo/platformplayer/views/fields/Field.kt @@ -4,6 +4,10 @@ import com.futo.platformplayer.constructs.Event2 import com.futo.platformplayer.constructs.Event3 import java.lang.reflect.Field +@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.RUNTIME) +annotation class AdvancedField(); + @Target(AnnotationTarget.FIELD, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY) @Retention(AnnotationRetention.RUNTIME) @@ -22,6 +26,8 @@ interface IField { val obj : Any?; val field : Field?; + val isAdvanced: Boolean; + val value: Any?; val onChanged : Event3; @@ -29,7 +35,7 @@ interface IField { val searchContent: String?; - fun fromField(obj : Any, field : Field, formField: FormField? = null) : IField; + fun fromField(obj : Any, field : Field, formField: FormField? = null, advanced: Boolean = false) : IField; fun setField(); fun setValue(value: Any); diff --git a/app/src/main/java/com/futo/platformplayer/views/fields/FieldForm.kt b/app/src/main/java/com/futo/platformplayer/views/fields/FieldForm.kt index 1262e345..566a5024 100644 --- a/app/src/main/java/com/futo/platformplayer/views/fields/FieldForm.kt +++ b/app/src/main/java/com/futo/platformplayer/views/fields/FieldForm.kt @@ -37,6 +37,8 @@ class FieldForm : LinearLayout { private var _fields : List = arrayListOf(); + private var _showAdvancedSettings: Boolean = false; + constructor(context : Context, attrs : AttributeSet? = null) : super(context, attrs) { inflate(context, R.layout.field_form, this); _containerSearch = findViewById(R.id.container_search); @@ -58,11 +60,17 @@ class FieldForm : LinearLayout { if(field is GroupField) { updateSettingsVisibility(field); } else if(field is View && field.descriptor != null) { - val txt = field.searchContent?.lowercase(); - if(txt != null) { - val visible = isGroupMatch || txt.contains(query); - field.visibility = if (visible) View.VISIBLE else View.GONE; - groupVisible = groupVisible || visible; + if(field.isAdvanced && !_showAdvancedSettings) + { + field.visibility = View.GONE; + } + else { + val txt = field.searchContent?.lowercase(); + if (txt != null) { + val visible = isGroupMatch || txt.contains(query); + field.visibility = if (visible) View.VISIBLE else View.GONE; + groupVisible = groupVisible || visible; + } } } } @@ -71,6 +79,10 @@ class FieldForm : LinearLayout { } } + fun setShowAdvancedSettings(show: Boolean) { + _showAdvancedSettings = show; + updateSettingsVisibility(); + } fun setSearchQuery(query: String) { _editSearch.setText(query); updateSettingsVisibility(); @@ -92,13 +104,22 @@ class FieldForm : LinearLayout { throw java.lang.IllegalStateException("Only views can be IFields"); } + if(field is ToggleField && field.descriptor?.id == "advancedSettings") { + _showAdvancedSettings = field.value as Boolean; + } + _fieldsContainer.addView(field as View); field.onChanged.subscribe { a1, a2, _ -> + if(field is ToggleField && field.descriptor?.id == "advancedSettings") { + setShowAdvancedSettings((a2 as Boolean)); + } + onChanged.emit(a1, a2); }; } _fields = newFields; + updateSettingsVisibility(); onLoaded?.invoke(); } } @@ -267,10 +288,12 @@ class FieldForm : LinearLayout { for(prop in objFields) { prop.first.javaField!!.isAccessible = true; + val advanced = prop.first.hasAnnotation(); + val field = when(prop.second.type) { GROUP -> GroupField(context).fromField(obj, prop.first.javaField!!, prop.second); - DROPDOWN -> DropdownField(context).fromField(obj, prop.first.javaField!!, prop.second); - TOGGLE -> ToggleField(context).fromField(obj, prop.first.javaField!!, prop.second); + DROPDOWN -> DropdownField(context).fromField(obj, prop.first.javaField!!, prop.second, advanced); + TOGGLE -> ToggleField(context).fromField(obj, prop.first.javaField!!, prop.second, advanced); READONLYTEXT -> ReadOnlyTextField(context).fromField(obj, prop.first.javaField!!, prop.second); else -> throw java.lang.IllegalStateException("Unknown field type ${prop.second.type} for ${prop.second.title}") } diff --git a/app/src/main/java/com/futo/platformplayer/views/fields/GroupField.kt b/app/src/main/java/com/futo/platformplayer/views/fields/GroupField.kt index 133fb788..9621285a 100644 --- a/app/src/main/java/com/futo/platformplayer/views/fields/GroupField.kt +++ b/app/src/main/java/com/futo/platformplayer/views/fields/GroupField.kt @@ -34,6 +34,7 @@ class GroupField : LinearLayout, IField { private val _container : LinearLayout; override var reference: Any? = null; + override var isAdvanced: Boolean = false; override val value: Any? = null; @@ -100,7 +101,7 @@ class GroupField : LinearLayout, IField { return this; } - override fun fromField(obj: Any, field: Field, formField: FormField?) : GroupField { + override fun fromField(obj: Any, field: Field, formField: FormField?, advanced: Boolean) : GroupField { this._field = field; this._obj = obj; diff --git a/app/src/main/java/com/futo/platformplayer/views/fields/ReadOnlyTextField.kt b/app/src/main/java/com/futo/platformplayer/views/fields/ReadOnlyTextField.kt index d0cfb7dc..3fb78aeb 100644 --- a/app/src/main/java/com/futo/platformplayer/views/fields/ReadOnlyTextField.kt +++ b/app/src/main/java/com/futo/platformplayer/views/fields/ReadOnlyTextField.kt @@ -31,6 +31,7 @@ class ReadOnlyTextField : TableRow, IField { override val onChanged = Event3(); override var reference: Any? = null; + override var isAdvanced: Boolean = false; override val value: Any? = null; @@ -45,7 +46,7 @@ class ReadOnlyTextField : TableRow, IField { override fun setValue(value: Any) {} - override fun fromField(obj : Any, field : Field, formField: FormField?) : ReadOnlyTextField { + override fun fromField(obj : Any, field : Field, formField: FormField?, advanced: Boolean) : ReadOnlyTextField { this._field = field; this._obj = obj; diff --git a/app/src/main/java/com/futo/platformplayer/views/fields/ToggleField.kt b/app/src/main/java/com/futo/platformplayer/views/fields/ToggleField.kt index 8e1cfbbb..c419092c 100644 --- a/app/src/main/java/com/futo/platformplayer/views/fields/ToggleField.kt +++ b/app/src/main/java/com/futo/platformplayer/views/fields/ToggleField.kt @@ -33,6 +33,7 @@ class ToggleField : TableRow, IField { private var _lastValue: Boolean = false; override var reference: Any? = null; + override var isAdvanced: Boolean = false; override val onChanged = Event3(); @@ -75,7 +76,7 @@ class ToggleField : TableRow, IField { return this; } - override fun fromField(obj : Any, field : Field, formField: FormField?) : ToggleField { + override fun fromField(obj : Any, field : Field, formField: FormField?, advanced: Boolean) : ToggleField { this._field = field; this._obj = obj; @@ -87,6 +88,12 @@ class ToggleField : TableRow, IField { else _title.text = field.name; + val advancedFieldAttr = field.getAnnotation(AdvancedField::class.java) + if(advancedFieldAttr != null || advanced) { + Logger.w("ToggleField", "Found advanced field: " + field.name); + isAdvanced = true; + } + if(attrField == null || attrField.subtitle == -1) _description.visibility = View.GONE; else { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f81f1642..d382860d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -12,6 +12,8 @@ Channel Home Progress Bar + Advanced Settings + If advanced settings should be shown, this exposes additional settings to finetune your experience. If a historical progress bar should be shown Hide hidden from home in search Hide videos and creators hidden from home also in search results From e3e7b0c345ea646667ab2737f7d7a4d9c8a0d4de Mon Sep 17 00:00:00 2001 From: Kelvin Date: Wed, 4 Jun 2025 20:50:10 +0200 Subject: [PATCH 3/4] More advanced settings --- app/src/main/java/com/futo/platformplayer/Settings.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt index 363eb917..c25efdb0 100644 --- a/app/src/main/java/com/futo/platformplayer/Settings.kt +++ b/app/src/main/java/com/futo/platformplayer/Settings.kt @@ -226,6 +226,7 @@ class Settings : FragmentedStorageFileJson() { @FormField(R.string.show_home_filters_plugin_names, FieldForm.TOGGLE, R.string.show_home_filters_plugin_names_description, 5) var showHomeFiltersPluginNames: Boolean = false; + @AdvancedField @FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6) var previewFeedItems: Boolean = true; @@ -258,6 +259,7 @@ class Settings : FragmentedStorageFileJson() { @DropdownFieldOptionsId(R.array.feed_style) var searchFeedStyle: Int = 1; + @AdvancedField @FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 5) var previewFeedItems: Boolean = true; From ccc686ed50b3fa2cb7253f61d87cebdeedfbd336 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Wed, 4 Jun 2025 21:26:41 +0200 Subject: [PATCH 4/4] Downloads size ordering, Subsgroup removal on unsubscribe, multi-key like querying --- .../mainactivity/main/DownloadsFragment.kt | 6 ++++- .../platformplayer/states/StateHistory.kt | 9 ++++++-- .../states/StateSubscriptions.kt | 15 +++++++++++-- .../stores/db/ManagedDBStore.kt | 22 +++++++++++++++++++ app/src/main/res/values/strings.xml | 2 ++ 5 files changed, 49 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/DownloadsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/DownloadsFragment.kt index 536700d8..0e429430 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/DownloadsFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/DownloadsFragment.kt @@ -150,7 +150,7 @@ class DownloadsFragment : MainFragment() { spinnerSortBy.adapter = ArrayAdapter(context, R.layout.spinner_item_simple, resources.getStringArray(R.array.downloads_sortby_array)).also { it.setDropDownViewResource(R.layout.spinner_dropdownitem_simple); }; - val options = listOf("nameAsc", "nameDesc", "downloadDateAsc", "downloadDateDesc", "releasedAsc", "releasedDesc"); + val options = listOf("nameAsc", "nameDesc", "downloadDateAsc", "downloadDateDesc", "releasedAsc", "releasedDesc", "sizeAsc", "sizeDesc"); spinnerSortBy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) { when(pos) { @@ -160,6 +160,8 @@ class DownloadsFragment : MainFragment() { 3 -> ordering.setAndSave("downloadDateDesc") 4 -> ordering.setAndSave("releasedAsc") 5 -> ordering.setAndSave("releasedDesc") + 6 -> ordering.setAndSave("sizeAsc") + 7 -> ordering.setAndSave("sizeDesc") else -> ordering.setAndSave("") } updateContentFilters() @@ -257,6 +259,8 @@ class DownloadsFragment : MainFragment() { "nameDesc" -> vidsToReturn.sortedByDescending { it.name.lowercase() } "releasedAsc" -> vidsToReturn.sortedBy { it.datetime ?: OffsetDateTime.MAX } "releasedDesc" -> vidsToReturn.sortedByDescending { it.datetime ?: OffsetDateTime.MIN } + "sizeAsc" -> vidsToReturn.sortedBy { it.videoSource.sumOf { it.fileSize } + it.audioSource.sumOf { it.fileSize } } + "sizeDesc" -> vidsToReturn.sortedByDescending { it.videoSource.sumOf { it.fileSize } + it.audioSource.sumOf { it.fileSize } } else -> vidsToReturn } } diff --git a/app/src/main/java/com/futo/platformplayer/states/StateHistory.kt b/app/src/main/java/com/futo/platformplayer/states/StateHistory.kt index 3047731d..97ab82c1 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateHistory.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateHistory.kt @@ -131,8 +131,13 @@ class StateHistory { fun getHistoryPager(): IPager { return _historyDBStore.getObjectPager(); } - fun getHistorySearchPager(query: String): IPager { - return _historyDBStore.queryLikeObjectPager(DBHistory.Index::name, "%${query}%", 10); + fun getHistorySearchPager(query: String, withAuthor: Boolean = false): IPager { + return if(!withAuthor) + _historyDBStore.queryLikeObjectPager(DBHistory.Index::name, "%${query}%", 10) + else + _historyDBStore.queryLikeObjectPager(DBHistory.Index::name, "%${query}%", 10) + //_historyDBStore.queryLike2ObjectPager(DBHistory.Index::name, DBHistory.Index::auth,"%${query}%", 10) + //TODO: See if we can include author name? } fun getHistoryIndexByUrl(url: String): DBHistory.Index? { return historyIndex[url]; diff --git a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt index 1d1acff6..60026ea6 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt @@ -329,8 +329,19 @@ class StateSubscriptions { } } - if(StateSubscriptionGroups.instance.hasSubscriptionGroup(sub.channel.url)) - getSubscriptionOtherOrCreate(sub.channel.url, sub.channel.name, sub.channel.thumbnail); + if(StateSubscriptionGroups.instance.hasSubscriptionGroup(sub.channel.url)) { + val subGroups = StateSubscriptionGroups.instance.getSubscriptionGroups().filter { it.urls.contains(sub.channel.url) }; + for(group in subGroups) { + group.urls.remove(sub.channel.url); + StateSubscriptionGroups.instance.updateSubscriptionGroup(group); + } + /* + getSubscriptionOtherOrCreate( + sub.channel.url, + sub.channel.name, + sub.channel.thumbnail + ); */ + } } return sub; } diff --git a/app/src/main/java/com/futo/platformplayer/stores/db/ManagedDBStore.kt b/app/src/main/java/com/futo/platformplayer/stores/db/ManagedDBStore.kt index 2e493eef..6b9ed65f 100644 --- a/app/src/main/java/com/futo/platformplayer/stores/db/ManagedDBStore.kt +++ b/app/src/main/java/com/futo/platformplayer/stores/db/ManagedDBStore.kt @@ -274,10 +274,17 @@ class ManagedDBStore, T, D: ManagedDBDatabase, DA val queryStr = "SELECT * FROM ${descriptor.table_name} WHERE ${field} LIKE ? ${_orderSQL} LIMIT ? OFFSET ?"; val query = SimpleSQLiteQuery(queryStr, arrayOf(obj, pageSize, page * pageSize)); return deserializeIndexes(dbDaoBase.getMultiple(query)); + }fun queryLike2Page(field: String, field2: String, obj: String, page: Int, pageSize: Int): List { + val queryStr = "SELECT * FROM ${descriptor.table_name} WHERE ${field} LIKE ? OR ${field2} LIKE ? ${_orderSQL} LIMIT ? OFFSET ?"; + val query = SimpleSQLiteQuery(queryStr, arrayOf(obj, obj, pageSize, page * pageSize)); + return deserializeIndexes(dbDaoBase.getMultiple(query)); } fun queryLikeObjectPage(field: String, obj: String, page: Int, pageSize: Int): List { return convertObjects(queryLikePage(field, obj, page, pageSize)); } + fun queryLike2ObjectPage(field: String, field2: String, obj: String, page: Int, pageSize: Int): List { + return convertObjects(queryLike2Page(field, field2, obj, page, pageSize)); + } //Query Page Objects @@ -336,6 +343,13 @@ class ManagedDBStore, T, D: ManagedDBDatabase, DA queryLikePage(field, obj, it - 1, pageSize); }); } + fun queryLike2Pager(field: KProperty<*>, field2: KProperty<*>, obj: String, pageSize: Int): IPager = queryLike2Pager(validateFieldName(field), validateFieldName(field2), obj, pageSize); + fun queryLike2Pager(field: String, field2: String, obj: String, pageSize: Int): IPager { + return AdhocPager({ + Logger.i("ManagedDBStore", "Next Page [query: ${obj}](${it}) ${pageSize}"); + queryLike2Page(field, field2, obj, it - 1, pageSize); + }); + } fun queryLikeObjectPager(field: KProperty<*>, obj: String, pageSize: Int): IPager = queryLikeObjectPager(validateFieldName(field), obj, pageSize); fun queryLikeObjectPager(field: String, obj: String, pageSize: Int): IPager { return AdhocPager({ @@ -344,6 +358,14 @@ class ManagedDBStore, T, D: ManagedDBDatabase, DA }); } + fun queryLike2ObjectPager(field: KProperty<*>, field2: KProperty<*>, obj: String, pageSize: Int): IPager = queryLike2ObjectPager(validateFieldName(field), validateFieldName(field2), obj, pageSize); + fun queryLike2ObjectPager(field: String, field2: String, obj: String, pageSize: Int): IPager { + return AdhocPager({ + Logger.i("ManagedDBStore", "Next Page [query: ${obj}](${it}) ${pageSize}"); + queryLike2ObjectPage(field, field2, obj, it - 1, pageSize); + }); + } + //Query Pager with convert fun queryPager(field: KProperty<*>, obj: Any, pageSize: Int, convert: (I)->X): IPager = queryPager(validateFieldName(field), obj, pageSize, convert); fun queryPager(field: String, obj: Any, pageSize: Int, convert: (I)->X): IPager { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d382860d..9acb16a1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -996,6 +996,8 @@ Download Date (Newest) Release Date (Oldest) Release Date (Newest) + Size (Smallest) + Size (Largest) Name (Ascending)