Skip to content
Snippets Groups Projects
Settings.kt 41.1 KiB
Newer Older
Koen's avatar
Koen committed
package com.futo.platformplayer

import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.webkit.CookieManager
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.activities.ManageTabsActivity
import com.futo.platformplayer.activities.PolycentricHomeActivity
import com.futo.platformplayer.activities.PolycentricProfileActivity
import com.futo.platformplayer.activities.SettingsActivity
Koen's avatar
Koen committed
import com.futo.platformplayer.activities.SyncHomeActivity
Koen's avatar
Koen committed
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.serializers.FlexibleBooleanSerializer
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateBackup
import com.futo.platformplayer.states.StateCache
import com.futo.platformplayer.states.StateMeta
import com.futo.platformplayer.states.StatePayment
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.states.StateUpdate
Koen's avatar
Koen committed
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.FragmentedStorageFileJson
import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.fields.DropdownFieldOptionsId
import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.FormField
import com.futo.platformplayer.views.fields.FormFieldButton
Kelvin's avatar
Kelvin committed
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
Koen's avatar
Koen committed
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
Koen's avatar
Koen committed
import java.io.File
import java.time.OffsetDateTime

Koen's avatar
Koen committed
@Serializable
data class MenuBottomBarSetting(val id: Int, var enabled: Boolean);

@Serializable()
class Settings : FragmentedStorageFileJson() {
    var didFirstStart: Boolean = false;

    @Serializable
    val tabs: MutableList<MenuBottomBarSetting> = MenuBottomBarFragment.buttonDefinitions.map { MenuBottomBarSetting(it.id, true) }.toMutableList()

    @Transient
    val onTabsChanged = Event0();

Koen's avatar
Koen committed
    @FormField(R.string.sync_grayjay, FieldForm.BUTTON, R.string.sync_grayjay_description, -8)
    @FormFieldButton(R.drawable.ic_update)
    fun syncGrayjay() {
        SettingsActivity.getActivity()?.let {
            it.startActivity(Intent(it, SyncHomeActivity::class.java))
        }
    }


    @FormField(R.string.manage_polycentric_identity, FieldForm.BUTTON, R.string.manage_your_polycentric_identity, -7)
    @FormFieldButton(R.drawable.ic_person)
Koen's avatar
Koen committed
    fun managePolycentricIdentity() {
        SettingsActivity.getActivity()?.let {
            if (StatePolycentric.instance.enabled) {
                if (StatePolycentric.instance.processHandle != null) {
                    it.startActivity(Intent(it, PolycentricProfileActivity::class.java));
                } else {
                    it.startActivity(Intent(it, PolycentricHomeActivity::class.java));
                }
Koen's avatar
Koen committed
            } else {
                UIDialogs.toast(it, "Polycentric is disabled")
    @FormField(R.string.show_faq, FieldForm.BUTTON, R.string.get_answers_to_common_questions, -6)
    @FormFieldButton(R.drawable.ic_quiz)
    fun openFAQ() {
        try {
            val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(Settings.URL_FAQ))
            SettingsActivity.getActivity()?.startActivity(browserIntent);
        } catch (e: Throwable) {
            //Ignored
        }
    }
    @FormField(R.string.show_issues, FieldForm.BUTTON, R.string.a_list_of_user_reported_and_self_reported_issues, -5)
    @FormFieldButton(R.drawable.ic_data_alert)
    fun openIssues() {
        try {
            val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/futo-org/grayjay-android/issues"))
            SettingsActivity.getActivity()?.startActivity(browserIntent);
        } catch (e: Throwable) {
            //Ignored
        }
    }

Koen's avatar
Koen committed
    @FormField(
        R.string.submit_feedback, FieldForm.BUTTON,
        R.string.give_feedback_on_the_application, -1
Koen's avatar
Koen committed
    )
    @FormFieldButton(R.drawable.ic_bug)
Koen's avatar
Koen committed
    fun submitFeedback() {
        try {
            val i = Intent(Intent.ACTION_VIEW);
            val subject = "Feedback Grayjay";
            val body = "Hey,\n\nI have some feedback on the Grayjay app.\nVersion information (version_name = ${BuildConfig.VERSION_NAME}, version_code = ${BuildConfig.VERSION_CODE}, flavor = ${BuildConfig.FLAVOR}, build_type = ${BuildConfig.BUILD_TYPE}})\n" +
                    "Device information (brand= ${Build.BRAND}, manufacturer = ${Build.MANUFACTURER}, device = ${Build.DEVICE}, version-sdk = ${Build.VERSION.SDK_INT}, version-os = ${Build.VERSION.BASE_OS})\n\n";
Koen's avatar
Koen committed
            val data = Uri.parse("mailto:grayjay@futo.org?subject=" + Uri.encode(subject) + "&body=" + Uri.encode(body));
            i.data = data;

            StateApp.withContext { it.startActivity(i); };
        } catch (e: Throwable) {
            //Ignored
        }
Koen's avatar
Koen committed

    @FormField(R.string.manage_tabs, FieldForm.BUTTON, R.string.change_tabs_visible_on_the_home_screen, -4)
    @FormFieldButton(R.drawable.ic_tabs)
Koen's avatar
Koen committed
    fun manageTabs() {
        try {
            SettingsActivity.getActivity()?.let {
                it.startActivity(Intent(it, ManageTabsActivity::class.java));
            }
        } catch (e: Throwable) {
            //Ignored
        }
    }

    @FormField(R.string.import_data, FieldForm.BUTTON, R.string.import_data_description, -3)
Kelvin's avatar
Kelvin committed
    @FormFieldButton(R.drawable.ic_move_up)
    fun import() {
        val act = SettingsActivity.getActivity() ?: return;
        val intent = MainActivity.getImportOptionsIntent(act);
        act.startActivity(intent);
    }

    @FormField(R.string.link_handling, FieldForm.BUTTON, R.string.allow_grayjay_to_handle_links, -2)
    @FormFieldButton(R.drawable.ic_link)
    fun manageLinks() {
        try {
            SettingsActivity.getActivity()?.let { UIDialogs.showUrlHandlingPrompt(it) }
        } catch (e: Throwable) {
            Logger.e(TAG, "Failed to show url handling prompt", e)
        }
    }
    /*@FormField(R.string.disable_battery_optimization, FieldForm.BUTTON, R.string.click_to_go_to_battery_optimization_settings_disabling_battery_optimization_will_prevent_the_os_from_killing_media_sessions, -1)
    @FormFieldButton(R.drawable.battery_full_24px)
    fun ignoreBatteryOptimization() {
        SettingsActivity.getActivity()?.let {
            val intent = Intent()
            val packageName = it.packageName
            val pm = it.getSystemService(POWER_SERVICE) as PowerManager;
            if (!pm.isIgnoringBatteryOptimizations(packageName)) {
                intent.setAction(android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
                intent.setData(Uri.parse("package:$packageName"))
                it.startActivity(intent)
                UIDialogs.toast(it, "Please ignore battery optimizations for Grayjay")
            } else {
                UIDialogs.toast(it, "Battery optimizations already disabled for Grayjay")
            }
        }
    }*/

    @FormField(R.string.language, "group", -1, 0)
    var language = LanguageSettings();
    @Serializable
    class LanguageSettings {
        @FormField(R.string.app_language, FieldForm.DROPDOWN, R.string.may_require_restart, 5, "app_language")
        @DropdownFieldOptionsId(R.array.app_languages)
        var appLanguage: Int = 0;

        fun getAppLanguageLocaleString(): String? {
            return when(appLanguage) {
                0 -> null
                1 -> "en";
                2 -> "de";
                3 -> "es";
                4 -> "pt";
                5 -> "fr"
                6 -> "ja";
                7 -> "ko";
                8 -> "zh";
                9 -> "ru";
                10 -> "ar";
                else -> null
            }
        }
    }

    @FormField(R.string.home, "group", R.string.configure_how_your_home_tab_works_and_feels, 1)
Koen's avatar
Koen committed
    var home = HomeSettings();
    @Serializable
    class HomeSettings {
        @FormField(R.string.feed_style, FieldForm.DROPDOWN, R.string.may_require_restart, 5)
Koen's avatar
Koen committed
        @DropdownFieldOptionsId(R.array.feed_style)
        var homeFeedStyle: Int = 1;

        fun getHomeFeedStyle(): FeedStyle {
            if(homeFeedStyle == 0)
                return FeedStyle.PREVIEW;
            else
                return FeedStyle.THUMBNAIL;
        }

        @FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
        var previewFeedItems: Boolean = true;
        @FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)


        @FormField(R.string.clear_hidden, FieldForm.BUTTON, R.string.clear_hidden_description, 8)
        @FormFieldButton(R.drawable.ic_visibility_off)
        fun clearHidden() {
            StateMeta.instance.removeAllHiddenCreators();
            StateMeta.instance.removeAllHiddenVideos();
            SettingsActivity.getActivity()?.let {
                UIDialogs.toast(it, "Creators and videos should show up again");
            }
        }
Koen's avatar
Koen committed
    }

    @FormField(R.string.search, "group", -1, 2)
Koen's avatar
Koen committed
    var search = SearchSettings();
    @Serializable
    class SearchSettings {
        @FormField(R.string.search_history, FieldForm.TOGGLE, R.string.may_require_restart, 3)
Koen's avatar
Koen committed
        @Serializable(with = FlexibleBooleanSerializer::class)
        var searchHistory: Boolean = true;


        @FormField(R.string.feed_style, FieldForm.DROPDOWN, -1, 4)
Koen's avatar
Koen committed
        @DropdownFieldOptionsId(R.array.feed_style)
        var searchFeedStyle: Int = 1;

        @FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 5)
        var previewFeedItems: Boolean = true;

        @FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
Koen's avatar
Koen committed

        fun getSearchFeedStyle(): FeedStyle {
            if(searchFeedStyle == 0)
                return FeedStyle.PREVIEW;
            else
                return FeedStyle.THUMBNAIL;
        }
    }


    @FormField(R.string.channel, "group", -1, 3)
    var channel = ChannelSettings();
    @Serializable
    class ChannelSettings {

        @FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
    }

    @FormField(R.string.subscriptions, "group", R.string.configure_how_your_subscriptions_works_and_feels, 4)
Koen's avatar
Koen committed
    var subscriptions = SubscriptionsSettings();
    @Serializable
    class SubscriptionsSettings {
        @FormField(R.string.feed_style, FieldForm.DROPDOWN, R.string.may_require_restart, 4)
Koen's avatar
Koen committed
        @DropdownFieldOptionsId(R.array.feed_style)
        var subscriptionsFeedStyle: Int = 1;

        fun getSubscriptionsFeedStyle(): FeedStyle {
            if(subscriptionsFeedStyle == 0)
                return FeedStyle.PREVIEW;
            else
                return FeedStyle.THUMBNAIL;
        }

        @FormField(R.string.show_subscription_group, FieldForm.TOGGLE, R.string.show_subscription_group_description, 5)
        var showSubscriptionGroups: Boolean = true;

        @FormField(R.string.preview_feed_items, FieldForm.TOGGLE, R.string.preview_feed_items_description, 6)
        @FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 7)
        @FormField(R.string.fetch_on_app_boot, FieldForm.TOGGLE, R.string.shortly_after_opening_the_app_start_fetching_subscriptions, 8)
Kelvin's avatar
Kelvin committed
        @Serializable(with = FlexibleBooleanSerializer::class)
        var fetchOnAppBoot: Boolean = true;

        @FormField(R.string.fetch_on_tab_opened, FieldForm.TOGGLE, R.string.fetch_on_tab_opened_description, 9)
        var fetchOnTabOpen: Boolean = true;

        @FormField(R.string.background_update, FieldForm.DROPDOWN, R.string.experimental_background_update_for_subscriptions_cache, 10, "background_update")
Koen's avatar
Koen committed
        @DropdownFieldOptionsId(R.array.background_interval)
        var subscriptionsBackgroundUpdateInterval: Int = 0;

        fun getSubscriptionsBackgroundIntervalMinutes(): Int = when(subscriptionsBackgroundUpdateInterval) {
            0 -> 0;
            1 -> 15;
            2 -> 60;
            3 -> 60 * 3;
            4 -> 60 * 6;
            5 -> 60 * 12;
            6 -> 60 * 24;
            else -> 0
        };


        @FormField(R.string.subscription_concurrency, FieldForm.DROPDOWN, R.string.specify_how_many_threads_are_used_to_fetch_channels, 11)
Koen's avatar
Koen committed
        @DropdownFieldOptionsId(R.array.thread_count)
        var subscriptionConcurrency: Int = 3;

        fun getSubscriptionsConcurrency() : Int {
            return threadIndexToCount(subscriptionConcurrency);
        }
        @FormField(R.string.show_watch_metrics, FieldForm.TOGGLE, R.string.show_watch_metrics_description, 12)
        var showWatchMetrics: Boolean = false;

        @FormField(R.string.track_playtime_locally, FieldForm.TOGGLE, R.string.track_playtime_locally_description, 13)
        var allowPlaytimeTracking: Boolean = true;
        @FormField(R.string.always_reload_from_cache, FieldForm.TOGGLE, R.string.always_reload_from_cache_description, 14)
        var alwaysReloadFromCache: Boolean = false;
        @FormField(R.string.peek_channel_contents, FieldForm.TOGGLE, R.string.peek_channel_contents_description, 15)
        var peekChannelContents: Boolean = false;

        @FormField(R.string.clear_channel_cache, FieldForm.BUTTON, R.string.clear_channel_cache_description, 16)
        fun clearChannelCache() {
            UIDialogs.toast(SettingsActivity.getActivity()!!, "Started clearing..");
Kelvin's avatar
Kelvin committed
            StateCache.instance.clear();
            UIDialogs.toast(SettingsActivity.getActivity()!!, "Finished clearing");
        }
Koen's avatar
Koen committed
    }

    @FormField(R.string.player, "group", R.string.change_behavior_of_the_player, 5)
Koen's avatar
Koen committed
    var playback = PlaybackSettings();
    @Serializable
    class PlaybackSettings {
        @FormField(R.string.primary_language, FieldForm.DROPDOWN, -1, -1)
        @DropdownFieldOptionsId(R.array.audio_languages)
Koen's avatar
Koen committed
        var primaryLanguage: Int = 0;

        fun getPrimaryLanguage(context: Context): String? {
            return when(primaryLanguage) {
                0 -> "en";
                1 -> "es";
                2 -> "de";
                3 -> "fr";
                4 -> "ja";
                5 -> "ko";
                6 -> "th";
                7 -> "vi";
                8 -> "id";
                9 -> "hi";
                10 -> "ar";
                11 -> "tu";
                12 -> "ru";
                13 -> "pt";
                14 -> "zh";
                else -> null
            }
        }

        //= context.resources.getStringArray(R.array.audio_languages)[primaryLanguage];
Koen's avatar
Koen committed

        @FormField(R.string.default_playback_speed, FieldForm.DROPDOWN, -1, 0)
Koen's avatar
Koen committed
        @DropdownFieldOptionsId(R.array.playback_speeds)
        var defaultPlaybackSpeed: Int = 3;
        fun getDefaultPlaybackSpeed(): Float = when(defaultPlaybackSpeed) {
            0 -> 0.25f;
            1 -> 0.5f;
            2 -> 0.75f;
            3 -> 1.0f;
            4 -> 1.25f;
            5 -> 1.5f;
            6 -> 1.75f;
            7 -> 2.0f;
            8 -> 2.25f;
            else -> 1.0f;
        };

        @FormField(R.string.preferred_quality, FieldForm.DROPDOWN, R.string.preferred_quality_description, 1)
Koen's avatar
Koen committed
        @DropdownFieldOptionsId(R.array.preferred_quality_array)
        var preferredQuality: Int = 0;

        @FormField(R.string.preferred_metered_quality, FieldForm.DROPDOWN, R.string.preferred_metered_quality_description, 2)
Koen's avatar
Koen committed
        @DropdownFieldOptionsId(R.array.preferred_quality_array)
        var preferredMeteredQuality: Int = 0;
        fun getPreferredQualityPixelCount(): Int = preferedQualityToPixels(preferredQuality);
        fun getPreferredMeteredQualityPixelCount(): Int = preferedQualityToPixels(preferredMeteredQuality);
        fun getCurrentPreferredQualityPixelCount(): Int = if(!StateApp.instance.isCurrentMetered()) getPreferredQualityPixelCount() else getPreferredMeteredQualityPixelCount();

        @FormField(R.string.preferred_preview_quality, FieldForm.DROPDOWN, R.string.preferred_preview_quality_description, 3)
Koen's avatar
Koen committed
        @DropdownFieldOptionsId(R.array.preferred_quality_array)
        var preferredPreviewQuality: Int = 5;
        fun getPreferredPreviewQualityPixelCount(): Int = preferedQualityToPixels(preferredPreviewQuality);


        @FormField(R.string.simplify_sources, FieldForm.TOGGLE, R.string.simplify_sources_description, 4)
        var simplifySources: Boolean = true;

        @FormField(R.string.auto_rotate, FieldForm.DROPDOWN, -1, 5)
Koen's avatar
Koen committed
        @DropdownFieldOptionsId(R.array.system_enabled_disabled_array)
        var autoRotate: Int = 2;

        @FormField(R.string.background_behavior, FieldForm.DROPDOWN, -1, 7)
Koen's avatar
Koen committed
        @DropdownFieldOptionsId(R.array.player_background_behavior)
        var backgroundPlay: Int = 2;

        fun isBackgroundContinue() = backgroundPlay == 1;
        fun isBackgroundPictureInPicture() = backgroundPlay == 2;

        @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)
Koen's avatar
Koen committed
        @DropdownFieldOptionsId(R.array.resume_after_preview)
        var resumeAfterPreview: Int = 1;

        fun shouldResumePreview(previewedPosition: Long): Boolean{
            if(resumeAfterPreview == 2)
                return true;
            if(resumeAfterPreview == 1 && previewedPosition > 10)
                return true;
            return false;
        }
        @FormField(R.string.chapter_update_fps_title, FieldForm.DROPDOWN, R.string.chapter_update_fps_description, 8)
        @DropdownFieldOptionsId(R.array.chapter_fps)
        var chapterUpdateFPS: Int = 0;

        fun getChapterUpdateFrames(): Int {
            return when(chapterUpdateFPS) {
                0 -> 24
                1 -> 30
                2 -> 60
                3 -> 120
                else -> 1
            };
        }

        @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.background_switch_audio, FieldForm.TOGGLE, R.string.background_switch_audio_description, 10)

        @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;
        @FormField(R.string.restart_after_connectivity_loss, FieldForm.DROPDOWN, R.string.restart_playback_when_gaining_connectivity_after_a_loss, 12)
        @DropdownFieldOptionsId(R.array.restart_playback_after_loss)
        var restartPlaybackAfterConnectivityLoss: Int = 1;

        @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, 18)
        var preferWebmVideo: Boolean = false;
        @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, 20)
        var allowVideoToGoUnderCutout: Boolean = true;
Koen J's avatar
Koen J committed

        @FormField(R.string.autoplay, FieldForm.TOGGLE, R.string.autoplay, 21)
Koen J's avatar
Koen J committed
        var autoplay: Boolean = false;

        @FormField(R.string.delete_watchlist_on_finish, FieldForm.TOGGLE, R.string.delete_watchlist_on_finish_description, 22)
        var deleteFromWatchLaterAuto: Boolean = true;
Koen's avatar
Koen committed
    }

    @FormField(R.string.comments, "group", R.string.comments_description, 6)
Kelvin's avatar
Kelvin committed
    var comments = CommentSettings();
    @Serializable
    class CommentSettings {
Kelvin's avatar
Kelvin committed
        var didAskPolycentricDefault: Boolean = false;

Kelvin's avatar
Kelvin committed
        @FormField(R.string.default_comment_section, FieldForm.DROPDOWN, -1, 0)
        @DropdownFieldOptionsId(R.array.comment_sections)
        var defaultCommentSection: Int = 2;
Koen J's avatar
Koen J committed
        @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;
    @FormField(R.string.downloads, "group", R.string.configure_downloading_of_videos, 7)
Koen's avatar
Koen committed
    var downloads = Downloads();
    @Serializable
    class Downloads {

        @FormField(R.string.download_when, FieldForm.DROPDOWN, R.string.configure_when_videos_should_be_downloaded, 0)
Koen's avatar
Koen committed
        @DropdownFieldOptionsId(R.array.when_download)
        var whenDownload: Int = 0;

        fun shouldDownload(): Boolean {
            return when (whenDownload) {
                0 -> !StateApp.instance.isCurrentMetered();
                1 -> StateApp.instance.isNetworkState(StateApp.NetworkState.WIFI, StateApp.NetworkState.ETHERNET);
                2 -> true;
                else -> false;
            }
        }

        @FormField(R.string.default_video_quality, FieldForm.DROPDOWN, -1, 2)
Koen's avatar
Koen committed
        @DropdownFieldOptionsId(R.array.preferred_video_download)
        var preferredVideoQuality: Int = 4;
        fun getDefaultVideoQualityPixels(): Int = preferedQualityToPixels(preferredVideoQuality);

        @FormField(R.string.default_audio_quality, FieldForm.DROPDOWN, -1, 3)
Koen's avatar
Koen committed
        @DropdownFieldOptionsId(R.array.preferred_audio_download)
        var preferredAudioQuality: Int = 1;
        fun isHighBitrateDefault(): Boolean = preferredAudioQuality > 0;

        @FormField(R.string.byte_range_download, FieldForm.TOGGLE, R.string.attempt_to_utilize_byte_ranges, 4)
Koen's avatar
Koen committed
        @Serializable(with = FlexibleBooleanSerializer::class)
        var byteRangeDownload: Boolean = true;

        @FormField(R.string.byte_range_concurrency, FieldForm.DROPDOWN, R.string.number_of_concurrent_threads_to_multiply_download_speeds_from_throttled_sources, 5)
Koen's avatar
Koen committed
        @DropdownFieldOptionsId(R.array.thread_count)
        var byteRangeConcurrency: Int = 3;
        fun getByteRangeThreadCount(): Int {
            return threadIndexToCount(byteRangeConcurrency);
        }
    }

    @FormField(R.string.browsing, "group", R.string.configure_browsing_behavior, 8)
Koen's avatar
Koen committed
    var browsing = Browsing();
    @Serializable
    class Browsing {
        @FormField(R.string.enable_video_cache, FieldForm.TOGGLE, R.string.cache_to_quickly_load_previously_fetched_videos, 0)
Koen's avatar
Koen committed
        @Serializable(with = FlexibleBooleanSerializer::class)
Kelvin's avatar
Kelvin committed
        var videoCache: Boolean = false; //Temporary default disabled to prevent ui freeze?
Koen's avatar
Koen committed
    }

    @FormField(R.string.casting, "group", R.string.configure_casting, 9)
Koen's avatar
Koen committed
    var casting = Casting();
    @Serializable
    class Casting {
        @FormField(R.string.enabled, FieldForm.TOGGLE, R.string.enable_casting, 0)
Koen's avatar
Koen committed
        @Serializable(with = FlexibleBooleanSerializer::class)
        var enabled: Boolean = true;

Koen's avatar
Koen committed
        @FormField(R.string.keep_screen_on, FieldForm.TOGGLE, R.string.keep_screen_on_while_casting, 1)
        @Serializable(with = FlexibleBooleanSerializer::class)
        var keepScreenOn: Boolean = true;
Koen's avatar
Koen committed

        @FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 1)
        @Serializable(with = FlexibleBooleanSerializer::class)
        var alwaysProxyRequests: Boolean = false;

Koen's avatar
Koen committed
        /*TODO: Should we have a different casting quality?
        @FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
        @DropdownFieldOptionsId(R.array.preferred_quality_array)
        var preferredQuality: Int = 4;
        fun getPreferredQualityPixelCount(): Int {
            when (preferredQuality) {
                0 -> return 1280 * 720;
                1 -> return 3840 * 2160;
                2 -> return 1920 * 1080;
                3 -> return 1280 * 720;
                4 -> return 640 * 480;
                else -> return 0;
            }
        }*/
    }

    @FormField(R.string.logging, FieldForm.GROUP, -1, 10)
Koen's avatar
Koen committed
    var logging = Logging();
    @Serializable
    class Logging {
        @FormField(R.string.log_level, FieldForm.DROPDOWN, -1, 0)
Koen's avatar
Koen committed
        @DropdownFieldOptionsId(R.array.log_levels)
        var logLevel: Int = 0;

        fun isVerbose() = logLevel >= 4;

        @FormField(R.string.submit_logs, FieldForm.BUTTON, R.string.submit_logs_to_help_us_narrow_down_issues, 1)
Koen's avatar
Koen committed
        fun submitLogs() {
            StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
Koen's avatar
Koen committed
                try {
                    if (!Logger.submitLogs()) {
                        withContext(Dispatchers.Main) {
                            SettingsActivity.getActivity()?.let { UIDialogs.toast(it, it.getString(R.string.please_enable_logging_to_submit_logs)) }
Koen's avatar
Koen committed
                        }
                    }
                } catch (e: Throwable) {
                    Logger.e("Settings", "Failed to submit logs.", e);
                }
            }
        }
    }

    @FormField(R.string.announcement, FieldForm.GROUP, -1, 11)
Koen's avatar
Koen committed
    var announcementSettings = AnnouncementSettings();
    @Serializable
    class AnnouncementSettings {
        @FormField(R.string.reset_announcements, FieldForm.BUTTON, R.string.reset_hidden_announcements, 1)
Koen's avatar
Koen committed
        fun resetAnnouncements() {
            StateAnnouncement.instance.resetAnnouncements();
            SettingsActivity.getActivity()?.let { UIDialogs.toast(it, it.getString(R.string.announcements_reset)); };
    @FormField(R.string.notifications, FieldForm.GROUP, -1, 12)
    var notifications = NotificationSettings();
    @Serializable
    class NotificationSettings {
        @FormField(R.string.planned_content_notifications, FieldForm.TOGGLE, R.string.planned_content_notifications_description, 1)
        var plannedContentNotification: Boolean = true;
    }

    @FormField(R.string.plugins, FieldForm.GROUP, -1, 13)
Koen's avatar
Koen committed
    @Transient
    var plugins = Plugins();
    @Serializable
    class Plugins {

        @FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0)
Koen's avatar
Koen committed
        var clearCookiesOnLogout: Boolean = true;

        @FormField(R.string.clear_cookies, FieldForm.BUTTON, R.string.clears_in_app_browser_cookies, 1)
Koen's avatar
Koen committed
        fun clearCookies() {
            val cookieManager: CookieManager = CookieManager.getInstance();
            cookieManager.removeAllCookies(null);
        }
Koen's avatar
Koen committed
        /*@FormField(R.string.reinstall_embedded_plugins, FieldForm.BUTTON, R.string.also_removes_any_data_related_plugin_like_login_or_settings, 1)
Koen's avatar
Koen committed
        fun reinstallEmbedded() {
            StateApp.instance.scopeOrNull!!.launch(Dispatchers.IO) {
                try {
                    StatePlugins.instance.reinstallEmbeddedPlugins(StateApp.instance.context);

                    withContext(Dispatchers.Main) {
                        StateApp.instance.contextOrNull?.let {
                            UIDialogs.toast(it, it.getString(R.string.embedded_plugins_reinstalled_a_reboot_is_recommended));
Koen's avatar
Koen committed
                        };
                    }
                } catch (ex: Exception) {
                    withContext(Dispatchers.Main) {
                        StateApp.withContext {
                            UIDialogs.toast(it, "Failed: " + ex.message);
                        };
                    }
                }
            }
Koen's avatar
Koen committed
        }*/
    @FormField(R.string.external_storage, FieldForm.GROUP, -1, 14)
    var storage = Storage();
    @Serializable
    class Storage {
        var storage_general: String? = null;
        var storage_download: String? = null;

        fun getStorageGeneralUri(): Uri? = storage_general?.let { Uri.parse(it) };
        fun getStorageDownloadUri(): Uri? = storage_download?.let { Uri.parse(it) };
        fun isStorageMainValid(context: Context): Boolean = StateApp.instance.isValidStorageUri(context, getStorageGeneralUri());
        fun isStorageDownloadValid(context: Context): Boolean = StateApp.instance.isValidStorageUri(context, getStorageDownloadUri());

        @FormField(R.string.change_external_general_directory, FieldForm.BUTTON, R.string.change_the_external_directory_for_general_files, 3)
        fun changeStorageGeneral() {
            SettingsActivity.getActivity()?.let {
                StateApp.instance.changeExternalGeneralDirectory(it);
            }
        }
        @FormField(R.string.change_external_downloads_directory, FieldForm.BUTTON, R.string.change_the_external_storage_for_download_files, 4)
        fun changeStorageDownload() {
            SettingsActivity.getActivity()?.let {
                StateApp.instance.changeExternalDownloadDirectory(it);
            }
        }

        @FormField(R.string.clear_external_downloads_directory, FieldForm.BUTTON, R.string.clear_the_external_storage_for_download_files, 5)
        fun clearStorageDownload() {
            Settings.instance.storage.storage_download = null;
            Settings.instance.save();
            SettingsActivity.getActivity()?.let { UIDialogs.toast(it, "Cleared download storage directory") };
        }
    @FormField(R.string.auto_update, "group", R.string.configure_the_auto_updater, 15)
Koen's avatar
Koen committed
    var autoUpdate = AutoUpdate();
    @Serializable
    class AutoUpdate {
        @FormField(R.string.check, FieldForm.DROPDOWN, -1, 0)
Koen's avatar
Koen committed
        @DropdownFieldOptionsId(R.array.auto_update_when_array)
        var check: Int = 0;

        @FormField(R.string.background_download, FieldForm.DROPDOWN, R.string.configure_if_background_download_should_be_used, 1)
Koen's avatar
Koen committed
        @DropdownFieldOptionsId(R.array.background_download)
        var backgroundDownload: Int = 0;

        @FormField(R.string.download_when, FieldForm.DROPDOWN, R.string.configure_when_updates_should_be_downloaded, 2)
Koen's avatar
Koen committed
        @DropdownFieldOptionsId(R.array.when_download)
        var whenDownload: Int = 0;

        fun shouldDownload(): Boolean {
            return when (whenDownload) {
                0 -> !StateApp.instance.isCurrentMetered();
                1 -> StateApp.instance.isNetworkState(StateApp.NetworkState.WIFI, StateApp.NetworkState.ETHERNET);
                2 -> true;
                else -> false;
            }
        }

        fun isAutoUpdateEnabled(): Boolean {
            return check == 0 && !BuildConfig.IS_PLAYSTORE_BUILD;
        }

        @FormField(R.string.manual_check, FieldForm.BUTTON, R.string.manually_check_for_updates, 3)
Koen's avatar
Koen committed
        fun manualCheck() {
            if (!BuildConfig.IS_PLAYSTORE_BUILD) {
                SettingsActivity.getActivity()?.let {
Koen's avatar
Koen committed
                    StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
                        StateUpdate.instance.checkForUpdates(it, true)
                    }
Koen's avatar
Koen committed
                }
            } else {
                SettingsActivity.getActivity()?.let {
                    try {
                        it.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=${it.packageName}")))
                    } catch (e: ActivityNotFoundException) {
                        UIDialogs.toast(it, it.getString(R.string.failed_to_show_store));
        @FormField(R.string.view_changelog, FieldForm.BUTTON, R.string.review_the_current_and_past_changelogs, 4)
Koen's avatar
Koen committed
        fun viewChangelog() {
            SettingsActivity.getActivity()?.let {
                UIDialogs.toast(it.getString(R.string.retrieving_changelog));

                StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
Koen's avatar
Koen committed
                    try {
                        val version = StateUpdate.instance.downloadVersionCode(ManagedHttpClient()) ?: return@launch;
                        Logger.i(TAG, "Version retrieved $version");

                        withContext(Dispatchers.Main) {
                            UIDialogs.showChangelogDialog(it, version);
                        }
                    } catch (e: Throwable) {
                        Logger.e("Settings", "Failed to submit logs.", e);
                    }
                }
            };
        }

        @FormField(R.string.remove_cached_version, FieldForm.BUTTON, R.string.remove_the_last_downloaded_version, 5)
Koen's avatar
Koen committed
        fun removeCachedVersion() {
            StateApp.withContext {
                val outputDirectory = File(it.filesDir, "autoupdate");
                if (!outputDirectory.exists()) {
                    UIDialogs.toast("Directory does not exist");
                    return@withContext;
                }

                File(outputDirectory, "last_version.apk").delete();
                File(outputDirectory, "last_version.txt").delete();
                UIDialogs.toast("Removed downloaded version");
            }
        }
    }

    @FormField(R.string.backup, FieldForm.GROUP, -1, 16)
Koen's avatar
Koen committed
    var backup = Backup();
    @Serializable
    class Backup {
        @Serializable(with = OffsetDateTimeSerializer::class)
        var lastAutoBackupTime: OffsetDateTime = OffsetDateTime.MIN;
        var didAskAutoBackup: Boolean = false;
        var autoBackupPassword: String? = null;
        fun shouldAutomaticBackup() = autoBackupPassword != null;

        @FormField(R.string.automatic_backup, FieldForm.READONLYTEXT, -1, 0)
Koen's avatar
Koen committed
        val automaticBackupText get() = if(!shouldAutomaticBackup()) "None" else "Every Day";

        @FormField(R.string.set_automatic_backup, FieldForm.BUTTON, R.string.configure_daily_backup_in_case_of_catastrophic_failure, 1)
Koen's avatar
Koen committed
        fun configureAutomaticBackup() {
            UIDialogs.showAutomaticBackupDialog(SettingsActivity.getActivity()!!, autoBackupPassword != null) {
                SettingsActivity.getActivity()?.reloadSettings();
            };
Koen's avatar
Koen committed
        }
        @FormField(R.string.restore_automatic_backup, FieldForm.BUTTON, R.string.restore_a_previous_automatic_backup, 2)
Koen's avatar
Koen committed
        fun restoreAutomaticBackup() {
            val activity = SettingsActivity.getActivity()!!

            if(!StateBackup.hasAutomaticBackup())
                UIDialogs.toast(activity, activity.getString(R.string.you_don_t_have_any_automatic_backups), false);
Koen's avatar
Koen committed
            else
                UIDialogs.showAutomaticRestoreDialog(activity, activity.lifecycleScope);
        }


        @FormField(R.string.export_data, FieldForm.BUTTON, R.string.creates_a_zip_file_with_your_data_which_can_be_imported_by_opening_it_with_grayjay, 3)
Koen's avatar
Koen committed
        fun export() {
Kelvin's avatar
Kelvin committed
            val activity = SettingsActivity.getActivity() ?: return;
            UISlideOverlays.showOverlay(activity.overlay, "Select export type", null, {},
                SlideUpMenuItem(activity, R.drawable.ic_share, "Share", "", tag = null, call = {
Kelvin's avatar
Kelvin committed
                    StateBackup.shareExternalBackup();
                }),
                SlideUpMenuItem(activity, R.drawable.ic_download, "File", "", tag = null, call = {
Kelvin's avatar
Kelvin committed
                    StateBackup.saveExternalBackup(activity);
                })
            )
    @FormField(R.string.payment, FieldForm.GROUP, -1, 17)
Koen's avatar
Koen committed
    var payment = Payment();
    @Serializable
    class Payment {
        @FormField(R.string.payment_status, FieldForm.READONLYTEXT, -1, 1)
        val paymentStatus: String get() = SettingsActivity.getActivity()?.let { if (StatePayment.instance.hasPaid) it.getString(R.string.paid) else it.getString(R.string.not_paid); } ?: "Unknown";
Koen's avatar
Koen committed

        @FormField(R.string.clear_payment, FieldForm.BUTTON, R.string.deletes_license_keys_from_app, 2)
Koen's avatar
Koen committed
        fun clearPayment() {
            SettingsActivity.getActivity()?.let { context ->
                UIDialogs.showConfirmationDialog(context, "Are you sure you want to delete your license?", {
                    StatePayment.instance.clearLicenses();
                    SettingsActivity.getActivity()?.let {
                        UIDialogs.toast(it, it.getString(R.string.licenses_cleared_might_require_app_restart));
                        it.reloadSettings();
                    }
                })
    @FormField(R.string.other, FieldForm.GROUP, -1, 18)
    var other = Other();
    @Serializable
    class Other {
        @FormField(R.string.playlist_delete_confirmation, FieldForm.TOGGLE, R.string.playlist_delete_confirmation_description, 2)
        var playlistDeleteConfirmation: Boolean = true;

        @FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 3)
        var polycentricEnabled: Boolean = true;
Kelvin's avatar
Kelvin committed

        @FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 4)
        var polycentricLocalCache: Boolean = true;
    @FormField(R.string.gesture_controls, FieldForm.GROUP, -1, 19)
    var gestureControls = GestureControls();
    @Serializable
    class GestureControls {
        @FormField(R.string.volume_slider, FieldForm.TOGGLE, R.string.volume_slider_descr, 1)
        var volumeSlider: Boolean = true;

        @FormField(R.string.brightness_slider, FieldForm.TOGGLE, R.string.brightness_slider_descr, 2)
        var brightnessSlider: Boolean = true;

        @FormField(R.string.toggle_full_screen, FieldForm.TOGGLE, R.string.toggle_full_screen_descr, 3)
        var toggleFullscreen: Boolean = true;

        @FormField(R.string.system_brightness, FieldForm.TOGGLE, R.string.system_brightness_descr, 4)
        var useSystemBrightness: Boolean = false;
        @FormField(R.string.system_volume, FieldForm.TOGGLE, R.string.system_volume_descr, 5)
        var useSystemVolume: Boolean = true;

        @FormField(R.string.restore_system_brightness, FieldForm.TOGGLE, R.string.restore_system_brightness_descr, 6)
        var restoreSystemBrightness: Boolean = true;

        @FormField(R.string.zoom_option, FieldForm.TOGGLE, R.string.zoom_option_descr, 7)
        var zoom: Boolean = true;

        @FormField(R.string.pan_option, FieldForm.TOGGLE, R.string.pan_option_descr, 8)
        var pan: Boolean = true;
Koen's avatar
Koen committed
    @FormField(R.string.synchronization, FieldForm.GROUP, -1, 20)
    var synchronization = Synchronization();
    @Serializable
    class Synchronization {
        @FormField(R.string.enabled, FieldForm.TOGGLE, R.string.enabled_description, 1)
        var enabled: Boolean = true;

        @FormField(R.string.broadcast, FieldForm.TOGGLE, R.string.broadcast_description, 1)
Koen J's avatar
Koen J committed
        var broadcast: Boolean = false;
Koen's avatar
Koen committed

        @FormField(R.string.connect_discovered, FieldForm.TOGGLE, R.string.connect_discovered_description, 2)
        var connectDiscovered: Boolean = true;

        @FormField(R.string.connect_last, FieldForm.TOGGLE, R.string.connect_last_description, 3)
        var connectLast: Boolean = true;
    }

    @FormField(R.string.info, FieldForm.GROUP, -1, 21)
Koen's avatar
Koen committed
    var info = Info();
    @Serializable
    class Info {
        @FormField(R.string.version_code, FieldForm.READONLYTEXT, -1, 1, "code")
Koen's avatar
Koen committed
        var versionCode = BuildConfig.VERSION_CODE;
        @FormField(R.string.version_name, FieldForm.READONLYTEXT, -1, 2)
Koen's avatar
Koen committed
        var versionName = BuildConfig.VERSION_NAME;
        @FormField(R.string.version_type, FieldForm.READONLYTEXT, -1, 3)
Koen's avatar
Koen committed
        var versionType = BuildConfig.BUILD_TYPE;
    }

    //region BOILERPLATE
    override fun encode(): String {
        return Json.encodeToString(this);
    }

    companion object {
        private const val TAG = "Settings";
        const val URL_FAQ = "https://grayjay.app/faq.html";
Koen's avatar
Koen committed

        private var _isFirst = true;

        val instance: Settings get() {
            if(_isFirst) {
                Logger.i(TAG, "Initial Settings fetch");
                _isFirst = false;
            }
            return FragmentedStorage.get<Settings>();
        }

        fun replace(text: String) {
            FragmentedStorage.replace<Settings>(text, true);
        }


        private fun preferedQualityToPixels(q: Int): Int {
            when (q) {
                0 -> return 1280 * 720;
                1 -> return 3840 * 2160;
                2 -> return 2560 * 1440;
                3 -> return 1920 * 1080;
                4 -> return 1280 * 720;
                5 -> return 854 * 480;
                6 -> return 640 * 360;
                7 -> return 426 * 240;
                8 -> return 256 * 144;
                else -> return 0;
            }
        }


        private fun threadIndexToCount(index: Int): Int {
            return when(index) {
                0 -> 1;
                1 -> 2;
                2 -> 4;
                3 -> 6;
                4 -> 8;
                5 -> 10;
                6 -> 15;
                else -> 1
            }
        }
    }
    //endregion
}