Advanced settings option, Playlist id saving for exports and backups, Sync synchronization to prevent dups

This commit is contained in:
Kelvin 2025-06-04 20:43:37 +02:00
commit 29f1bef099
11 changed files with 146 additions and 51 deletions

View file

@ -29,6 +29,7 @@ import com.futo.platformplayer.states.StateUpdate
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.FragmentedStorageFileJson import com.futo.platformplayer.stores.FragmentedStorageFileJson
import com.futo.platformplayer.views.FeedStyle 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.DropdownFieldOptionsId
import com.futo.platformplayer.views.fields.FieldForm import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.FormField 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) @FormField(R.string.language, "group", -1, 0)
var language = LanguageSettings(); var language = LanguageSettings();
@Serializable @Serializable
@ -224,7 +229,7 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6) @FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
var previewFeedItems: Boolean = true; var previewFeedItems: Boolean = true;
@AdvancedField
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6) @FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = true; 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) @FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 5)
var previewFeedItems: Boolean = true; var previewFeedItems: Boolean = true;
@AdvancedField
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6) @FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = true; var progressBar: Boolean = true;
@ -277,6 +283,7 @@ class Settings : FragmentedStorageFileJson() {
@Serializable @Serializable
class ChannelSettings { class ChannelSettings {
@AdvancedField
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6) @FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
var progressBar: Boolean = true; 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) @FormField(R.string.use_subscription_exchange, FieldForm.TOGGLE, R.string.use_subscription_exchange_description, 6)
var useSubscriptionExchange: Boolean = false; var useSubscriptionExchange: Boolean = false;
@AdvancedField
@FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6) @FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
var previewFeedItems: Boolean = true; var previewFeedItems: Boolean = true;
@AdvancedField
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 7) @FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 7)
var progressBar: Boolean = true; 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) @FormField(R.string.fetch_on_app_boot, FieldForm.TOGGLE, R.string.shortly_after_opening_the_app_start_fetching_subscriptions, 8)
@Serializable(with = FlexibleBooleanSerializer::class) @Serializable(with = FlexibleBooleanSerializer::class)
var fetchOnAppBoot: Boolean = true; var fetchOnAppBoot: Boolean = true;
@AdvancedField
@FormField(R.string.fetch_on_tab_opened, FieldForm.TOGGLE, R.string.fetch_on_tab_opened_description, 9) @FormField(R.string.fetch_on_tab_opened, FieldForm.TOGGLE, R.string.fetch_on_tab_opened_description, 9)
var fetchOnTabOpen: Boolean = true; 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) @FormField(R.string.show_watch_metrics, FieldForm.TOGGLE, R.string.show_watch_metrics_description, 12)
var showWatchMetrics: Boolean = false; var showWatchMetrics: Boolean = false;
@AdvancedField
@FormField(R.string.track_playtime_locally, FieldForm.TOGGLE, R.string.track_playtime_locally_description, 13) @FormField(R.string.track_playtime_locally, FieldForm.TOGGLE, R.string.track_playtime_locally_description, 13)
var allowPlaytimeTracking: Boolean = true; var allowPlaytimeTracking: Boolean = true;
@AdvancedField
@FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 14) @FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 14)
var alwaysReloadFromCache: Boolean = false; var alwaysReloadFromCache: Boolean = false;
@AdvancedField
@FormField(R.string.peek_channel_contents, FieldForm.TOGGLE, R.string.peek_channel_contents_description, 15) @FormField(R.string.peek_channel_contents, FieldForm.TOGGLE, R.string.peek_channel_contents_description, 15)
var peekChannelContents: Boolean = false; var peekChannelContents: Boolean = false;
@ -425,9 +439,11 @@ class Settings : FragmentedStorageFileJson() {
var preferredPreviewQuality: Int = 5; var preferredPreviewQuality: Int = 5;
fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality); fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality);
@AdvancedField
@FormField(R.string.simplify_sources, FieldForm.TOGGLE, R.string.simplify_sources_description, 4) @FormField(R.string.simplify_sources, FieldForm.TOGGLE, R.string.simplify_sources_description, 4)
var simplifySources: Boolean = true; 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) @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 var alwaysAllowReverseLandscapeAutoRotate: Boolean = true
@ -438,6 +454,7 @@ class Settings : FragmentedStorageFileJson() {
fun isBackgroundContinue() = backgroundPlay == 1; fun isBackgroundContinue() = backgroundPlay == 1;
fun isBackgroundPictureInPicture() = backgroundPlay == 2; 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) @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) @DropdownFieldOptionsId(R.array.resume_after_preview)
var resumeAfterPreview: Int = 1; 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) @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; var useLiveChatWindow: Boolean = true;
@ -497,6 +515,7 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.autoplay, FieldForm.TOGGLE, R.string.autoplay, 21) @FormField(R.string.autoplay, FieldForm.TOGGLE, R.string.autoplay, 21)
var autoplay: Boolean = false; var autoplay: Boolean = false;
@AdvancedField
@FormField(R.string.delete_watchlist_on_finish, FieldForm.TOGGLE, R.string.delete_watchlist_on_finish_description, 22) @FormField(R.string.delete_watchlist_on_finish, FieldForm.TOGGLE, R.string.delete_watchlist_on_finish_description, 22)
var deleteFromWatchLaterAuto: Boolean = true; var deleteFromWatchLaterAuto: Boolean = true;
@ -530,6 +549,7 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.default_recommendations, FieldForm.TOGGLE, R.string.default_recommendations_description, 0) @FormField(R.string.default_recommendations, FieldForm.TOGGLE, R.string.default_recommendations_description, 0)
var recommendationsDefault: Boolean = false; var recommendationsDefault: Boolean = false;
@AdvancedField
@FormField(R.string.hide_recommendations, FieldForm.TOGGLE, R.string.hide_recommendations_description, 0) @FormField(R.string.hide_recommendations, FieldForm.TOGGLE, R.string.hide_recommendations_description, 0)
var hideRecommendations: Boolean = false; var hideRecommendations: Boolean = false;
@ -566,10 +586,12 @@ class Settings : FragmentedStorageFileJson() {
var preferredAudioQuality: Int = 1; var preferredAudioQuality: Int = 1;
fun isHighBitrateDefault(): Boolean = preferredAudioQuality > 0; fun isHighBitrateDefault(): Boolean = preferredAudioQuality > 0;
@AdvancedField
@FormField(R.string.byte_range_download, FieldForm.TOGGLE, R.string.attempt_to_utilize_byte_ranges, 4) @FormField(R.string.byte_range_download, FieldForm.TOGGLE, R.string.attempt_to_utilize_byte_ranges, 4)
@Serializable(with = FlexibleBooleanSerializer::class) @Serializable(with = FlexibleBooleanSerializer::class)
var byteRangeDownload: Boolean = true; 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) @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) @DropdownFieldOptionsId(R.array.thread_count)
var byteRangeConcurrency: Int = 3; var byteRangeConcurrency: Int = 3;
@ -599,11 +621,12 @@ class Settings : FragmentedStorageFileJson() {
@Serializable(with = FlexibleBooleanSerializer::class) @Serializable(with = FlexibleBooleanSerializer::class)
var keepScreenOn: Boolean = true; var keepScreenOn: Boolean = true;
@AdvancedField
@FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 3) @FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 3)
@Serializable(with = FlexibleBooleanSerializer::class) @Serializable(with = FlexibleBooleanSerializer::class)
var alwaysProxyRequests: Boolean = false; var alwaysProxyRequests: Boolean = false;
@AdvancedField
@FormField(R.string.allow_ipv6, FieldForm.TOGGLE, R.string.allow_ipv6_description, 4) @FormField(R.string.allow_ipv6, FieldForm.TOGGLE, R.string.allow_ipv6_description, 4)
@Serializable(with = FlexibleBooleanSerializer::class) @Serializable(with = FlexibleBooleanSerializer::class)
var allowIpv6: Boolean = true; var allowIpv6: Boolean = true;
@ -675,9 +698,11 @@ class Settings : FragmentedStorageFileJson() {
@Serializable @Serializable
class Plugins { class Plugins {
@AdvancedField
@FormField(R.string.check_disabled_plugin_updates, FieldForm.TOGGLE, R.string.check_disabled_plugin_updates_description, -1) @FormField(R.string.check_disabled_plugin_updates, FieldForm.TOGGLE, R.string.check_disabled_plugin_updates_description, -1)
var checkDisabledPluginsForUpdates: Boolean = false; var checkDisabledPluginsForUpdates: Boolean = false;
@AdvancedField
@FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0) @FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0)
var clearCookiesOnLogout: Boolean = true; var clearCookiesOnLogout: Boolean = true;
@ -896,8 +921,10 @@ class Settings : FragmentedStorageFileJson() {
var other = Other(); var other = Other();
@Serializable @Serializable
class Other { class Other {
@AdvancedField
@FormField(R.string.playlist_delete_confirmation, FieldForm.TOGGLE, R.string.playlist_delete_confirmation_description, 2) @FormField(R.string.playlist_delete_confirmation, FieldForm.TOGGLE, R.string.playlist_delete_confirmation_description, 2)
var playlistDeleteConfirmation: Boolean = true; var playlistDeleteConfirmation: Boolean = true;
@AdvancedField
@FormField(R.string.playlist_allow_dups, FieldForm.TOGGLE, R.string.playlist_allow_dups_description, 3) @FormField(R.string.playlist_allow_dups, FieldForm.TOGGLE, R.string.playlist_allow_dups_description, 3)
var playlistAllowDups: Boolean = true; var playlistAllowDups: Boolean = true;

View file

@ -423,17 +423,25 @@ class StatePlaylists {
class PlaylistBackup: ReconstructStore<Playlist>() { class PlaylistBackup: ReconstructStore<Playlist>() {
override fun toReconstruction(obj: Playlist): String { override fun toReconstruction(obj: Playlist): String {
val items = ArrayList<String>(); val items = ArrayList<String>();
items.add(obj.name); items.add(obj.name + ":::" + obj.id);
items.addAll(obj.videos.map { it.url }); items.addAll(obj.videos.map { it.url });
return items.map { it.replace("\n","") }.joinToString("\n"); return items.map { it.replace("\n","") }.joinToString("\n");
} }
override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder, importCache: ImportCache?): Playlist { override suspend fun toObject(id: String, backup: String, reconstructionBuilder: Builder, importCache: ImportCache?): Playlist {
var idToUse = id;
val items = backup.split("\n"); val items = backup.split("\n");
if(items.size <= 0) { if(items.size <= 0) {
throw IllegalStateException("Cannot reconstructor playlist ${id}"); 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 { val videos = items.drop(1).filter { it.isNotEmpty() }.map {
try { try {
val videoUrl = it; val videoUrl = it;
@ -465,7 +473,7 @@ class StatePlaylists {
throw ReconstructionException(name, "${name}:[${it}] ${ex.message}", ex); throw ReconstructionException(name, "${name}:[${it}] ${ex.message}", ex);
} }
}.filter { it != null }.map { it!! } }.filter { it != null }.map { it!! }
return Playlist(id, name, videos); return Playlist(idToUse, name, videos);
} }
} }
} }

View file

@ -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) { private fun handleData(session: SyncSession, opcode: UByte, subOpcode: UByte, data: ByteBuffer) {
val remotePublicKey = session.remotePublicKey val remotePublicKey = session.remotePublicKey
when (subOpcode) { when (subOpcode) {
@ -307,7 +312,9 @@ class StateSync {
data.get(dataBody); data.get(dataBody);
val json = String(dataBody, Charsets.UTF_8); val json = String(dataBody, Charsets.UTF_8);
val subPackage = Serializer.json.decodeFromString<SyncSubscriptionsPackage>(json); val subPackage = Serializer.json.decodeFromString<SyncSubscriptionsPackage>(json);
handleSyncSubscriptionPackage(session, subPackage); synchronized(_lockSubscriptions) {
handleSyncSubscriptionPackage(session, subPackage);
}
if(subPackage.subscriptions.size > 0) { if(subPackage.subscriptions.size > 0) {
val newestSub = subPackage.subscriptions.maxOf { it.creationTime }; val newestSub = subPackage.subscriptions.maxOf { it.creationTime };
@ -327,21 +334,23 @@ class StateSync {
val pack = Serializer.json.decodeFromString<SyncSubscriptionGroupsPackage>(json); val pack = Serializer.json.decodeFromString<SyncSubscriptionGroupsPackage>(json);
var lastSubgroupChange = OffsetDateTime.MIN; var lastSubgroupChange = OffsetDateTime.MIN;
for(group in pack.groups){ synchronized(_lockSubscriptionGroups) {
if(group.lastChange > lastSubgroupChange) for(group in pack.groups){
lastSubgroupChange = group.lastChange; if(group.lastChange > lastSubgroupChange)
lastSubgroupChange = group.lastChange;
val existing = StateSubscriptionGroups.instance.getSubscriptionGroup(group.id); val existing = StateSubscriptionGroups.instance.getSubscriptionGroup(group.id);
if(existing == null) if(existing == null)
StateSubscriptionGroups.instance.updateSubscriptionGroup(group, false, true); StateSubscriptionGroups.instance.updateSubscriptionGroup(group, false, true);
else if(existing.lastChange < group.lastChange) { else if(existing.lastChange < group.lastChange) {
existing.name = group.name; existing.name = group.name;
existing.urls = group.urls; existing.urls = group.urls;
existing.image = group.image; existing.image = group.image;
existing.priority = group.priority; existing.priority = group.priority;
existing.lastChange = group.lastChange; existing.lastChange = group.lastChange;
StateSubscriptionGroups.instance.updateSubscriptionGroup(existing, false, true); StateSubscriptionGroups.instance.updateSubscriptionGroup(existing, false, true);
}
} }
} }
for(removal in pack.groupRemovals) { for(removal in pack.groupRemovals) {
@ -358,18 +367,20 @@ class StateSync {
val json = String(dataBody, Charsets.UTF_8); val json = String(dataBody, Charsets.UTF_8);
val pack = Serializer.json.decodeFromString<SyncPlaylistsPackage>(json); val pack = Serializer.json.decodeFromString<SyncPlaylistsPackage>(json);
for(playlist in pack.playlists) { synchronized(_lockPlaylists) {
val existing = StatePlaylists.instance.getPlaylist(playlist.id); for(playlist in pack.playlists) {
val existing = StatePlaylists.instance.getPlaylist(playlist.id);
if(existing == null) if(existing == null)
StatePlaylists.instance.createOrUpdatePlaylist(playlist, false); StatePlaylists.instance.createOrUpdatePlaylist(playlist, false);
else if(existing.dateUpdate < playlist.dateUpdate) { else if(existing.dateUpdate < playlist.dateUpdate) {
existing.dateUpdate = playlist.dateUpdate; existing.dateUpdate = playlist.dateUpdate;
existing.name = playlist.name; existing.name = playlist.name;
existing.videos = playlist.videos; existing.videos = playlist.videos;
existing.dateCreation = playlist.dateCreation; existing.dateCreation = playlist.dateCreation;
existing.datePlayed = playlist.datePlayed; existing.datePlayed = playlist.datePlayed;
StatePlaylists.instance.createOrUpdatePlaylist(existing, false); StatePlaylists.instance.createOrUpdatePlaylist(existing, false);
}
} }
} }
for(removal in pack.playlistRemovals) { 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})"); Logger.i(TAG, "SyncWatchLater received ${pack.videos.size} (${pack.videoAdds?.size}, ${pack.videoRemovals?.size})");
val allExisting = StatePlaylists.instance.getWatchLater(); val allExisting = StatePlaylists.instance.getWatchLater();
for(video in pack.videos) { synchronized(_lockWatchlater) {
val existing = allExisting.firstOrNull { it.url == video.url }; for(video in pack.videos) {
val time = if(pack.videoAdds != null && pack.videoAdds.containsKey(video.url)) (pack.videoAdds[video.url] ?: 0).sToOffsetDateTimeUTC() else OffsetDateTime.MIN; val existing = allExisting.firstOrNull { it.url == video.url };
val removalTime = StatePlaylists.instance.getWatchLaterRemovalTime(video.url) ?: OffsetDateTime.MIN; val time = if(pack.videoAdds != null && pack.videoAdds.containsKey(video.url)) (pack.videoAdds[video.url] ?: 0).sToOffsetDateTimeUTC() else OffsetDateTime.MIN;
if(existing == null && time > removalTime) { val removalTime = StatePlaylists.instance.getWatchLaterRemovalTime(video.url) ?: OffsetDateTime.MIN;
StatePlaylists.instance.addToWatchLater(video, false); if(existing == null && time > removalTime) {
if(time > OffsetDateTime.MIN) StatePlaylists.instance.addToWatchLater(video, false);
StatePlaylists.instance.setWatchLaterAddTime(video.url, time); if(time > OffsetDateTime.MIN)
StatePlaylists.instance.setWatchLaterAddTime(video.url, time);
}
} }
} }
for(removal in pack.videoRemovals) { for(removal in pack.videoRemovals) {

View file

@ -41,6 +41,8 @@ class ButtonField : BigButton, IField {
return null; return null;
}; };
override var isAdvanced: Boolean = false;
//private val _title : TextView; //private val _title : TextView;
//private val _subtitle : TextView; //private val _subtitle : TextView;
@ -89,7 +91,7 @@ class ButtonField : BigButton, IField {
return this; 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"); throw IllegalStateException("ButtonField should only be used for methods");
} }
override fun setField() { override fun setField() {

View file

@ -40,6 +40,8 @@ class DropdownField : TableRow, IField {
override var reference: Any? = null; override var reference: Any? = null;
override var isAdvanced: Boolean = false;
override val onChanged = Event3<IField, Any, Any>(); override val onChanged = Event3<IField, Any, Any>();
override val value: Any? get() = _selected; override val value: Any? get() = _selected;
@ -112,7 +114,7 @@ class DropdownField : TableRow, IField {
return this; 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._field = field;
this._obj = obj; this._obj = obj;
@ -133,6 +135,9 @@ class DropdownField : TableRow, IField {
_description.visibility = View.GONE; _description.visibility = View.GONE;
} }
val advancedFieldAttr = field.getAnnotation(AdvancedField::class.java)
if(advancedFieldAttr != null || advanced)
isAdvanced = true;
_options = (field.getAnnotation(DropdownFieldOptions::class.java)?.options ?: _options = (field.getAnnotation(DropdownFieldOptions::class.java)?.options ?:
field.getAnnotation(DropdownFieldOptionsId::class.java)?.optionsId?.let { resources.getStringArray(it) } ?: field.getAnnotation(DropdownFieldOptionsId::class.java)?.optionsId?.let { resources.getStringArray(it) } ?:

View file

@ -4,6 +4,10 @@ import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.constructs.Event3 import com.futo.platformplayer.constructs.Event3
import java.lang.reflect.Field import java.lang.reflect.Field
@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class AdvancedField();
@Target(AnnotationTarget.FIELD, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY) @Target(AnnotationTarget.FIELD, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
@ -22,6 +26,8 @@ interface IField {
val obj : Any?; val obj : Any?;
val field : Field?; val field : Field?;
val isAdvanced: Boolean;
val value: Any?; val value: Any?;
val onChanged : Event3<IField, Any, Any>; val onChanged : Event3<IField, Any, Any>;
@ -29,7 +35,7 @@ interface IField {
val searchContent: String?; 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 setField();
fun setValue(value: Any); fun setValue(value: Any);

View file

@ -37,6 +37,8 @@ class FieldForm : LinearLayout {
private var _fields : List<IField> = arrayListOf(); private var _fields : List<IField> = arrayListOf();
private var _showAdvancedSettings: Boolean = false;
constructor(context : Context, attrs : AttributeSet? = null) : super(context, attrs) { constructor(context : Context, attrs : AttributeSet? = null) : super(context, attrs) {
inflate(context, R.layout.field_form, this); inflate(context, R.layout.field_form, this);
_containerSearch = findViewById(R.id.container_search); _containerSearch = findViewById(R.id.container_search);
@ -58,11 +60,17 @@ class FieldForm : LinearLayout {
if(field is GroupField) { if(field is GroupField) {
updateSettingsVisibility(field); updateSettingsVisibility(field);
} else if(field is View && field.descriptor != null) { } else if(field is View && field.descriptor != null) {
val txt = field.searchContent?.lowercase(); if(field.isAdvanced && !_showAdvancedSettings)
if(txt != null) { {
val visible = isGroupMatch || txt.contains(query); field.visibility = View.GONE;
field.visibility = if (visible) View.VISIBLE else View.GONE; }
groupVisible = groupVisible || visible; 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) { fun setSearchQuery(query: String) {
_editSearch.setText(query); _editSearch.setText(query);
updateSettingsVisibility(); updateSettingsVisibility();
@ -92,13 +104,22 @@ class FieldForm : LinearLayout {
throw java.lang.IllegalStateException("Only views can be IFields"); 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); _fieldsContainer.addView(field as View);
field.onChanged.subscribe { a1, a2, _ -> field.onChanged.subscribe { a1, a2, _ ->
if(field is ToggleField && field.descriptor?.id == "advancedSettings") {
setShowAdvancedSettings((a2 as Boolean));
}
onChanged.emit(a1, a2); onChanged.emit(a1, a2);
}; };
} }
_fields = newFields; _fields = newFields;
updateSettingsVisibility();
onLoaded?.invoke(); onLoaded?.invoke();
} }
} }
@ -267,10 +288,12 @@ class FieldForm : LinearLayout {
for(prop in objFields) { for(prop in objFields) {
prop.first.javaField!!.isAccessible = true; prop.first.javaField!!.isAccessible = true;
val advanced = prop.first.hasAnnotation<AdvancedField>();
val field = when(prop.second.type) { val field = when(prop.second.type) {
GROUP -> GroupField(context).fromField(obj, prop.first.javaField!!, prop.second); GROUP -> GroupField(context).fromField(obj, prop.first.javaField!!, prop.second);
DROPDOWN -> DropdownField(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); TOGGLE -> ToggleField(context).fromField(obj, prop.first.javaField!!, prop.second, advanced);
READONLYTEXT -> ReadOnlyTextField(context).fromField(obj, prop.first.javaField!!, prop.second); 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}") else -> throw java.lang.IllegalStateException("Unknown field type ${prop.second.type} for ${prop.second.title}")
} }

View file

@ -34,6 +34,7 @@ class GroupField : LinearLayout, IField {
private val _container : LinearLayout; private val _container : LinearLayout;
override var reference: Any? = null; override var reference: Any? = null;
override var isAdvanced: Boolean = false;
override val value: Any? = null; override val value: Any? = null;
@ -100,7 +101,7 @@ class GroupField : LinearLayout, IField {
return this; 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._field = field;
this._obj = obj; this._obj = obj;

View file

@ -31,6 +31,7 @@ class ReadOnlyTextField : TableRow, IField {
override val onChanged = Event3<IField, Any, Any>(); override val onChanged = Event3<IField, Any, Any>();
override var reference: Any? = null; override var reference: Any? = null;
override var isAdvanced: Boolean = false;
override val value: Any? = null; override val value: Any? = null;
@ -45,7 +46,7 @@ class ReadOnlyTextField : TableRow, IField {
override fun setValue(value: Any) {} 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._field = field;
this._obj = obj; this._obj = obj;

View file

@ -33,6 +33,7 @@ class ToggleField : TableRow, IField {
private var _lastValue: Boolean = false; private var _lastValue: Boolean = false;
override var reference: Any? = null; override var reference: Any? = null;
override var isAdvanced: Boolean = false;
override val onChanged = Event3<IField, Any, Any>(); override val onChanged = Event3<IField, Any, Any>();
@ -75,7 +76,7 @@ class ToggleField : TableRow, IField {
return this; 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._field = field;
this._obj = obj; this._obj = obj;
@ -87,6 +88,12 @@ class ToggleField : TableRow, IField {
else else
_title.text = field.name; _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) if(attrField == null || attrField.subtitle == -1)
_description.visibility = View.GONE; _description.visibility = View.GONE;
else { else {

View file

@ -12,6 +12,8 @@
<string name="channel">Channel</string> <string name="channel">Channel</string>
<string name="home">Home</string> <string name="home">Home</string>
<string name="progress_bar">Progress Bar</string> <string name="progress_bar">Progress Bar</string>
<string name="advanced_settings">Advanced Settings</string>
<string name="advanced_settings_description">If advanced settings should be shown, this exposes additional settings to finetune your experience.</string>
<string name="progress_bar_description">If a historical progress bar should be shown</string> <string name="progress_bar_description">If a historical progress bar should be shown</string>
<string name="hide_hidden_from_search">Hide hidden from home in search</string> <string name="hide_hidden_from_search">Hide hidden from home in search</string>
<string name="hide_hidden_from_search_description">Hide videos and creators hidden from home also in search results</string> <string name="hide_hidden_from_search_description">Hide videos and creators hidden from home also in search results</string>