From d47f9696a5304d84811fe1e9d348b55e6966c7b5 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sun, 13 Sep 2026 19:38:38 +0200 Subject: [PATCH 1/4] Added compose subtitle settings --- .../cloudstream3/ui/player/GeneratorPlayer.kt | 3 +- .../ui/settings/SettingsFragmentScreen.kt | 24 +- .../ui/subtitles/SubtitlesFragment.kt | 72 ++-- .../ui/subtitles/SubtitlesFragment2.kt | 8 + .../ui/subtitles/SubtitlesScreen.kt | 330 +++++++++++++++++ .../cloudstream3/utils/BaseComposeFragment.kt | 43 +++ .../main/res/drawable/border_color_24px.xml | 10 + .../main/res/drawable/closed_caption_24px.xml | 10 + app/src/main/res/drawable/delete_24px.xml | 10 + .../main/res/drawable/font_download_24px.xml | 10 + .../res/drawable/format_align_center_24px.xml | 10 + .../main/res/drawable/format_bold_24px.xml | 10 + .../res/drawable/format_color_fill_24px.xml | 10 + .../res/drawable/format_color_text_24px.xml | 10 + .../main/res/drawable/format_italic_24px.xml | 10 + .../res/drawable/imagesearch_roller_24px.xml | 10 + .../main/res/drawable/language_download.xml | 7 + .../main/res/drawable/language_download2.xml | 14 + .../main/res/drawable/rounded_corner_24px.xml | 10 + app/src/main/res/drawable/shadow_24px.xml | 10 + app/src/main/res/drawable/shadow_add_24px.xml | 10 + .../main/res/drawable/text_ad_off_24px.xml | 10 + .../main/res/drawable/text_fields_24px.xml | 10 + .../res/drawable/text_select_move_up_24px.xml | 10 + app/src/main/res/drawable/uppercase_24px.xml | 10 + .../main/res/navigation/mobile_navigation.xml | 2 +- app/src/main/res/values/strings.xml | 4 +- .../mihon/common/preference/DataPreference.kt | 238 ++++++++++++ .../common/preference/DataPreferenceStore.kt | 109 ++++++ .../composeResources/values/strings.xml | 3 + .../cloudstream4/compose/ColorDialog.kt | 345 ++++++++++++++++++ .../cloudstream4/compose/Modifiers.kt | 20 +- .../mihon/presentation/settings/Preference.kt | 42 ++- .../presentation/settings/PreferenceItem.kt | 69 +++- 34 files changed, 1445 insertions(+), 58 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment2.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesScreen.kt create mode 100644 app/src/main/res/drawable/border_color_24px.xml create mode 100644 app/src/main/res/drawable/closed_caption_24px.xml create mode 100644 app/src/main/res/drawable/delete_24px.xml create mode 100644 app/src/main/res/drawable/font_download_24px.xml create mode 100644 app/src/main/res/drawable/format_align_center_24px.xml create mode 100644 app/src/main/res/drawable/format_bold_24px.xml create mode 100644 app/src/main/res/drawable/format_color_fill_24px.xml create mode 100644 app/src/main/res/drawable/format_color_text_24px.xml create mode 100644 app/src/main/res/drawable/format_italic_24px.xml create mode 100644 app/src/main/res/drawable/imagesearch_roller_24px.xml create mode 100644 app/src/main/res/drawable/language_download.xml create mode 100644 app/src/main/res/drawable/language_download2.xml create mode 100644 app/src/main/res/drawable/rounded_corner_24px.xml create mode 100644 app/src/main/res/drawable/shadow_24px.xml create mode 100644 app/src/main/res/drawable/shadow_add_24px.xml create mode 100644 app/src/main/res/drawable/text_ad_off_24px.xml create mode 100644 app/src/main/res/drawable/text_fields_24px.xml create mode 100644 app/src/main/res/drawable/text_select_move_up_24px.xml create mode 100644 app/src/main/res/drawable/uppercase_24px.xml create mode 100644 shared/src/androidMain/kotlin/com/mihon/common/preference/DataPreference.kt create mode 100644 shared/src/androidMain/kotlin/com/mihon/common/preference/DataPreferenceStore.kt create mode 100644 shared/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/ColorDialog.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index 4495a560262..e4616c93bcf 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -101,6 +101,7 @@ import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.subtitles.SUBTITLE_AUTO_SELECT_KEY import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageTagIETF +import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragmentDialog2 import com.lagradost.cloudstream3.utils.AppContextUtils.getShortSeasonText import com.lagradost.cloudstream3.utils.AppContextUtils.html import com.lagradost.cloudstream3.utils.AppContextUtils.sortSubs @@ -1046,7 +1047,7 @@ class GeneratorPlayer : FullScreenPlayer() { binding.subtitleSettingsBtt.setOnClickListener { safe { - val subtitlesFragment = SubtitlesFragment() + val subtitlesFragment = SubtitlesFragmentDialog2() subtitlesFragment.systemBarsAddPadding = true subtitlesFragment.show(this.parentFragmentManager, "SubtitleSettings") } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragmentScreen.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragmentScreen.kt index 269452ff2a9..03c66d9fd55 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragmentScreen.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragmentScreen.kt @@ -70,6 +70,7 @@ import com.lagradost.cloudstream3.utils.txt import com.lagradost.cloudstream4.compose.Screen import com.lagradost.cloudstream4.compose.TV import com.lagradost.cloudstream4.compose.circle +import com.lagradost.cloudstream4.compose.circleBorder import com.lagradost.cloudstream4.compose.focusOutline import com.lagradost.cloudstream4.compose.isLayout import com.lagradost.cloudstream4.theme.CloudStreamPreviewTheme @@ -229,23 +230,12 @@ object SettingsFragmentScreen : Screen { account.customImage ?: profileImages.getOrNull(account.defaultImageIndex) ?: profileImages.first() - Box( - modifier = Modifier - .size(50.dp) - .border( - 2.dp, - MaterialTheme.colorScheme.onBackground.copy(alpha = 0.2f), - CircleShape - ) - .circle(), - ) { - AsyncImage( - contentScale = ContentScale.Crop, - model = image, - modifier = Modifier.fillMaxSize(), - contentDescription = null, - ) - } + AsyncImage( + contentScale = ContentScale.Crop, + model = image, + modifier = Modifier.circleBorder(50.dp), + contentDescription = null, + ) Spacer(modifier = Modifier.width(MaterialTheme.padding.medium)) Column { Text( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt index bf77ef24767..cd7ba911d74 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt @@ -62,6 +62,27 @@ const val SUBTITLE_KEY = "subtitle_settings" const val SUBTITLE_AUTO_SELECT_KEY = "subs_auto_select" const val SUBTITLE_DOWNLOAD_KEY = "subs_auto_download" +@Serializable +enum class SubtitleFont( + @FontRes val resource: Int, + val label: String +) { + @JsonProperty("Trebuchet") @SerialName("Trebuchet") Trebuchet(R.font.trebuchet_ms, "Trebuchet MS"), + @JsonProperty("Netflix") @SerialName("Netflix") Netflix(R.font.netflix_sans, "Netflix Sans"), + @JsonProperty("Google") @SerialName("Google") Google(R.font.google_sans, "Google Sans"), + @JsonProperty("Open") @SerialName("Open") Open(R.font.open_sans, "Open Sans"), + @JsonProperty("Futura") @SerialName("Futura") Futura(R.font.futura, "Futura"), + @JsonProperty("Consola") @SerialName("Consola") Consola(R.font.consola, "Consola"), + @JsonProperty("Gotham") @SerialName("Gotham") Gotham(R.font.gotham, "Gotham"), + @JsonProperty("Lucida") @SerialName("Lucida") Lucida(R.font.lucida_grande, "Lucida Grande"), + @JsonProperty("STIX") @SerialName("STIX") STIX(R.font.stix_general, "STIX General"), + @JsonProperty("TimesNewRoman") @SerialName("TimesNewRoman") TimesNewRoman(R.font.times_new_roman, "Times New Roman"), + @JsonProperty("Verdana") @SerialName("Verdana") Verdana(R.font.verdana, "Verdana"), + @JsonProperty("Ubuntu") @SerialName("Ubuntu") Ubuntu(R.font.ubuntu_regular, "Ubuntu"), + @JsonProperty("Comic") @SerialName("Comic") Comic(R.font.comic_sans, "Comic Sans"), + @JsonProperty("Poppins") @SerialName("Poppins") Poppins(R.font.poppins_regular, "Poppins"), +} + @Serializable data class SaveCaptionStyle( @JsonProperty("foregroundColor") @SerialName("foregroundColor") var foregroundColor: Int, @@ -70,7 +91,7 @@ data class SaveCaptionStyle( @OptIn(UnstableApi::class) @JsonProperty("edgeType") @SerialName("edgeType") var edgeType: @CaptionStyleCompat.EdgeType Int, @JsonProperty("edgeColor") @SerialName("edgeColor") var edgeColor: Int, - @FontRes @JsonProperty("typeface") @SerialName("typeface") var typeface: Int?, + @JsonProperty("font") @SerialName("font") var font: SubtitleFont? = null, @JsonProperty("typefaceFilePath") @SerialName("typefaceFilePath") var typefaceFilePath: String?, @JsonProperty("elevation") @SerialName("elevation") var elevation: Int, // in dp @JsonProperty("fixedTextSize") @SerialName("fixedTextSize") var fixedTextSize: Float?, // in sp @@ -130,7 +151,7 @@ class SubtitlesFragment : BaseDialogFragment( } fun Cue.Builder.applyStyle(style: SaveCaptionStyle): Cue.Builder { - val edgeSize = style.edgeSize + val edgeSize = style.edgeSize ?: 0.0f /* This is old code for only applying on non null @@ -151,7 +172,7 @@ class SubtitlesFragment : BaseDialogFragment( // 2. apply edge text?.let { text -> val customSpan = SpannableString.valueOf(text) - if (edgeSize != null) { + if (edgeSize > 0.0f) { customSpan.setSpan( OutlineSpan(edgeSize), 0, customSpan.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE @@ -186,9 +207,9 @@ class SubtitlesFragment : BaseDialogFragment( // 4. apply radius text?.let { text -> val customSpan = SpannableString.valueOf(text) - val radius = style.backgroundRadius + val radius = style.backgroundRadius ?: 0.0f - if (radius != null && style.backgroundColor != Color.TRANSPARENT) { + if (radius > 0.0f && style.backgroundColor != Color.TRANSPARENT) { val styleSpan = RoundedBackgroundColorSpan( style.backgroundColor, this.textAlignment ?: Layout.Alignment.ALIGN_CENTER, @@ -217,22 +238,22 @@ class SubtitlesFragment : BaseDialogFragment( private fun Context.fromSaveToStyle(data: SaveCaptionStyle): CaptionStyleCompat { return CaptionStyleCompat( data.foregroundColor, - // we actually override with a custom span when backgroundRadius != null - if (data.backgroundRadius == null) data.backgroundColor else Color.TRANSPARENT, + data.backgroundColor, data.windowColor, data.edgeType, data.edgeColor, data.typefaceFilePath?.let { try { // RuntimeException: Font asset not found + // Fuck android, they have no good way to load a font from a uri without a copy Typeface.createFromFile(File(it)) - } catch (e: Exception) { + } catch (_: Exception) { null } - } ?: data.typeface?.let { + } ?: data.font?.let { font -> ResourcesCompat.getFont( this, - it + font.resource ) } ?: Typeface.SANS_SERIF @@ -264,17 +285,18 @@ class SubtitlesFragment : BaseDialogFragment( } fun getCurrentSavedStyle(): SaveCaptionStyle { - return cachedSubtitleStyle ?: (getKey(SUBTITLE_KEY) ?: SaveCaptionStyle( - foregroundColor = getDefColor(0), - backgroundColor = getDefColor(2), - windowColor = getDefColor(3), - edgeType = CaptionStyleCompat.EDGE_TYPE_OUTLINE, - edgeColor = getDefColor(1), - typeface = null, - typefaceFilePath = null, - elevation = DEF_SUBS_ELEVATION, - fixedTextSize = null, - )).also { cachedSubtitleStyle = it } + return cachedSubtitleStyle ?: (getKey(SUBTITLE_KEY) + ?: SaveCaptionStyle( + foregroundColor = getDefColor(0), + backgroundColor = getDefColor(2), + windowColor = getDefColor(3), + edgeType = CaptionStyleCompat.EDGE_TYPE_OUTLINE, + edgeColor = getDefColor(1), + font = null, + typefaceFilePath = null, + elevation = DEF_SUBS_ELEVATION, + fixedTextSize = null, + )).also { cachedSubtitleStyle = it } } private fun Context.getSavedFonts(): List { @@ -671,7 +693,7 @@ class SubtitlesFragment : BaseDialogFragment( savedFontTypes.indexOfFirst { it.absolutePath == state.typefaceFilePath } .let { index -> if (index == -1) - fontTypes.indexOfFirst { it.first == state.typeface } + fontTypes.indexOfFirst { it.first == state.font?.resource } else index + fontTypes.size } @@ -684,18 +706,18 @@ class SubtitlesFragment : BaseDialogFragment( dismissCallback ) { index -> if (index < fontTypes.size) { - state.typeface = fontTypes[index].first + state.font = SubtitleFont.entries.firstOrNull { it.resource == fontTypes[index].first } state.typefaceFilePath = null } else { state.typefaceFilePath = savedFontTypes[index - fontTypes.size].absolutePath - state.typeface = null + state.font = null } textView.context.updateState() } } subsFont.setOnLongClickListener { textView -> - state.typeface = null + state.font = null state.typefaceFilePath = null textView.context.updateState() showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment2.kt new file mode 100644 index 00000000000..b9fd405567e --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment2.kt @@ -0,0 +1,8 @@ +package com.lagradost.cloudstream3.ui.subtitles + +import com.lagradost.cloudstream3.utils.BaseComposeFragment +import com.lagradost.cloudstream3.utils.BaseDialogComposeFragment +import com.lagradost.cloudstream4.compose.Screen + +class SubtitlesFragment2 : BaseComposeFragment(), Screen by SubtitlesScreen +class SubtitlesFragmentDialog2 : BaseDialogComposeFragment(), Screen by SubtitlesScreen \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesScreen.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesScreen.kt new file mode 100644 index 00000000000..ff134f57f4e --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesScreen.kt @@ -0,0 +1,330 @@ +package com.lagradost.cloudstream3.ui.subtitles + +import android.text.SpannableString +import androidx.annotation.OptIn +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.media3.common.text.Cue +import androidx.media3.common.util.UnstableApi +import androidx.media3.ui.CaptionStyleCompat +import androidx.media3.ui.SubtitleView +import coil3.compose.AsyncImage +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.ui.player.CustomDecoder +import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.applyStyle +import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.applyStyleEvent +import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getCurrentSavedStyle +import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.saveStyle +import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.setSubtitleViewStyle +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.AppUtils.toJsonLiteral +import com.lagradost.cloudstream3.utils.SubtitleHelper.languages +import com.mihon.common.preference.AndroidPreferenceStore +import com.mihon.common.preference.DataPreferenceStore +import com.mihon.presentation.settings.Preference +import com.mihon.presentation.settings.SearchableSettings +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentMapOf +import kotlin.collections.toSet + +object SubtitlesScreen : SearchableSettings { + @Composable + override fun getTitleRes(): String = stringResource(R.string.subtitles_settings) + + @OptIn(UnstableApi::class) + @Composable + override fun getPreferences(): List { + val initialState = remember { getCurrentSavedStyle() } + var state by remember { mutableStateOf(initialState) } + val context = LocalContext.current + + SideEffect(state) { + // When we modify the state, we modify the stored data + context.saveStyle(state) + } + + DisposableEffect(Unit) { + onDispose { + if(initialState != state) { + applyStyleEvent.invoke(state) + } + } + } + + val text = stringResource(R.string.subtitles_example_text) + val fixedText = SpannableString.valueOf(if (state.upperCase) text.uppercase() else text) + + val dataStore = DataPreferenceStore(LocalContext.current) + val androidStore = AndroidPreferenceStore(LocalContext.current) + val autoSelectSubtitles = dataStore.getString(SUBTITLE_AUTO_SELECT_KEY, "en") + val downloadSubsLanguage = + dataStore.getObjectFromString(SUBTITLE_DOWNLOAD_KEY, setOf("en"), serializer = { obj -> + obj.toList().toJsonLiteral() + }, deserializer = { json -> + parseJson>(json).toSet() + }) + + val subtitlesFilterSubLang = androidStore.getBoolean("filter_sub_lang_key", false) + + return persistentListOf( + Preference.PreferenceItem.CustomPreference( + title = stringResource(R.string.skip_type_preview), content = { + Box( + modifier = Modifier + .fillMaxWidth() + .height(75.dp) + ) { + AsyncImage( + contentDescription = stringResource(R.string.preview_background_img_des), + model = R.drawable.subtitles_preview_background, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize() + ) + AndroidView(modifier = Modifier.fillMaxSize(), factory = { context -> + SubtitleView(context).apply { + setSubtitleViewStyle(this, state, false) + this.setCues( + listOf( + Cue.Builder().setText(fixedText).applyStyle(state).build() + ) + ) + } + }, update = { view -> + setSubtitleViewStyle(view, state, false) + view.setCues( + listOf( + Cue.Builder().setText(fixedText).applyStyle(state).build() + ) + ) + }) + } + }), + Preference.PreferenceGroup( + title = stringResource(R.string.subs_font), + preferenceItems = persistentListOf( + Preference.PreferenceItem.BasicListPreference( + icon = painterResource(R.drawable.font_download_24px), + title = stringResource(R.string.subs_font), + value = state.font, + entries = mapOf(null to stringResource(R.string.normal)) + SubtitleFont.entries.associateWith { it.label }, + onValueChanged = { newValue -> + state = state.copy(font = newValue) + }), + Preference.PreferenceItem.BasicColorPreference( + title = stringResource(R.string.subs_text_color), + value = Color(state.foregroundColor), + icon = painterResource(R.drawable.format_color_text_24px), + onValueChanged = { newValue -> + state = state.copy(foregroundColor = newValue.toArgb()) + } + ), + Preference.PreferenceItem.SliderPreference( + value = state.fixedTextSize?.toInt() ?: 25, + icon = painterResource(R.drawable.text_fields_24px), + title = stringResource(R.string.subs_font_size), + valueRange = 5..60, + onValueChanged = { newValue -> + state = state.copy(fixedTextSize = newValue.toFloat()) + }), + + Preference.PreferenceItem.BasicSwitchPreference( + value = state.upperCase, + icon = painterResource(R.drawable.uppercase_24px), + title = stringResource(R.string.uppercase_all_subtitles), + onValueChanged = { newValue -> + state = state.copy(upperCase = newValue) + }, + ), + Preference.PreferenceItem.BasicSwitchPreference( + value = state.bold, + icon = painterResource(R.drawable.format_bold_24px), + title = stringResource(R.string.all_subtitles_bold), + onValueChanged = { newValue -> + state = state.copy(bold = newValue) + }, + ), + Preference.PreferenceItem.BasicSwitchPreference( + value = state.italic, + icon = painterResource(R.drawable.format_italic_24px), + title = stringResource(R.string.all_subtitles_italic), + onValueChanged = { newValue -> + state = state.copy(italic = newValue) + }, + ), + ) + ), + Preference.PreferenceGroup( + title = stringResource(R.string.subs_edge), + preferenceItems = persistentListOf( + Preference.PreferenceItem.BasicListPreference( + icon = painterResource(R.drawable.shadow_24px), + title = stringResource(R.string.subs_edge_type), + value = state.edgeType, + entries = persistentMapOf( + CaptionStyleCompat.EDGE_TYPE_NONE to stringResource(R.string.subtitles_none), + CaptionStyleCompat.EDGE_TYPE_OUTLINE to stringResource(R.string.subtitles_outline), + CaptionStyleCompat.EDGE_TYPE_DEPRESSED to stringResource(R.string.subtitles_depressed), + CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW to stringResource(R.string.subtitles_shadow), + CaptionStyleCompat.EDGE_TYPE_RAISED to stringResource(R.string.subtitles_raised), + ), + onValueChanged = { newValue -> + state = state.copy(edgeType = newValue) + }), + Preference.PreferenceItem.BasicColorPreference( + enabled = state.edgeType != CaptionStyleCompat.EDGE_TYPE_NONE, + title = stringResource(R.string.subs_outline_color), + value = Color(state.edgeColor), + icon = painterResource(R.drawable.border_color_24px), + onValueChanged = { newValue -> + state = state.copy(edgeColor = newValue.toArgb()) + } + ), + Preference.PreferenceItem.SliderPreference( + enabled = state.edgeType == CaptionStyleCompat.EDGE_TYPE_OUTLINE, + value = state.edgeSize?.toInt() ?: 0, + icon = painterResource(R.drawable.shadow_add_24px), + title = stringResource(R.string.subs_edge_size), + valueRange = 0..60, + onValueChanged = { newValue -> + state = state.copy(edgeSize = newValue.toFloat()) + }), + ) + ), + Preference.PreferenceGroup( + title = stringResource(R.string.background), + preferenceItems = persistentListOf( + Preference.PreferenceItem.BasicColorPreference( + title = stringResource(R.string.subs_window_color), + value = Color(state.windowColor), + icon = painterResource(R.drawable.imagesearch_roller_24px), + onValueChanged = { newValue -> + state = state.copy(windowColor = newValue.toArgb()) + } + ), + Preference.PreferenceItem.BasicColorPreference( + title = stringResource(R.string.subs_background_color), + value = Color(state.backgroundColor), + icon = painterResource(R.drawable.format_color_fill_24px), + onValueChanged = { newValue -> + state = state.copy(backgroundColor = newValue.toArgb()) + } + ), + Preference.PreferenceItem.SliderPreference( + value = state.backgroundRadius?.toInt() ?: 0, + icon = painterResource(R.drawable.rounded_corner_24px), + title = stringResource(R.string.background_radius), + valueRange = 0..50, + steps = 9, + onValueChanged = { newValue -> + state = state.copy(backgroundRadius = newValue.toFloat()) + }), + ) + ), + Preference.PreferenceGroup( + title = stringResource(R.string.pref_category_player_layout), + preferenceItems = persistentListOf( + Preference.PreferenceItem.BasicListPreference( + icon = painterResource(R.drawable.format_align_center_24px), + title = stringResource(R.string.subs_subtitle_alignment), + value = state.alignment, + entries = persistentMapOf( + null to stringResource(R.string.automatic), + CustomDecoder.SSA_ALIGNMENT_BOTTOM_LEFT to stringResource(R.string.bottom_left), + CustomDecoder.SSA_ALIGNMENT_BOTTOM_CENTER to stringResource(R.string.bottom_center), + CustomDecoder.SSA_ALIGNMENT_BOTTOM_RIGHT to stringResource(R.string.bottom_right), + CustomDecoder.SSA_ALIGNMENT_MIDDLE_LEFT to stringResource(R.string.middle_left), + CustomDecoder.SSA_ALIGNMENT_MIDDLE_CENTER to stringResource(R.string.middle_center), + CustomDecoder.SSA_ALIGNMENT_MIDDLE_RIGHT to stringResource(R.string.middle_right), + CustomDecoder.SSA_ALIGNMENT_TOP_LEFT to stringResource(R.string.top_left), + CustomDecoder.SSA_ALIGNMENT_TOP_CENTER to stringResource(R.string.top_center), + CustomDecoder.SSA_ALIGNMENT_TOP_RIGHT to stringResource(R.string.top_right), + ), + onValueChanged = { newValue -> + state = state.copy(alignment = newValue) + }), + Preference.PreferenceItem.SliderPreference( + value = state.elevation, + title = stringResource(R.string.subs_subtitle_elevation), + icon = painterResource(R.drawable.text_select_move_up_24px), + valueRange = 0..400, + steps = 39, + onValueChanged = { newValue -> + state = state.copy(elevation = newValue) + }), + ) + ), + + Preference.PreferenceGroup( + title = stringResource(R.string.extension_language), + preferenceItems = persistentListOf( + Preference.PreferenceItem.ListPreference( + icon = painterResource(R.drawable.language_korean_latin_24px), + title = stringResource(R.string.subs_auto_select_language), + preference = autoSelectSubtitles, + entries = mapOf( + "None" to stringResource(R.string.none) + ) + languages.map { it.IETF_tag to it.nameNextToFlagEmoji() }.sortedBy { + it.second.substringAfter("\u00a0").lowercase() + }.toMap(), + ), + Preference.PreferenceItem.MultiSelectListPreference( + icon = painterResource(R.drawable.language_download2), + title = stringResource(R.string.subs_download_languages), + preference = downloadSubsLanguage, + entries = mapOf( + "None" to stringResource(R.string.none) + ) + languages.map { it.IETF_tag to it.nameNextToFlagEmoji() }.sortedBy { + it.second.substringAfter("\u00a0").lowercase() + }.toMap(), + ), + Preference.PreferenceItem.SwitchPreference( + preference = subtitlesFilterSubLang, + icon = painterResource(R.drawable.filter_alt_24px), + title = stringResource(R.string.subtitles_filter_lang), + ), + ) + ), + + Preference.PreferenceGroup( + title = stringResource(R.string.pref_category_ui_features), + preferenceItems = persistentListOf( + Preference.PreferenceItem.BasicSwitchPreference( + value = state.removeCaptions, + icon = painterResource(R.drawable.closed_caption_24px), + title = stringResource(R.string.subtitles_remove_captions), + subtitle = "[Knocking on door] Hello → Hello", + onValueChanged = { newValue -> + state = state.copy(removeCaptions = newValue) + }, + ), + Preference.PreferenceItem.BasicSwitchPreference( + value = state.removeBloat, + icon = painterResource(R.drawable.text_ad_off_24px), + title = stringResource(R.string.subtitles_remove_bloat), + onValueChanged = { newValue -> + state = state.copy(removeBloat = newValue) + }, + ), + ) + ), + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BaseComposeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BaseComposeFragment.kt index 91a75d0ab02..b11b037546d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BaseComposeFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BaseComposeFragment.kt @@ -5,7 +5,9 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment +import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLandscape @@ -40,4 +42,45 @@ abstract class BaseComposeFragment : Fragment(), Screen { padBottom = isLandscape() ) } +} + +abstract class BaseDialogComposeFragment : DialogFragment(), Screen { + var systemBarsAddPadding = isLayout(TV or EMULATOR) + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = createComposeView(inflater, container, savedInstanceState) + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + dialog?.window?.decorView?.let { view -> + fixSystemBarsPadding( + view, + padLeft = systemBarsAddPadding, + padBottom = systemBarsAddPadding || isLandscape() + ) + } + } + + override fun onStart() { + super.onStart() + dialog?.window?.setWindowAnimations(R.style.DialogFullscreenPlayer) + } + + override fun getTheme(): Int { + return R.style.DialogFullscreenPlayer + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + dialog?.window?.decorView?.let { view -> + fixSystemBarsPadding( + view, + padLeft = systemBarsAddPadding, + padBottom = systemBarsAddPadding || isLandscape() + ) + } + } } \ No newline at end of file diff --git a/app/src/main/res/drawable/border_color_24px.xml b/app/src/main/res/drawable/border_color_24px.xml new file mode 100644 index 00000000000..f4a5bbf3b0d --- /dev/null +++ b/app/src/main/res/drawable/border_color_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/closed_caption_24px.xml b/app/src/main/res/drawable/closed_caption_24px.xml new file mode 100644 index 00000000000..793e1e0611a --- /dev/null +++ b/app/src/main/res/drawable/closed_caption_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/delete_24px.xml b/app/src/main/res/drawable/delete_24px.xml new file mode 100644 index 00000000000..237e75aa6fd --- /dev/null +++ b/app/src/main/res/drawable/delete_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/font_download_24px.xml b/app/src/main/res/drawable/font_download_24px.xml new file mode 100644 index 00000000000..82f04c483d1 --- /dev/null +++ b/app/src/main/res/drawable/font_download_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/format_align_center_24px.xml b/app/src/main/res/drawable/format_align_center_24px.xml new file mode 100644 index 00000000000..8a425daa4f6 --- /dev/null +++ b/app/src/main/res/drawable/format_align_center_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/format_bold_24px.xml b/app/src/main/res/drawable/format_bold_24px.xml new file mode 100644 index 00000000000..61aa7f5090e --- /dev/null +++ b/app/src/main/res/drawable/format_bold_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/format_color_fill_24px.xml b/app/src/main/res/drawable/format_color_fill_24px.xml new file mode 100644 index 00000000000..3dc2047e99d --- /dev/null +++ b/app/src/main/res/drawable/format_color_fill_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/format_color_text_24px.xml b/app/src/main/res/drawable/format_color_text_24px.xml new file mode 100644 index 00000000000..e1d88be3317 --- /dev/null +++ b/app/src/main/res/drawable/format_color_text_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/format_italic_24px.xml b/app/src/main/res/drawable/format_italic_24px.xml new file mode 100644 index 00000000000..0f69910e85c --- /dev/null +++ b/app/src/main/res/drawable/format_italic_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/imagesearch_roller_24px.xml b/app/src/main/res/drawable/imagesearch_roller_24px.xml new file mode 100644 index 00000000000..184f6f60a29 --- /dev/null +++ b/app/src/main/res/drawable/imagesearch_roller_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/language_download.xml b/app/src/main/res/drawable/language_download.xml new file mode 100644 index 00000000000..129dd4eead5 --- /dev/null +++ b/app/src/main/res/drawable/language_download.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/src/main/res/drawable/language_download2.xml b/app/src/main/res/drawable/language_download2.xml new file mode 100644 index 00000000000..30cb8bf13ab --- /dev/null +++ b/app/src/main/res/drawable/language_download2.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/rounded_corner_24px.xml b/app/src/main/res/drawable/rounded_corner_24px.xml new file mode 100644 index 00000000000..6d3180c4287 --- /dev/null +++ b/app/src/main/res/drawable/rounded_corner_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/shadow_24px.xml b/app/src/main/res/drawable/shadow_24px.xml new file mode 100644 index 00000000000..09bf8d62e20 --- /dev/null +++ b/app/src/main/res/drawable/shadow_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/shadow_add_24px.xml b/app/src/main/res/drawable/shadow_add_24px.xml new file mode 100644 index 00000000000..9ff30d54c8c --- /dev/null +++ b/app/src/main/res/drawable/shadow_add_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/text_ad_off_24px.xml b/app/src/main/res/drawable/text_ad_off_24px.xml new file mode 100644 index 00000000000..2a108446b05 --- /dev/null +++ b/app/src/main/res/drawable/text_ad_off_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/text_fields_24px.xml b/app/src/main/res/drawable/text_fields_24px.xml new file mode 100644 index 00000000000..49153d403fa --- /dev/null +++ b/app/src/main/res/drawable/text_fields_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/text_select_move_up_24px.xml b/app/src/main/res/drawable/text_select_move_up_24px.xml new file mode 100644 index 00000000000..7311798a9ea --- /dev/null +++ b/app/src/main/res/drawable/text_select_move_up_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/uppercase_24px.xml b/app/src/main/res/drawable/uppercase_24px.xml new file mode 100644 index 00000000000..68b9166ae87 --- /dev/null +++ b/app/src/main/res/drawable/uppercase_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index 6a1ea6912fd..e6691e91e17 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -416,7 +416,7 @@ Outline Color Background Color Window Color + Edge Edge Type Subtitle Elevation Font @@ -486,7 +487,7 @@ Invalid URL Error Remove closed captions from subtitles - Remove bloat from subtitles + Remove ads from subtitles Filter by preferred media language Extras Trailer @@ -743,6 +744,7 @@ Make all subtitles bold Make all subtitles italic Background Radius + Background How many different items that can be downloaded in parallel Parallel downloads Concurrent connections diff --git a/shared/src/androidMain/kotlin/com/mihon/common/preference/DataPreference.kt b/shared/src/androidMain/kotlin/com/mihon/common/preference/DataPreference.kt new file mode 100644 index 00000000000..dcf4eedfa81 --- /dev/null +++ b/shared/src/androidMain/kotlin/com/mihon/common/preference/DataPreference.kt @@ -0,0 +1,238 @@ +package com.mihon.common.preference + +import android.content.SharedPreferences +import android.content.SharedPreferences.Editor +import androidx.core.content.edit +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn + + +sealed class DataPreference( + private val preferences: SharedPreferences, + private val keyFlow: MutableSharedFlow, + private val key: String, + private val defaultValue: T, +) : PreferenceData { + + abstract fun read(preferences: SharedPreferences, key: String, defaultValue: T): T + + abstract fun write(key: String, value: T): Editor.() -> Unit + + override fun key(): String { + return key + } + + override fun get(): T { + return try { + read(preferences, key, defaultValue) + } catch (e: ClassCastException) { + //logcat { "Invalid value for $key; deleting" } + delete() + defaultValue + } + } + + override fun set(value: T) { + preferences.edit(action = write(key, value)) + keyFlow.tryEmit(key) + } + + override fun isSet(): Boolean { + return preferences.contains(key) + } + + override fun delete() { + preferences.edit { + remove(key) + } + } + + override fun defaultValue(): T { + return defaultValue + } + + override fun changes(): Flow { + return keyFlow + .filter { it == key || it == null } + .onStart { emit("ignition") } + .map { get() } + .conflate() + } + + override fun stateIn(scope: CoroutineScope): StateFlow { + return changes().stateIn(scope, SharingStarted.Eagerly, get()) + } + + class StringPrimitive( + preferences: SharedPreferences, + keyFlow: MutableSharedFlow, + key: String, + defaultValue: String, + ) : DataPreference(preferences, keyFlow, key, defaultValue) { + override fun read( + preferences: SharedPreferences, + key: String, + defaultValue: String, + ): String { + return preferences.getString(key, defaultValue) ?: defaultValue + } + + override fun write(key: String, value: String): Editor.() -> Unit = { + putString(key, value) + } + } + + class LongPrimitive( + preferences: SharedPreferences, + keyFlow: MutableSharedFlow, + key: String, + defaultValue: Long, + ) : DataPreference(preferences, keyFlow, key, defaultValue) { + override fun read(preferences: SharedPreferences, key: String, defaultValue: Long): Long { + return preferences.getLong(key, defaultValue) + } + + override fun write(key: String, value: Long): Editor.() -> Unit = { + putLong(key, value) + } + } + + class IntPrimitive( + preferences: SharedPreferences, + keyFlow: MutableSharedFlow, + key: String, + defaultValue: Int, + ) : DataPreference(preferences, keyFlow, key, defaultValue) { + override fun read(preferences: SharedPreferences, key: String, defaultValue: Int): Int { + return preferences.getInt(key, defaultValue) + } + + override fun write(key: String, value: Int): Editor.() -> Unit = { + putInt(key, value) + } + } + + class FloatPrimitive( + preferences: SharedPreferences, + keyFlow: MutableSharedFlow, + key: String, + defaultValue: Float, + ) : DataPreference(preferences, keyFlow, key, defaultValue) { + override fun read(preferences: SharedPreferences, key: String, defaultValue: Float): Float { + return preferences.getFloat(key, defaultValue) + } + + override fun write(key: String, value: Float): Editor.() -> Unit = { + putFloat(key, value) + } + } + + class BooleanPrimitive( + preferences: SharedPreferences, + keyFlow: MutableSharedFlow, + key: String, + defaultValue: Boolean, + ) : DataPreference(preferences, keyFlow, key, defaultValue) { + override fun read( + preferences: SharedPreferences, + key: String, + defaultValue: Boolean, + ): Boolean { + return preferences.getBoolean(key, defaultValue) + } + + override fun write(key: String, value: Boolean): Editor.() -> Unit = { + putBoolean(key, value) + } + } + + class StringSetPrimitive( + preferences: SharedPreferences, + keyFlow: MutableSharedFlow, + key: String, + defaultValue: Set, + ) : DataPreference>(preferences, keyFlow, key, defaultValue) { + override fun read( + preferences: SharedPreferences, + key: String, + defaultValue: Set, + ): Set { + return preferences.getStringSet(key, defaultValue) ?: defaultValue + } + + override fun write(key: String, value: Set): Editor.() -> Unit = { + putStringSet(key, value) + } + } + + class ObjectAsString( + preferences: SharedPreferences, + keyFlow: MutableSharedFlow, + key: String, + defaultValue: T, + private val serializer: (T) -> String, + private val deserializer: (String) -> T, + ) : DataPreference(preferences, keyFlow, key, defaultValue) { + override fun read(preferences: SharedPreferences, key: String, defaultValue: T): T { + return try { + preferences.getString(key, null)?.let(deserializer) ?: defaultValue + } catch (e: Exception) { + defaultValue + } + } + + override fun write(key: String, value: T): Editor.() -> Unit = { + putString(key, serializer(value)) + } + } + + class ObjectAsInt( + preferences: SharedPreferences, + keyFlow: MutableSharedFlow, + key: String, + defaultValue: T, + private val serializer: (T) -> Int, + private val deserializer: (Int) -> T, + ) : DataPreference(preferences, keyFlow, key, defaultValue) { + override fun read(preferences: SharedPreferences, key: String, defaultValue: T): T { + return try { + if (preferences.contains(key)) preferences.getInt(key, 0).let(deserializer) else defaultValue + } catch (e: Exception) { + defaultValue + } + } + + override fun write(key: String, value: T): Editor.() -> Unit = { + putInt(key, serializer(value)) + } + } + + class ObjectSetAsStringSet( + preferences: SharedPreferences, + keyFlow: MutableSharedFlow, + key: String, + defaultValue: Set, + private val serializer: (T) -> String, + private val deserializer: (String) -> T?, + ) : DataPreference>(preferences, keyFlow, key, defaultValue) { + override fun read(preferences: SharedPreferences, key: String, defaultValue: Set): Set { + return try { + preferences.getStringSet(key, null)?.mapNotNull(deserializer)?.toSet() ?: defaultValue + } catch (_: Exception) { + defaultValue + } + } + + override fun write(key: String, value: Set): Editor.() -> Unit = { + putStringSet(key, value.map(serializer).toSet()) + } + } +} \ No newline at end of file diff --git a/shared/src/androidMain/kotlin/com/mihon/common/preference/DataPreferenceStore.kt b/shared/src/androidMain/kotlin/com/mihon/common/preference/DataPreferenceStore.kt new file mode 100644 index 00000000000..4f197768da5 --- /dev/null +++ b/shared/src/androidMain/kotlin/com/mihon/common/preference/DataPreferenceStore.kt @@ -0,0 +1,109 @@ +package com.mihon.common.preference + +import android.content.Context +import android.content.SharedPreferences +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.AppUtils.toJsonLiteral +import kotlinx.coroutines.flow.MutableSharedFlow + +/** + * Same as AndroidPreferenceStore for DataStore but with some optimization to only see changes when values are + * changed by this store. This is done to avoid having a global key listener where we might pollute + * the setKey from other places. + * */ +class DataPreferenceStore(private val sharedPreferences: SharedPreferences) : PreferenceStore { + constructor(context: Context) : this( + context.getSharedPreferences( + "rebuild_preference", + Context.MODE_PRIVATE + ) + ) + + companion object { + private val keyFlow: MutableSharedFlow = + MutableSharedFlow(extraBufferCapacity = 16) + } + + override fun getString(key: String, defaultValue: String): PreferenceData { + return DataPreference.StringPrimitive(sharedPreferences, keyFlow, key, defaultValue) + } + + override fun getLong(key: String, defaultValue: Long): PreferenceData { + return DataPreference.LongPrimitive(sharedPreferences, keyFlow, key, defaultValue) + } + + override fun getInt(key: String, defaultValue: Int): PreferenceData { + return DataPreference.IntPrimitive(sharedPreferences, keyFlow, key, defaultValue) + } + + override fun getFloat(key: String, defaultValue: Float): PreferenceData { + return DataPreference.FloatPrimitive(sharedPreferences, keyFlow, key, defaultValue) + } + + override fun getBoolean(key: String, defaultValue: Boolean): PreferenceData { + return DataPreference.BooleanPrimitive(sharedPreferences, keyFlow, key, defaultValue) + } + + override fun getStringSet(key: String, defaultValue: Set): PreferenceData> { + return DataPreference.StringSetPrimitive(sharedPreferences, keyFlow, key, defaultValue) + } + + inline fun getJson(key: String, defaultValue: T): PreferenceData = + getObjectFromString(key, defaultValue, serializer = { obj -> + obj.toJsonLiteral() + }, deserializer = { json -> + parseJson(json) + }) + + override fun getObjectFromString( + key: String, + defaultValue: T, + serializer: (T) -> String, + deserializer: (String) -> T, + ): PreferenceData { + return DataPreference.ObjectAsString( + preferences = sharedPreferences, + keyFlow = keyFlow, + key = key, + defaultValue = defaultValue, + serializer = serializer, + deserializer = deserializer, + ) + } + + override fun getObjectFromInt( + key: String, + defaultValue: T, + serializer: (T) -> Int, + deserializer: (Int) -> T, + ): PreferenceData { + return DataPreference.ObjectAsInt( + preferences = sharedPreferences, + keyFlow = keyFlow, + key = key, + defaultValue = defaultValue, + serializer = serializer, + deserializer = deserializer, + ) + } + + override fun getObjectSetFromStringSet( + key: String, + defaultValue: Set, + serializer: (T) -> String, + deserializer: (String) -> T?, + ): PreferenceData> { + return DataPreference.ObjectSetAsStringSet( + preferences = sharedPreferences, + keyFlow = keyFlow, + key = key, + defaultValue = defaultValue, + serializer = serializer, + deserializer = deserializer, + ) + } + + override fun getAll(): Map { + return sharedPreferences.all ?: emptyMap() + } +} diff --git a/shared/src/commonMain/composeResources/values/strings.xml b/shared/src/commonMain/composeResources/values/strings.xml index fbd98f6492a..6d81554c1ab 100644 --- a/shared/src/commonMain/composeResources/values/strings.xml +++ b/shared/src/commonMain/composeResources/values/strings.xml @@ -4,6 +4,9 @@ No results found Ok Cancel + Transparency + Luminance + Color Navigate up Lorem ipsum Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n\nUt enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. diff --git a/shared/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/ColorDialog.kt b/shared/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/ColorDialog.kt new file mode 100644 index 00000000000..a89085556bb --- /dev/null +++ b/shared/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/ColorDialog.kt @@ -0,0 +1,345 @@ +package com.lagradost.cloudstream4.compose + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties +import com.lagradost.cloudstream4.generated.resources.Res +import com.lagradost.cloudstream4.generated.resources.cancel +import com.lagradost.cloudstream4.generated.resources.color +import com.lagradost.cloudstream4.generated.resources.luminance +import com.lagradost.cloudstream4.generated.resources.ok +import com.lagradost.cloudstream4.generated.resources.transparency +import com.mihon.material.Slider +import com.mihon.material.padding +import org.jetbrains.compose.resources.stringResource +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min +import kotlin.math.roundToInt + + +internal val colors = arrayOf( + Color(0xFFF44336), // RED 500 + Color(0xFFE91E63), // PINK 500 + Color(0xFFFF2C93), // LIGHT PINK 500 + Color(0xFF9C27B0), // PURPLE 500 + Color(0xFF673AB7), // DEEP PURPLE 500 + Color(0xFF3F51B5), // INDIGO 500 + Color(0xFF2196F3), // BLUE 500 + Color(0xFF03A9F4), // LIGHT BLUE 500 + Color(0xFF00BCD4), // CYAN 500 + Color(0xFF009688), // TEAL 500 + Color(0xFF4CAF50), // GREEN 500 + Color(0xFF8BC34A), // LIGHT GREEN 500 + Color(0xFFCDDC39), // LIME 500 + Color(0xFFFFEB3B), // YELLOW 500 + Color(0xFFFFC107), // AMBER 500 + Color(0xFFFF9800), // ORANGE 500 + Color(0xFF795548), // BROWN 500 + Color(0xFF607D8B), // BLUE GREY 500 + Color(0xFF9E9E9E), // GREY 500 +) +internal val colors2 = arrayOf( + Color(0xFF795548), // BROWN 500 + Color(0xFF607D8B), // BLUE GREY 500 + Color(0xFF9E9E9E), // GREY 500 +) + +@Composable +fun ColorDialog( + title: String, + color: Color, + dismiss: () -> Unit, + confirm: (Color) -> Unit, +) { + var alpha by remember { mutableFloatStateOf(color.alpha) } + var selectedColor by remember { mutableStateOf(color) } + + AlertDialog( + properties = DialogProperties(usePlatformDefaultWidth = false), + containerColor = MaterialTheme.colorScheme.background, + onDismissRequest = dismiss, + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + ColorCircle( + color = selectedColor.copy(alpha = alpha), + ) + Spacer(modifier = Modifier.size(MaterialTheme.padding.medium)) + Text(text = title) + } + }, + text = { + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + ) { + Text( + text = stringResource(Res.string.color), + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(MaterialTheme.padding.extraSmall), + style = MaterialTheme.typography.titleMedium + ) + FlowRow { + ColorCircle( + color = Color.White.copy(alpha = alpha), + ) { + selectedColor = Color.White + } + + val hsl = floatArrayOf(0.0f, 1.0f, 0.5f) + (0..18).forEach { hue -> + hsl[0] = hue.toFloat() * 18f + val color = hslToColor(hsl) + ColorCircle( + color = color.copy(alpha = alpha), + ) { + selectedColor = color + } + } + colors2.forEach { color -> + ColorCircle( + color = color.copy(alpha = alpha), + ) { + selectedColor = color + } + } + } + + Text( + text = stringResource(Res.string.luminance), + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(MaterialTheme.padding.extraSmall), + style = MaterialTheme.typography.titleMedium + ) + + // https://github.com/mhssn95/compose-color-picker/blob/main/colorPicker/src/main/java/io/mhssn/colorpicker/ext/drawExt.kt + Row(modifier = Modifier.horizontalScroll(rememberScrollState())) { + val hsl = floatArrayOf(0.0f, 0.0f, 0.0f) + (0..10).forEach { luminance -> + rbgToHSL(selectedColor.red, selectedColor.green, selectedColor.blue, hsl) + hsl[2] = luminance.toFloat() * 0.1f + val color = hslToColor(hsl) + ColorCircle( + color = color.copy(alpha = alpha), + ) { + selectedColor = color + } + } + } + + Text( + text = stringResource(Res.string.transparency), + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(MaterialTheme.padding.extraSmall), + style = MaterialTheme.typography.titleMedium + ) + val haptic = LocalHapticFeedback.current + Slider( + modifier = Modifier.focusOutline().padding(MaterialTheme.padding.extraSmall), + value = (alpha * 10.0f).toInt(), + onValueChange = f@{ newValue -> + alpha = newValue.toFloat() * 0.1f + haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) + }, + valueRange = 0..10, + ) + } + }, + confirmButton = { + WhiteButton( + text = stringResource(Res.string.ok), + onClick = { confirm(selectedColor.copy(alpha = alpha)) }) + }, + dismissButton = { + BlackButton(text = stringResource(Res.string.cancel), onClick = dismiss) + }) +} + +@Composable +fun ColorCircle( + color: Color, + modifier: Modifier = Modifier, + size: Dp = 50.dp, + clickable: (() -> Unit)? = null +) { + Canvas( + modifier = modifier + .focusOutline(shape = CircleShape) + .padding(MaterialTheme.padding.extraSmall) + .size(size) + .border( + 1.dp, + if (color.alpha > 0.5f) MaterialTheme.colorScheme.onBackground else color.copy( + alpha = 1.0f + ), + CircleShape + ) + .circle() + .clickable(enabled = clickable != null) { + clickable?.invoke() + } + ) { + drawTransparentBackground(10) + drawRect(color) + } +} + +internal fun DrawScope.drawTransparentBackground(verticalBoxesSize: Int = 10) { + val boxSize = size.height / verticalBoxesSize + repeat((size.width / boxSize).roundToInt() + 1) { x -> + repeat(verticalBoxesSize) { y -> + drawRect( + if ((y + x) % 2 == 0) { + Color.LightGray + } else { + Color.White + }, topLeft = Offset(x * boxSize, y * boxSize), size = Size(boxSize, boxSize) + ) + } + } +} + +/** + * Convert RGB components to HSL (hue-saturation-lightness). + * + * * outHsl[0] is Hue [0, 360) + * * outHsl[1] is Saturation [0, 1] + * * outHsl[2] is Lightness [0, 1] + * + * + * @param rf red component value [0, 1] + * @param gf green component value [0, 1] + * @param bf blue component value [0, 1] + * @param outHsl 3-element array which holds the resulting HSL components + */ +fun rbgToHSL( + rf: Float, + gf: Float, + bf: Float, + outHsl: FloatArray +) { + val max = max(rf, max(gf, bf)) + val min = min(rf, min(gf, bf)) + val deltaMaxMin = max - min + + var h: Float + val s: Float + val l = (max + min) / 2f + + if (max == min) { + // Monochromatic + s = 0f + h = s + } else { + if (max == rf) { + h = ((gf - bf) / deltaMaxMin) % 6f + } else if (max == gf) { + h = ((bf - rf) / deltaMaxMin) + 2f + } else { + h = ((rf - gf) / deltaMaxMin) + 4f + } + + s = deltaMaxMin / (1f - abs(2f * l - 1f)) + } + + h = (h * 60f) % 360f + if (h < 0) { + h += 360f + } + + outHsl[0] = h.coerceIn(0f, 360f) + outHsl[1] = s.coerceIn(0f, 1f) + outHsl[2] = l.coerceIn(0f, 1f) +} + +fun hslToColor(hsl: FloatArray): Color { + val h = hsl[0] + val s = hsl[1] + val l = hsl[2] + + val c = (1f - abs(2 * l - 1f)) * s + val m = l - 0.5f * c + val x = c * (1f - abs((h / 60f % 2f) - 1f)) + + val hueSegment = h.toInt() / 60 + + var r = 0 + var g = 0 + var b = 0 + + when (hueSegment) { + 0 -> { + r = (255 * (c + m)).roundToInt() + g = (255 * (x + m)).roundToInt() + b = (255 * m).roundToInt() + } + + 1 -> { + r = (255 * (x + m)).roundToInt() + g = (255 * (c + m)).roundToInt() + b = (255 * m).roundToInt() + } + + 2 -> { + r = (255 * m).roundToInt() + g = (255 * (c + m)).roundToInt() + b = (255 * (x + m)).roundToInt() + } + + 3 -> { + r = (255 * m).roundToInt() + g = (255 * (x + m)).roundToInt() + b = (255 * (c + m)).roundToInt() + } + + 4 -> { + r = (255 * (x + m)).roundToInt() + g = (255 * m).roundToInt() + b = (255 * (c + m)).roundToInt() + } + + 5, 6 -> { + r = (255 * (c + m)).roundToInt() + g = (255 * m).roundToInt() + b = (255 * (x + m)).roundToInt() + } + } + + r = r.coerceIn(0, 255) + g = g.coerceIn(0, 255) + b = b.coerceIn(0, 255) + + return Color(r, g, b) +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/Modifiers.kt b/shared/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/Modifiers.kt index ee910dd3642..140c89b7655 100644 --- a/shared/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/Modifiers.kt +++ b/shared/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/Modifiers.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream4.compose import androidx.compose.foundation.border +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme @@ -15,6 +16,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp /* @@ -39,18 +41,30 @@ fun Modifier.rounded(): Modifier = fun Modifier.circle(): Modifier = clip(CircleShape) +@Composable +fun Modifier.circleBorder(size: Dp): Modifier = + this.size(size) + .border( + 2.dp, + MaterialTheme.colorScheme.onBackground.copy(alpha = 0.2f), + CircleShape + ) + .circle() + + /** * If we should have outlines on items when focused by default, this should be false for desktop * as we do not use a dpad for that * */ -val LocalFocusOutlineDefault: ProvidableCompositionLocal = staticCompositionLocalOf { true } +val LocalFocusOutlineDefault: ProvidableCompositionLocal = + staticCompositionLocalOf { true } @Composable fun Modifier.focusOutline( - enabled : Boolean = LocalFocusOutlineDefault.current, + enabled: Boolean = LocalFocusOutlineDefault.current, shape: Shape = RoundedShape() ): Modifier { - if(!enabled) return this + if (!enabled) return this var hasFocus by remember { mutableStateOf(false) } diff --git a/shared/src/commonMain/kotlin/com/mihon/presentation/settings/Preference.kt b/shared/src/commonMain/kotlin/com/mihon/presentation/settings/Preference.kt index 11ad8b9846f..e857cd9281b 100644 --- a/shared/src/commonMain/kotlin/com/mihon/presentation/settings/Preference.kt +++ b/shared/src/commonMain/kotlin/com/mihon/presentation/settings/Preference.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import com.lagradost.cloudstream4.generated.resources.Res import com.lagradost.cloudstream4.generated.resources.none @@ -46,6 +47,30 @@ sealed class Preference { override val icon: Painter? = null ) : PreferenceItem() + /** + * A [PreferenceItem] that provides a two-state toggleable option. + */ + data class BasicSwitchPreference( + val value: Boolean, + override val title: String, + override val subtitle: String? = null, + override val enabled: Boolean = true, + override val onValueChanged: suspend (value: Boolean) -> Unit = {}, + override val icon: Painter? = null + ) : PreferenceItem() + + /** + * A [PreferenceItem] that provides a color + */ + data class BasicColorPreference( + val value: Color, + override val title: String, + override val subtitle: String? = null, + override val enabled: Boolean = true, + override val onValueChanged: suspend (value: Color) -> Unit = {}, + override val icon: Painter? = null + ) : PreferenceItem() + /** * A [PreferenceItem] that provides a slider to select an integer number. */ @@ -91,17 +116,20 @@ sealed class Preference { /** * [ListPreference] but with no connection to a [PreferenceData] */ - data class BasicListPreference( - val value: String, - val entries: Map, + data class BasicListPreference( + val value: T, + val entries: Map, override val title: String, override val subtitle: String? = "%s", - val subtitleProvider: @Composable (value: String, entries: Map) -> String? = + val subtitleProvider: @Composable (value: T, entries: Map) -> String? = { v, e -> subtitle?.format(e[v]) }, override val icon: Painter? = null, override val enabled: Boolean = true, - override val onValueChanged: suspend (value: String) -> Unit = {}, - ) : PreferenceItem() + override val onValueChanged: suspend (value: T) -> Unit = {}, + ) : PreferenceItem() { + @Suppress("UNCHECKED_CAST") + internal suspend fun internalOnValueChanged(value: Any?) = onValueChanged(value as T) + } /** * A [PreferenceItem] that displays a list of entries as a dialog. @@ -187,7 +215,7 @@ sealed class Preference { override val title: String, override val enabled: Boolean = true, - val preferenceItems: List>, + val preferenceItems: List>, ) : Preference() } diff --git a/shared/src/commonMain/kotlin/com/mihon/presentation/settings/PreferenceItem.kt b/shared/src/commonMain/kotlin/com/mihon/presentation/settings/PreferenceItem.kt index 151f3cb69bd..6dc0f8fdf18 100644 --- a/shared/src/commonMain/kotlin/com/mihon/presentation/settings/PreferenceItem.kt +++ b/shared/src/commonMain/kotlin/com/mihon/presentation/settings/PreferenceItem.kt @@ -11,10 +11,16 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.runtime.structuralEqualityPolicy import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.lagradost.cloudstream4.compose.ColorCircle +import com.lagradost.cloudstream4.compose.ColorDialog +import com.mihon.material.BaseSliderItem import com.mihon.presentation.settings.widget.EditTextPreferenceWidget import com.mihon.presentation.settings.widget.InfoWidget import com.mihon.presentation.settings.widget.ListPreferenceWidget @@ -25,7 +31,6 @@ import com.mihon.presentation.settings.widget.SwitchPreferenceWidget import com.mihon.presentation.settings.widget.TextPreferenceWidget import com.mihon.presentation.settings.widget.TitleFontSize import kotlinx.coroutines.launch -import com.mihon.material.BaseSliderItem val LocalPreferenceHighlighted = compositionLocalOf(structuralEqualityPolicy()) { false } val LocalPreferenceMinHeight = compositionLocalOf(structuralEqualityPolicy()) { 56.dp } @@ -78,6 +83,51 @@ internal fun PreferenceItem( }, ) } + + is Preference.PreferenceItem.BasicSwitchPreference -> { + SwitchPreferenceWidget( + title = item.title, + subtitle = item.subtitle, + icon = item.icon, + checked = item.value, + onCheckedChanged = { newValue -> + scope.launch { + item.onValueChanged(newValue) + } + }, + ) + } + + is Preference.PreferenceItem.BasicColorPreference -> { + var isDialogShown by remember { mutableStateOf(false) } + + TextPreferenceWidget( + title = item.title, + subtitle = item.subtitle, + icon = item.icon, + onPreferenceClick = { + isDialogShown = true + }, + widget = { + ColorCircle(item.value, size = 30.dp) + } + ) + + if (isDialogShown) { + ColorDialog( + title = item.title, + color = item.value, + dismiss = { isDialogShown = false }, + confirm = { color -> + isDialogShown = false + scope.launch { + item.onValueChanged(color) + } + } + ) + } + } + is Preference.PreferenceItem.SliderPreference -> { BaseSliderItem( value = item.value, @@ -85,7 +135,8 @@ internal fun PreferenceItem( steps = item.steps, title = item.title, subtitle = item.subtitle, - valueString = item.valueString.takeUnless { it.isNullOrEmpty() } ?: item.value.toString(), + valueString = item.valueString.takeUnless { it.isNullOrEmpty() } + ?: item.value.toString(), onChange = { scope.launch { item.onValueChanged(it) @@ -99,6 +150,7 @@ internal fun PreferenceItem( icon = item.icon ) } + is Preference.PreferenceItem.ListPreference<*> -> { val value by item.preference.collectAsState() ListPreferenceWidget( @@ -117,6 +169,7 @@ internal fun PreferenceItem( }, ) } + is Preference.PreferenceItem.BasicListPreference -> { ListPreferenceWidget( value = item.value, @@ -124,9 +177,16 @@ internal fun PreferenceItem( subtitle = item.subtitleProvider(item.value, item.entries), icon = item.icon, entries = item.entries, - onValueChange = { scope.launch { item.onValueChanged(it) } }, + onValueChange = { newValue -> + scope.launch { + item.internalOnValueChanged( + newValue + ) + } + }, ) } + is Preference.PreferenceItem.MultiSelectListPreference<*> -> { val values by item.preference.collectAsState() MultiSelectListPreferenceWidget( @@ -144,6 +204,7 @@ internal fun PreferenceItem( }, ) } + is Preference.PreferenceItem.TextPreference -> { TextPreferenceWidget( title = item.title, @@ -153,6 +214,7 @@ internal fun PreferenceItem( onPreferenceClick = item.onClick, ) } + is Preference.PreferenceItem.EditTextPreference -> { val values by item.preference.collectAsState() EditTextPreferenceWidget( @@ -180,6 +242,7 @@ internal fun PreferenceItem( is Preference.PreferenceItem.InfoPreference -> { InfoWidget(text = item.title) } + is Preference.PreferenceItem.CustomPreference -> { item.content() } From d2259ed26320928b709ec214a3aacb0b51838a9f Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sun, 13 Sep 2026 22:22:41 +0200 Subject: [PATCH 2/4] Fixed styling issue caused by regex replace --- .../ui/subtitles/SubtitlesFragment.kt | 44 ++++++++----------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt index cd7ba911d74..460b54c8ba1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt @@ -9,6 +9,7 @@ import android.os.Bundle import android.text.Layout import android.text.Spannable import android.text.SpannableString +import android.text.SpannableStringBuilder import android.text.style.StyleSpan import android.util.DisplayMetrics import android.util.TypedValue @@ -169,22 +170,18 @@ class SubtitlesFragment : BaseDialogFragment( // instead we use the inherit size of the subtitle view setTextSize(Cue.DIMEN_UNSET, Cue.TYPE_UNSET) - // 2. apply edge text?.let { text -> - val customSpan = SpannableString.valueOf(text) + val textSpanBuilder = SpannableStringBuilder(text) + + // 2. apply edge if (edgeSize > 0.0f) { - customSpan.setSpan( - OutlineSpan(edgeSize), 0, customSpan.length, + textSpanBuilder.setSpan( + OutlineSpan(edgeSize), 0, textSpanBuilder.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ) } - setText(customSpan) - } - - // 3. apply bold + italic - text?.let { text -> - val customSpan = SpannableString.valueOf(text) + // 3. apply bold + italic val typeface = when (style.bold to style.italic) { (true to true) -> Typeface.BOLD_ITALIC (true to false) -> Typeface.BOLD @@ -196,19 +193,14 @@ class SubtitlesFragment : BaseDialogFragment( } if (typeface != Typeface.NORMAL) { val styleSpan = StyleSpan(typeface) - customSpan.setSpan( - styleSpan, 0, customSpan.length, + textSpanBuilder.setSpan( + styleSpan, 0, textSpanBuilder.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ) } - setText(customSpan) - } - // 4. apply radius - text?.let { text -> - val customSpan = SpannableString.valueOf(text) + // 4. apply radius val radius = style.backgroundRadius ?: 0.0f - if (radius > 0.0f && style.backgroundColor != Color.TRANSPARENT) { val styleSpan = RoundedBackgroundColorSpan( style.backgroundColor, @@ -216,19 +208,21 @@ class SubtitlesFragment : BaseDialogFragment( 2.0F + radius * 0.5f, radius ) - customSpan.setSpan( - styleSpan, 0, customSpan.length, + textSpanBuilder.setSpan( + styleSpan, 0, textSpanBuilder.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ) } - setText(customSpan) - } - // 5. remove captions - text?.let { text -> + // 5. remove captions but ensure that styling is kept + // Just doing .replace removes all styling! if (style.removeCaptions) { - setText(text.replace(captionRegex, "")) + for (match in captionRegex.findAll(text).toList().asReversed()) { + textSpanBuilder.delete(match.range.first, match.range.last + 1) + } } + + this.setText(textSpanBuilder) } // 6. set alignment From b51afc8ccf0812af444597f3da6bee2578318c5a Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sun, 13 Sep 2026 23:03:06 +0200 Subject: [PATCH 3/4] Added undo and reset --- .../ui/subtitles/SubtitlesFragment.kt | 33 ++++---- .../ui/subtitles/SubtitlesScreen.kt | 82 ++++++++++++++++++- .../main/res/drawable/reset_colors_24px.xml | 10 +++ app/src/main/res/drawable/undo_24px.xml | 11 +++ 4 files changed, 118 insertions(+), 18 deletions(-) create mode 100644 app/src/main/res/drawable/reset_colors_24px.xml create mode 100644 app/src/main/res/drawable/undo_24px.xml diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt index 460b54c8ba1..591abde9f65 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt @@ -19,6 +19,7 @@ import android.widget.Toast import androidx.annotation.FontRes import androidx.annotation.OptIn import androidx.annotation.Px +import androidx.compose.runtime.mutableStateOf import androidx.core.content.edit import androidx.core.content.res.ResourcesCompat import androidx.media3.common.text.Cue @@ -271,26 +272,28 @@ class SubtitlesFragment : BaseDialogFragment( } } - private var cachedSubtitleStyle: SaveCaptionStyle? = null - fun Context.saveStyle(style: SaveCaptionStyle) { - cachedSubtitleStyle = style + if(subtitleStyleState.value !== style) { + subtitleStyleState.value = style + } this.setKey(SUBTITLE_KEY, style) } + val defaultSubtitleStyle = SaveCaptionStyle( + foregroundColor = getDefColor(0), + backgroundColor = getDefColor(2), + windowColor = getDefColor(3), + edgeType = CaptionStyleCompat.EDGE_TYPE_OUTLINE, + edgeColor = getDefColor(1), + font = null, + typefaceFilePath = null, + elevation = DEF_SUBS_ELEVATION, + fixedTextSize = null, + ) + val subtitleStyleState = mutableStateOf((getKey(SUBTITLE_KEY) ?: defaultSubtitleStyle)) + fun getCurrentSavedStyle(): SaveCaptionStyle { - return cachedSubtitleStyle ?: (getKey(SUBTITLE_KEY) - ?: SaveCaptionStyle( - foregroundColor = getDefColor(0), - backgroundColor = getDefColor(2), - windowColor = getDefColor(3), - edgeType = CaptionStyleCompat.EDGE_TYPE_OUTLINE, - edgeColor = getDefColor(1), - font = null, - typefaceFilePath = null, - elevation = DEF_SUBS_ELEVATION, - fixedTextSize = null, - )).also { cachedSubtitleStyle = it } + return subtitleStyleState.value } private fun Context.getSavedFonts(): List { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesScreen.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesScreen.kt index ff134f57f4e..f3c26dc06e2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesScreen.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesScreen.kt @@ -2,10 +2,15 @@ package com.lagradost.cloudstream3.ui.subtitles import android.text.SpannableString import androidx.annotation.OptIn +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.SideEffect @@ -31,29 +36,100 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.ui.player.CustomDecoder import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.applyStyle import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.applyStyleEvent +import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.defaultSubtitleStyle import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getCurrentSavedStyle import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.saveStyle import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.setSubtitleViewStyle +import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.subtitleStyleState import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.toJsonLiteral import com.lagradost.cloudstream3.utils.SubtitleHelper.languages +import com.lagradost.cloudstream4.compose.ActionDialog import com.mihon.common.preference.AndroidPreferenceStore import com.mihon.common.preference.DataPreferenceStore import com.mihon.presentation.settings.Preference import com.mihon.presentation.settings.SearchableSettings import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentMapOf -import kotlin.collections.toSet object SubtitlesScreen : SearchableSettings { @Composable override fun getTitleRes(): String = stringResource(R.string.subtitles_settings) + // For ux purposes we also want to show a diff of what will be reset + private val diff = arrayOf Any?>>( + R.string.subs_font to { style -> style.font }, + R.string.subs_text_color to { state -> state.foregroundColor }, + R.string.subs_font_size to { state -> state.fixedTextSize }, + R.string.uppercase_all_subtitles to { state -> state.upperCase }, + R.string.all_subtitles_bold to { state -> state.bold }, + R.string.all_subtitles_italic to { state -> state.italic }, + R.string.subs_edge_type to { state -> state.edgeType }, + R.string.subs_outline_color to { state -> state.edgeColor }, + R.string.subs_edge_size to { state -> state.edgeSize }, + R.string.subs_window_color to { state -> state.windowColor }, + R.string.subs_background_color to { state -> state.backgroundColor }, + R.string.background_radius to { state -> state.backgroundRadius }, + R.string.subs_subtitle_alignment to { state -> state.alignment }, + R.string.subs_subtitle_elevation to { state -> state.elevation }, + R.string.subtitles_remove_captions to { state -> state.removeCaptions }, + R.string.subtitles_remove_bloat to { state -> state.removeBloat }, + ) + + @Composable + override fun RowScope.AppBarAction() { + var dialogShown by remember { mutableStateOf(false) } + val initialState = remember { getCurrentSavedStyle() } + var currentState by subtitleStyleState + + if (dialogShown) { + ActionDialog( + title = stringResource(R.string.subs_default_reset_toast), + text = diff.filter { (_, f) -> f(defaultSubtitleStyle) != f(currentState) } + .map { (name, _) -> stringResource(name) }.joinToString(separator = "\n"), + confirmText = stringResource(R.string.reset_btn), + dismissText = stringResource(R.string.dismiss), + confirm = { + currentState = defaultSubtitleStyle + dialogShown = false + }, + dismiss = { + dialogShown = false + }) + } + + // Undo, current -> initial last saved value + AnimatedVisibility(currentState != initialState) { + IconButton(onClick = { + currentState = initialState + }) { + Icon( + painter = painterResource(R.drawable.undo_24px), + contentDescription = stringResource(R.string.reset_btn), + tint = MaterialTheme.colorScheme.onBackground + ) + } + } + + // Reset, current -> default + AnimatedVisibility(defaultSubtitleStyle != currentState) { + IconButton(onClick = { + dialogShown = true + }) { + Icon( + painter = painterResource(R.drawable.reset_colors_24px), + contentDescription = stringResource(R.string.subs_default_reset_toast), + tint = MaterialTheme.colorScheme.onBackground + ) + } + } + } + @OptIn(UnstableApi::class) @Composable override fun getPreferences(): List { val initialState = remember { getCurrentSavedStyle() } - var state by remember { mutableStateOf(initialState) } + var state by subtitleStyleState val context = LocalContext.current SideEffect(state) { @@ -63,7 +139,7 @@ object SubtitlesScreen : SearchableSettings { DisposableEffect(Unit) { onDispose { - if(initialState != state) { + if (initialState != state) { applyStyleEvent.invoke(state) } } diff --git a/app/src/main/res/drawable/reset_colors_24px.xml b/app/src/main/res/drawable/reset_colors_24px.xml new file mode 100644 index 00000000000..6cfa81413f9 --- /dev/null +++ b/app/src/main/res/drawable/reset_colors_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/undo_24px.xml b/app/src/main/res/drawable/undo_24px.xml new file mode 100644 index 00000000000..89a3f5b801d --- /dev/null +++ b/app/src/main/res/drawable/undo_24px.xml @@ -0,0 +1,11 @@ + + + From de6b4e475cc6c80ed7fbb88bcb63a1c6e10dd027 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Mon, 14 Sep 2026 12:59:48 +0200 Subject: [PATCH 4/4] Cleanup of code + Added Chromecast subtitles settings --- .../ui/settings/SettingsPlayerScreen.kt | 6 +- .../subtitles/ChromecastSubtitlesFragment.kt | 49 +++-- .../subtitles/ChromecastSubtitlesFragment2.kt | 6 + .../ui/subtitles/ChromecastSubtitlesScreen.kt | 177 ++++++++++++++++++ .../ui/subtitles/SubtitlesFragment.kt | 155 +++++++++------ .../ui/subtitles/SubtitlesScreen.kt | 79 +++++--- .../main/res/navigation/mobile_navigation.xml | 2 +- 7 files changed, 363 insertions(+), 111 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment2.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesScreen.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayerScreen.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayerScreen.kt index 309b65cb8b6..744f1bdb5a7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayerScreen.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayerScreen.kt @@ -227,7 +227,7 @@ object SettingsPlayerScreen : SearchableSettings { title = stringResource(R.string.double_tap_to_seek_amount_settings), icon = painterResource(R.drawable.go_forward_30), valueRange = 5..60, - steps = 10, + //steps = 10, onValueChanged = settings.player.doubleTapTime::set ), ) @@ -242,7 +242,7 @@ object SettingsPlayerScreen : SearchableSettings { subtitle = stringResource(R.string.android_tv_interface_on_seek_settings_summary), icon = painterResource(R.drawable.go_forward_30), valueRange = 5..60, - steps = 10, + //steps = 10, onValueChanged = settings.player.tvSeekOnTime::set ), Preference.PreferenceItem.SliderPreference( @@ -251,7 +251,7 @@ object SettingsPlayerScreen : SearchableSettings { subtitle = stringResource(R.string.android_tv_interface_off_seek_settings_summary), icon = painterResource(R.drawable.go_forward_30), valueRange = 5..60, - steps = 10, + //steps = 10, onValueChanged = settings.player.tvSeekOffTime::set ), ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt index ab64e99936d..0e203989fa8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt @@ -11,6 +11,8 @@ import android.view.View import android.widget.TextView import android.widget.Toast import androidx.annotation.OptIn +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.mutableStateOf import androidx.media3.common.text.Cue import androidx.media3.common.util.UnstableApi import com.fasterxml.jackson.annotation.JsonProperty @@ -44,15 +46,16 @@ import kotlinx.serialization.Serializable const val CHROME_SUBTITLE_KEY = "chome_subtitle_settings" @Serializable +@Immutable data class SaveChromeCaptionStyle( - @JsonProperty("fontFamily") @SerialName("fontFamily") var fontFamily: String? = null, - @JsonProperty("fontGenericFamily") @SerialName("fontGenericFamily") var fontGenericFamily: Int? = null, - @JsonProperty("backgroundColor") @SerialName("backgroundColor") var backgroundColor: Int = 0x00FFFFFF, // transparent - @JsonProperty("edgeColor") @SerialName("edgeColor") var edgeColor: Int = Color.BLACK, // BLACK - @JsonProperty("edgeType") @SerialName("edgeType") var edgeType: Int = EDGE_TYPE_OUTLINE, - @JsonProperty("foregroundColor") @SerialName("foregroundColor") var foregroundColor: Int = Color.WHITE, - @JsonProperty("fontScale") @SerialName("fontScale") var fontScale: Float = 1.05f, - @JsonProperty("windowColor") @SerialName("windowColor") var windowColor: Int = Color.TRANSPARENT, + @JsonProperty("fontFamily") @SerialName("fontFamily") val fontFamily: String? = null, + @JsonProperty("fontGenericFamily") @SerialName("fontGenericFamily") val fontGenericFamily: Int? = null, + @JsonProperty("backgroundColor") @SerialName("backgroundColor") val backgroundColor: Int = 0x00FFFFFF, // transparent + @JsonProperty("edgeColor") @SerialName("edgeColor") val edgeColor: Int = Color.BLACK, // BLACK + @JsonProperty("edgeType") @SerialName("edgeType") val edgeType: Int = EDGE_TYPE_OUTLINE, + @JsonProperty("foregroundColor") @SerialName("foregroundColor") val foregroundColor: Int = Color.WHITE, + @JsonProperty("fontScale") @SerialName("fontScale") val fontScale: Float = 1.05f, + @JsonProperty("windowColor") @SerialName("windowColor") val windowColor: Int = Color.TRANSPARENT, ) class ChromecastSubtitlesFragment : BaseFragment( @@ -92,6 +95,9 @@ class ChromecastSubtitlesFragment : BaseFragment(CHROME_SUBTITLE_KEY) ?: defaultState + return chromeCastSubtitleState.value } - private val defaultState = SaveChromeCaptionStyle() + val defaultChromeCastSubtitleState = SaveChromeCaptionStyle() + val chromeCastSubtitleState = mutableStateOf( + getKey(CHROME_SUBTITLE_KEY) ?: defaultChromeCastSubtitleState + ) } private fun onColorSelected(stuff: Pair) { @@ -134,10 +143,10 @@ class ChromecastSubtitlesFragment : BaseFragment state.foregroundColor = realColor - 1 -> state.edgeColor = realColor - 2 -> state.backgroundColor = realColor - 3 -> state.windowColor = realColor + 0 -> state = state.copy(foregroundColor = realColor) + 1 -> state = state.copy(edgeColor = realColor) + 2 -> state = state.copy(backgroundColor = realColor) + 3 -> state = state.copy(windowColor = realColor) else -> Unit } @@ -240,13 +249,13 @@ class ChromecastSubtitlesFragment : BaseFragment - state.edgeType = edgeTypes.map { it.first }[index] + state = state.copy(edgeType = edgeTypes.map { it.first }[index]) updateState() } } binding.subsEdgeType.setOnLongClickListener { - state.edgeType = defaultState.edgeType + state = state.copy(edgeType = defaultChromeCastSubtitleState.edgeType) updateState() showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true @@ -281,13 +290,13 @@ class ChromecastSubtitlesFragment : BaseFragment - state.fontScale = fontSizes.map { it.first }[index] + state = state.copy(fontScale = fontSizes.map { it.first }[index]) //textView.context.updateState() // font size not changed } } binding.subsFontSize.setOnLongClickListener { _ -> - state.fontScale = defaultState.fontScale + state = state.copy(fontScale = defaultChromeCastSubtitleState.fontScale) //textView.context.updateState() // font size not changed showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true @@ -314,12 +323,12 @@ class ChromecastSubtitlesFragment : BaseFragment - state.fontFamily = fontTypes.map { it.first }[index] + state = state.copy(fontFamily = fontTypes.map { it.first }[index]) updateState() } } binding.subsFont.setOnLongClickListener { _ -> - state.fontFamily = defaultState.fontFamily + state = state.copy(fontFamily = defaultChromeCastSubtitleState.fontFamily) updateState() showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment2.kt new file mode 100644 index 00000000000..a25a8aa8864 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment2.kt @@ -0,0 +1,6 @@ +package com.lagradost.cloudstream3.ui.subtitles + +import com.lagradost.cloudstream3.utils.BaseComposeFragment +import com.lagradost.cloudstream4.compose.Screen + +class ChromecastSubtitlesFragment2 : BaseComposeFragment(), Screen by ChromecastSubtitlesScreen \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesScreen.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesScreen.kt new file mode 100644 index 00000000000..da27be05e1a --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesScreen.kt @@ -0,0 +1,177 @@ +package com.lagradost.cloudstream3.ui.subtitles + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_DEPRESSED +import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_DROP_SHADOW +import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_NONE +import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_OUTLINE +import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_RAISED +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.ui.subtitles.ChromecastSubtitlesFragment.Companion.applyStyleEvent +import com.lagradost.cloudstream3.ui.subtitles.ChromecastSubtitlesFragment.Companion.chromeCastSubtitleState +import com.lagradost.cloudstream3.ui.subtitles.ChromecastSubtitlesFragment.Companion.defaultChromeCastSubtitleState +import com.lagradost.cloudstream3.ui.subtitles.ChromecastSubtitlesFragment.Companion.saveStyle +import com.lagradost.cloudstream3.ui.subtitles.SubtitlesScreen.SetUpUndoBar +import com.mihon.presentation.settings.Preference +import com.mihon.presentation.settings.SearchableSettings +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentMapOf +import kotlin.math.roundToInt + +object ChromecastSubtitlesScreen : SearchableSettings { + @Composable + override fun getTitleRes(): String = stringResource(R.string.chromecast_subtitles_settings) + + // For ux purposes we also want to show a diff of what will be reset + private val diff = arrayOf Any?>>( + R.string.subs_font to { style -> style.fontFamily }, + R.string.subs_text_color to { state -> state.foregroundColor }, + R.string.subs_font_size to { state -> state.fontScale }, + R.string.subs_edge_type to { state -> state.edgeType }, + R.string.subs_outline_color to { state -> state.edgeColor }, + R.string.subs_window_color to { state -> state.windowColor }, + R.string.subs_background_color to { state -> state.backgroundColor }, + ) + + @Composable + override fun RowScope.AppBarAction() { + val initialState = remember { chromeCastSubtitleState.value } + SetUpUndoBar( + diff = diff, + initialState = initialState, + currentState = chromeCastSubtitleState, + default = defaultChromeCastSubtitleState + ) + } + + @Composable + override fun getPreferences(): List { + val initialState = remember { chromeCastSubtitleState.value } + var state by remember { chromeCastSubtitleState } + + val context = LocalContext.current + + SideEffect(state) { + // When we modify the state, we modify the stored data + context.saveStyle(state) + } + + DisposableEffect(Unit) { + onDispose { + if (initialState != state) { + applyStyleEvent.invoke(state) + } + } + } + return getPreferences(state) { updater -> + state = updater(state) + } + } + + /** Unfortunately this causes a lot of re-compositions, but what can you do? */ + @Composable + fun getPreferences( + state : SaveChromeCaptionStyle, + update : (SaveChromeCaptionStyle.() -> SaveChromeCaptionStyle) -> Unit + ): List { + return persistentListOf( + Preference.PreferenceGroup( + title = stringResource(R.string.subs_font), + preferenceItems = persistentListOf( + Preference.PreferenceItem.BasicListPreference( + icon = painterResource(R.drawable.font_download_24px), + title = stringResource(R.string.subs_font), + value = state.fontFamily, + entries = persistentMapOf( + null to stringResource(R.string.normal), + "Droid Sans" to "Droid Sans", + "Droid Sans Mono" to "Droid Sans Mono", + "Droid Serif Regular" to "Droid Serif Regular", + "Cutive Mono" to "Cutive Mono", + "Short Stack" to "Short Stack", + "Quintessential" to "Quintessential", + "Alegreya Sans SC" to "Alegreya Sans SC", + ), + onValueChanged = { newValue -> + update { copy(fontFamily = newValue) } + }), + Preference.PreferenceItem.BasicColorPreference( + title = stringResource(R.string.subs_text_color), + value = Color(state.foregroundColor), + icon = painterResource(R.drawable.format_color_text_24px), + onValueChanged = { newValue -> + update { copy(foregroundColor = newValue.toArgb()) } + } + ), + Preference.PreferenceItem.SliderPreference( + value = state.fontScale.times(100.0f).roundToInt(), + icon = painterResource(R.drawable.text_fields_24px), + title = stringResource(R.string.subs_font_size), + valueRange = 75..150, + steps = 75/5 - 1, + onValueChanged = { newValue -> + update { copy(fontScale = newValue.toFloat() * 0.01f) } + }) + ) + ), + Preference.PreferenceGroup( + title = stringResource(R.string.subs_edge), + preferenceItems = persistentListOf( + Preference.PreferenceItem.BasicListPreference( + icon = painterResource(R.drawable.shadow_24px), + title = stringResource(R.string.subs_edge_type), + value = state.edgeType, + entries = persistentMapOf( + EDGE_TYPE_NONE to stringResource(R.string.subtitles_none), + EDGE_TYPE_OUTLINE to stringResource(R.string.subtitles_outline), + EDGE_TYPE_DEPRESSED to stringResource(R.string.subtitles_depressed), + EDGE_TYPE_DROP_SHADOW to stringResource(R.string.subtitles_shadow), + EDGE_TYPE_RAISED to stringResource(R.string.subtitles_raised), + ), + onValueChanged = { newValue -> + update { copy(edgeType = newValue) } + }), + Preference.PreferenceItem.BasicColorPreference( + enabled = state.edgeType != EDGE_TYPE_NONE, + title = stringResource(R.string.subs_outline_color), + value = Color(state.edgeColor), + icon = painterResource(R.drawable.border_color_24px), + onValueChanged = { newValue -> + update { copy(edgeColor = newValue.toArgb()) } + } + ), + )), + Preference.PreferenceGroup( + title = stringResource(R.string.background), + preferenceItems = persistentListOf( + Preference.PreferenceItem.BasicColorPreference( + title = stringResource(R.string.subs_window_color), + value = Color(state.windowColor), + icon = painterResource(R.drawable.imagesearch_roller_24px), + onValueChanged = { newValue -> + update { copy(windowColor = newValue.toArgb()) } + } + ), + Preference.PreferenceItem.BasicColorPreference( + title = stringResource(R.string.subs_background_color), + value = Color(state.backgroundColor), + icon = painterResource(R.drawable.format_color_fill_24px), + onValueChanged = { newValue -> + update { copy(backgroundColor = newValue.toArgb()) } + } + ), + )) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt index 591abde9f65..757d8a94033 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt @@ -19,6 +19,7 @@ import android.widget.Toast import androidx.annotation.FontRes import androidx.annotation.OptIn import androidx.annotation.Px +import androidx.compose.runtime.Immutable import androidx.compose.runtime.mutableStateOf import androidx.core.content.edit import androidx.core.content.res.ResourcesCompat @@ -69,47 +70,76 @@ enum class SubtitleFont( @FontRes val resource: Int, val label: String ) { - @JsonProperty("Trebuchet") @SerialName("Trebuchet") Trebuchet(R.font.trebuchet_ms, "Trebuchet MS"), - @JsonProperty("Netflix") @SerialName("Netflix") Netflix(R.font.netflix_sans, "Netflix Sans"), - @JsonProperty("Google") @SerialName("Google") Google(R.font.google_sans, "Google Sans"), - @JsonProperty("Open") @SerialName("Open") Open(R.font.open_sans, "Open Sans"), - @JsonProperty("Futura") @SerialName("Futura") Futura(R.font.futura, "Futura"), - @JsonProperty("Consola") @SerialName("Consola") Consola(R.font.consola, "Consola"), - @JsonProperty("Gotham") @SerialName("Gotham") Gotham(R.font.gotham, "Gotham"), - @JsonProperty("Lucida") @SerialName("Lucida") Lucida(R.font.lucida_grande, "Lucida Grande"), - @JsonProperty("STIX") @SerialName("STIX") STIX(R.font.stix_general, "STIX General"), - @JsonProperty("TimesNewRoman") @SerialName("TimesNewRoman") TimesNewRoman(R.font.times_new_roman, "Times New Roman"), - @JsonProperty("Verdana") @SerialName("Verdana") Verdana(R.font.verdana, "Verdana"), - @JsonProperty("Ubuntu") @SerialName("Ubuntu") Ubuntu(R.font.ubuntu_regular, "Ubuntu"), - @JsonProperty("Comic") @SerialName("Comic") Comic(R.font.comic_sans, "Comic Sans"), - @JsonProperty("Poppins") @SerialName("Poppins") Poppins(R.font.poppins_regular, "Poppins"), + @JsonProperty("Trebuchet") + @SerialName("Trebuchet") + Trebuchet(R.font.trebuchet_ms, "Trebuchet MS"), + @JsonProperty("Netflix") + @SerialName("Netflix") + Netflix(R.font.netflix_sans, "Netflix Sans"), + @JsonProperty("Google") + @SerialName("Google") + Google(R.font.google_sans, "Google Sans"), + @JsonProperty("Open") + @SerialName("Open") + Open(R.font.open_sans, "Open Sans"), + @JsonProperty("Futura") + @SerialName("Futura") + Futura(R.font.futura, "Futura"), + @JsonProperty("Consola") + @SerialName("Consola") + Consola(R.font.consola, "Consola"), + @JsonProperty("Gotham") + @SerialName("Gotham") + Gotham(R.font.gotham, "Gotham"), + @JsonProperty("Lucida") + @SerialName("Lucida") + Lucida(R.font.lucida_grande, "Lucida Grande"), + @JsonProperty("STIX") + @SerialName("STIX") + STIX(R.font.stix_general, "STIX General"), + @JsonProperty("TimesNewRoman") + @SerialName("TimesNewRoman") + TimesNewRoman(R.font.times_new_roman, "Times New Roman"), + @JsonProperty("Verdana") + @SerialName("Verdana") + Verdana(R.font.verdana, "Verdana"), + @JsonProperty("Ubuntu") + @SerialName("Ubuntu") + Ubuntu(R.font.ubuntu_regular, "Ubuntu"), + @JsonProperty("Comic") + @SerialName("Comic") + Comic(R.font.comic_sans, "Comic Sans"), + @JsonProperty("Poppins") + @SerialName("Poppins") + Poppins(R.font.poppins_regular, "Poppins"), } @Serializable +@Immutable data class SaveCaptionStyle( - @JsonProperty("foregroundColor") @SerialName("foregroundColor") var foregroundColor: Int, - @JsonProperty("backgroundColor") @SerialName("backgroundColor") var backgroundColor: Int, - @JsonProperty("windowColor") @SerialName("windowColor") var windowColor: Int, + @JsonProperty("foregroundColor") @SerialName("foregroundColor") val foregroundColor: Int, + @JsonProperty("backgroundColor") @SerialName("backgroundColor") val backgroundColor: Int, + @JsonProperty("windowColor") @SerialName("windowColor") val windowColor: Int, @OptIn(UnstableApi::class) - @JsonProperty("edgeType") @SerialName("edgeType") var edgeType: @CaptionStyleCompat.EdgeType Int, - @JsonProperty("edgeColor") @SerialName("edgeColor") var edgeColor: Int, - @JsonProperty("font") @SerialName("font") var font: SubtitleFont? = null, - @JsonProperty("typefaceFilePath") @SerialName("typefaceFilePath") var typefaceFilePath: String?, - @JsonProperty("elevation") @SerialName("elevation") var elevation: Int, // in dp - @JsonProperty("fixedTextSize") @SerialName("fixedTextSize") var fixedTextSize: Float?, // in sp - @Px @JsonProperty("edgeSize") @SerialName("edgeSize") var edgeSize: Float? = null, - @JsonProperty("removeCaptions") @SerialName("removeCaptions") var removeCaptions: Boolean = false, - @JsonProperty("removeBloat") @SerialName("removeBloat") var removeBloat: Boolean = true, + @JsonProperty("edgeType") @SerialName("edgeType") val edgeType: @CaptionStyleCompat.EdgeType Int, + @JsonProperty("edgeColor") @SerialName("edgeColor") val edgeColor: Int, + @JsonProperty("font") @SerialName("font") val font: SubtitleFont? = null, + @JsonProperty("typefaceFilePath") @SerialName("typefaceFilePath") val typefaceFilePath: String?, + @JsonProperty("elevation") @SerialName("elevation") val elevation: Int, // in dp + @JsonProperty("fixedTextSize") @SerialName("fixedTextSize") val fixedTextSize: Float?, // in sp + @Px @JsonProperty("edgeSize") @SerialName("edgeSize") val edgeSize: Float? = null, + @JsonProperty("removeCaptions") @SerialName("removeCaptions") val removeCaptions: Boolean = false, + @JsonProperty("removeBloat") @SerialName("removeBloat") val removeBloat: Boolean = true, /** Apply caps lock to the text */ - @JsonProperty("upperCase") @SerialName("upperCase") var upperCase: Boolean = false, + @JsonProperty("upperCase") @SerialName("upperCase") val upperCase: Boolean = false, /** Apply bold to the text */ - @JsonProperty("bold") @SerialName("bold") var bold: Boolean = false, + @JsonProperty("bold") @SerialName("bold") val bold: Boolean = false, /** Apply italic to the text */ - @JsonProperty("italic") @SerialName("italic") var italic: Boolean = false, + @JsonProperty("italic") @SerialName("italic") val italic: Boolean = false, /** in px, background radius, aka how round the background (backgroundColor) on each row is */ - @JsonProperty("backgroundRadius") @SerialName("backgroundRadius") var backgroundRadius: Float? = null, + @JsonProperty("backgroundRadius") @SerialName("backgroundRadius") val backgroundRadius: Float? = null, /** The SSA_ALIGNMENT */ - @JsonProperty("alignment") @SerialName("alignment") var alignment: Int? = null, + @JsonProperty("alignment") @SerialName("alignment") val alignment: Int? = null, ) const val DEF_SUBS_ELEVATION = 20 @@ -273,7 +303,7 @@ class SubtitlesFragment : BaseDialogFragment( } fun Context.saveStyle(style: SaveCaptionStyle) { - if(subtitleStyleState.value !== style) { + if (subtitleStyleState.value !== style) { subtitleStyleState.value = style } this.setKey(SUBTITLE_KEY, style) @@ -290,7 +320,8 @@ class SubtitlesFragment : BaseDialogFragment( elevation = DEF_SUBS_ELEVATION, fixedTextSize = null, ) - val subtitleStyleState = mutableStateOf((getKey(SUBTITLE_KEY) ?: defaultSubtitleStyle)) + val subtitleStyleState = + mutableStateOf((getKey(SUBTITLE_KEY) ?: defaultSubtitleStyle)) fun getCurrentSavedStyle(): SaveCaptionStyle { return subtitleStyleState.value @@ -337,10 +368,10 @@ class SubtitlesFragment : BaseDialogFragment( private fun Context.setColor(id: Int, color: Int?) { val realColor = color ?: getDefColor(id) when (id) { - 0 -> state.foregroundColor = realColor - 1 -> state.edgeColor = realColor - 2 -> state.backgroundColor = realColor - 3 -> state.windowColor = realColor + 0 -> state = state.copy(foregroundColor = realColor) + 1 -> state = state.copy(edgeColor = realColor) + 2 -> state = state.copy(backgroundColor = realColor) + 3 -> state = state.copy(windowColor = realColor) else -> Unit } @@ -466,7 +497,7 @@ class SubtitlesFragment : BaseDialogFragment( false, dismissCallback ) { index -> - state.elevation = elevationTypes.map { it.first }[index] + state = state.copy(elevation = elevationTypes.map { it.first }[index]) textView.context.updateState() if (hide) activity?.hideSystemUI() @@ -474,7 +505,7 @@ class SubtitlesFragment : BaseDialogFragment( } subsSubtitleElevation.setOnLongClickListener { - state.elevation = DEF_SUBS_ELEVATION + state = state.copy(elevation = DEF_SUBS_ELEVATION) it.context.updateState() showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true @@ -497,13 +528,14 @@ class SubtitlesFragment : BaseDialogFragment( false, dismissCallback ) { index -> - state.backgroundRadius = radiusTypes.map { it.first }[index]?.toFloat() + state = + state.copy(backgroundRadius = radiusTypes.map { it.first }[index]?.toFloat()) textView.context.updateState() } } subsBackgroundRadius.setOnLongClickListener { - state.backgroundRadius = null + state = state.copy(backgroundRadius = null) it.context.updateState() showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true @@ -531,7 +563,7 @@ class SubtitlesFragment : BaseDialogFragment( false, dismissCallback ) { index -> - state.alignment = alignmentTypes.map { it.first }[index] + state = state.copy(alignment = alignmentTypes.map { it.first }[index]) textView.context.updateState() } } @@ -559,13 +591,13 @@ class SubtitlesFragment : BaseDialogFragment( false, dismissCallback ) { index -> - state.edgeType = edgeTypes.map { it.first }[index] + state = state.copy(edgeType = edgeTypes.map { it.first }[index]) textView.context.updateState() } } subsEdgeType.setOnLongClickListener { - state.edgeType = CaptionStyleCompat.EDGE_TYPE_OUTLINE + state = state.copy(edgeType = CaptionStyleCompat.EDGE_TYPE_OUTLINE) it.context.updateState() showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true @@ -585,7 +617,7 @@ class SubtitlesFragment : BaseDialogFragment( false, dismissCallback ) { index -> - state.fixedTextSize = fontSizes.map { it.first }[index] + state = state.copy(fixedTextSize = fontSizes.map { it.first }[index]) textView.context.updateState() } } @@ -604,47 +636,47 @@ class SubtitlesFragment : BaseDialogFragment( false, dismissCallback ) { index -> - state.edgeSize = fontSizes.map { it.first }[index] + state = state.copy(edgeSize = fontSizes.map { it.first }[index]) textView.context.updateState() } } subtitlesRemoveBloat.isChecked = state.removeBloat subtitlesRemoveBloat.setOnCheckedChangeListener { _, b -> - state.removeBloat = b + state = state.copy(removeBloat = b) } subtitlesUppercase.isChecked = state.upperCase subtitlesUppercase.setOnCheckedChangeListener { _, b -> - state.upperCase = b + state = state.copy(upperCase = b) context?.updateState() } subtitlesRemoveCaptions.isChecked = state.removeCaptions subtitlesRemoveCaptions.setOnCheckedChangeListener { _, b -> - state.removeCaptions = b + state = state.copy(removeCaptions = b) } subtitlesBold.isChecked = state.bold subtitlesBold.setOnCheckedChangeListener { _, b -> - state.bold = b + state = state.copy(bold = b) context?.updateState() } subtitlesItalic.isChecked = state.italic subtitlesItalic.setOnCheckedChangeListener { _, b -> - state.italic = b + state = state.copy(italic = b) context?.updateState() } subsFontSize.setOnLongClickListener { _ -> - state.fixedTextSize = null + state = state.copy(fixedTextSize = null) context?.updateState() showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true } subsEdgeSize.setOnLongClickListener { _ -> - state.edgeSize = null + state = state.copy(edgeSize = null) context?.updateState() showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true @@ -702,20 +734,23 @@ class SubtitlesFragment : BaseDialogFragment( false, dismissCallback ) { index -> - if (index < fontTypes.size) { - state.font = SubtitleFont.entries.firstOrNull { it.resource == fontTypes[index].first } - state.typefaceFilePath = null + state = if (index < fontTypes.size) { + state.copy( + font = SubtitleFont.entries.firstOrNull { it.resource == fontTypes[index].first }, + typefaceFilePath = null + ) } else { - state.typefaceFilePath = savedFontTypes[index - fontTypes.size].absolutePath - state.font = null + state.copy( + typefaceFilePath = savedFontTypes[index - fontTypes.size].absolutePath, + font = null + ) } textView.context.updateState() } } subsFont.setOnLongClickListener { textView -> - state.font = null - state.typefaceFilePath = null + state = state.copy(font = null, typefaceFilePath = null) textView.context.updateState() showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesScreen.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesScreen.kt index f3c26dc06e2..4c62d7b821d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesScreen.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.MutableState import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -37,7 +38,6 @@ import com.lagradost.cloudstream3.ui.player.CustomDecoder import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.applyStyle import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.applyStyleEvent import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.defaultSubtitleStyle -import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getCurrentSavedStyle import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.saveStyle import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.setSubtitleViewStyle import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.subtitleStyleState @@ -77,20 +77,23 @@ object SubtitlesScreen : SearchableSettings { ) @Composable - override fun RowScope.AppBarAction() { + fun SetUpUndoBar( + diff: Array Any?>>, + initialState: T, + currentState: MutableState, + default: T + ) { + var currentState by currentState var dialogShown by remember { mutableStateOf(false) } - val initialState = remember { getCurrentSavedStyle() } - var currentState by subtitleStyleState - if (dialogShown) { ActionDialog( title = stringResource(R.string.subs_default_reset_toast), - text = diff.filter { (_, f) -> f(defaultSubtitleStyle) != f(currentState) } + text = diff.filter { (_, f) -> f(default) != f(currentState) } .map { (name, _) -> stringResource(name) }.joinToString(separator = "\n"), confirmText = stringResource(R.string.reset_btn), dismissText = stringResource(R.string.dismiss), confirm = { - currentState = defaultSubtitleStyle + currentState = default dialogShown = false }, dismiss = { @@ -112,7 +115,7 @@ object SubtitlesScreen : SearchableSettings { } // Reset, current -> default - AnimatedVisibility(defaultSubtitleStyle != currentState) { + AnimatedVisibility(currentState != default) { IconButton(onClick = { dialogShown = true }) { @@ -125,10 +128,21 @@ object SubtitlesScreen : SearchableSettings { } } + @Composable + override fun RowScope.AppBarAction() { + val initialState = remember { subtitleStyleState.value } + SetUpUndoBar( + diff = diff, + initialState = initialState, + currentState = subtitleStyleState, + default = defaultSubtitleStyle + ) + } + @OptIn(UnstableApi::class) @Composable override fun getPreferences(): List { - val initialState = remember { getCurrentSavedStyle() } + val initialState = remember { subtitleStyleState.value } var state by subtitleStyleState val context = LocalContext.current @@ -145,11 +159,24 @@ object SubtitlesScreen : SearchableSettings { } } + return getPreferences(state) { updater -> + state = updater(state) + } + } + + /** Unfortunately this causes a lot of re-compositions, but what can you do? */ + @OptIn(UnstableApi::class) + @Composable + fun getPreferences( + state: SaveCaptionStyle, + update: (SaveCaptionStyle.() -> SaveCaptionStyle) -> Unit + ): List { val text = stringResource(R.string.subtitles_example_text) val fixedText = SpannableString.valueOf(if (state.upperCase) text.uppercase() else text) val dataStore = DataPreferenceStore(LocalContext.current) val androidStore = AndroidPreferenceStore(LocalContext.current) + val autoSelectSubtitles = dataStore.getString(SUBTITLE_AUTO_SELECT_KEY, "en") val downloadSubsLanguage = dataStore.getObjectFromString(SUBTITLE_DOWNLOAD_KEY, setOf("en"), serializer = { obj -> @@ -202,14 +229,14 @@ object SubtitlesScreen : SearchableSettings { value = state.font, entries = mapOf(null to stringResource(R.string.normal)) + SubtitleFont.entries.associateWith { it.label }, onValueChanged = { newValue -> - state = state.copy(font = newValue) + update { copy(font = newValue) } }), Preference.PreferenceItem.BasicColorPreference( title = stringResource(R.string.subs_text_color), value = Color(state.foregroundColor), icon = painterResource(R.drawable.format_color_text_24px), onValueChanged = { newValue -> - state = state.copy(foregroundColor = newValue.toArgb()) + update { copy(foregroundColor = newValue.toArgb()) } } ), Preference.PreferenceItem.SliderPreference( @@ -218,7 +245,7 @@ object SubtitlesScreen : SearchableSettings { title = stringResource(R.string.subs_font_size), valueRange = 5..60, onValueChanged = { newValue -> - state = state.copy(fixedTextSize = newValue.toFloat()) + update { copy(fixedTextSize = newValue.toFloat()) } }), Preference.PreferenceItem.BasicSwitchPreference( @@ -226,7 +253,7 @@ object SubtitlesScreen : SearchableSettings { icon = painterResource(R.drawable.uppercase_24px), title = stringResource(R.string.uppercase_all_subtitles), onValueChanged = { newValue -> - state = state.copy(upperCase = newValue) + update { copy(upperCase = newValue) } }, ), Preference.PreferenceItem.BasicSwitchPreference( @@ -234,7 +261,7 @@ object SubtitlesScreen : SearchableSettings { icon = painterResource(R.drawable.format_bold_24px), title = stringResource(R.string.all_subtitles_bold), onValueChanged = { newValue -> - state = state.copy(bold = newValue) + update { copy(bold = newValue) } }, ), Preference.PreferenceItem.BasicSwitchPreference( @@ -242,7 +269,7 @@ object SubtitlesScreen : SearchableSettings { icon = painterResource(R.drawable.format_italic_24px), title = stringResource(R.string.all_subtitles_italic), onValueChanged = { newValue -> - state = state.copy(italic = newValue) + update { copy(italic = newValue) } }, ), ) @@ -262,7 +289,7 @@ object SubtitlesScreen : SearchableSettings { CaptionStyleCompat.EDGE_TYPE_RAISED to stringResource(R.string.subtitles_raised), ), onValueChanged = { newValue -> - state = state.copy(edgeType = newValue) + update { copy(edgeType = newValue) } }), Preference.PreferenceItem.BasicColorPreference( enabled = state.edgeType != CaptionStyleCompat.EDGE_TYPE_NONE, @@ -270,7 +297,7 @@ object SubtitlesScreen : SearchableSettings { value = Color(state.edgeColor), icon = painterResource(R.drawable.border_color_24px), onValueChanged = { newValue -> - state = state.copy(edgeColor = newValue.toArgb()) + update { copy(edgeColor = newValue.toArgb()) } } ), Preference.PreferenceItem.SliderPreference( @@ -280,7 +307,7 @@ object SubtitlesScreen : SearchableSettings { title = stringResource(R.string.subs_edge_size), valueRange = 0..60, onValueChanged = { newValue -> - state = state.copy(edgeSize = newValue.toFloat()) + update { copy(edgeSize = newValue.toFloat()) } }), ) ), @@ -292,7 +319,7 @@ object SubtitlesScreen : SearchableSettings { value = Color(state.windowColor), icon = painterResource(R.drawable.imagesearch_roller_24px), onValueChanged = { newValue -> - state = state.copy(windowColor = newValue.toArgb()) + update { copy(windowColor = newValue.toArgb()) } } ), Preference.PreferenceItem.BasicColorPreference( @@ -300,7 +327,7 @@ object SubtitlesScreen : SearchableSettings { value = Color(state.backgroundColor), icon = painterResource(R.drawable.format_color_fill_24px), onValueChanged = { newValue -> - state = state.copy(backgroundColor = newValue.toArgb()) + update { copy(backgroundColor = newValue.toArgb()) } } ), Preference.PreferenceItem.SliderPreference( @@ -310,7 +337,7 @@ object SubtitlesScreen : SearchableSettings { valueRange = 0..50, steps = 9, onValueChanged = { newValue -> - state = state.copy(backgroundRadius = newValue.toFloat()) + update { copy(backgroundRadius = newValue.toFloat()) } }), ) ), @@ -334,7 +361,7 @@ object SubtitlesScreen : SearchableSettings { CustomDecoder.SSA_ALIGNMENT_TOP_RIGHT to stringResource(R.string.top_right), ), onValueChanged = { newValue -> - state = state.copy(alignment = newValue) + update { copy(alignment = newValue) } }), Preference.PreferenceItem.SliderPreference( value = state.elevation, @@ -343,11 +370,10 @@ object SubtitlesScreen : SearchableSettings { valueRange = 0..400, steps = 39, onValueChanged = { newValue -> - state = state.copy(elevation = newValue) + update { copy(elevation = newValue) } }), ) ), - Preference.PreferenceGroup( title = stringResource(R.string.extension_language), preferenceItems = persistentListOf( @@ -378,7 +404,6 @@ object SubtitlesScreen : SearchableSettings { ), ) ), - Preference.PreferenceGroup( title = stringResource(R.string.pref_category_ui_features), preferenceItems = persistentListOf( @@ -388,7 +413,7 @@ object SubtitlesScreen : SearchableSettings { title = stringResource(R.string.subtitles_remove_captions), subtitle = "[Knocking on door] Hello → Hello", onValueChanged = { newValue -> - state = state.copy(removeCaptions = newValue) + update { copy(removeCaptions = newValue) } }, ), Preference.PreferenceItem.BasicSwitchPreference( @@ -396,7 +421,7 @@ object SubtitlesScreen : SearchableSettings { icon = painterResource(R.drawable.text_ad_off_24px), title = stringResource(R.string.subtitles_remove_bloat), onValueChanged = { newValue -> - state = state.copy(removeBloat = newValue) + update { copy(removeBloat = newValue) } }, ), ) diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index e6691e91e17..a65e3706dbd 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -427,7 +427,7 @@