diff --git a/app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt b/app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt index feb0067e02..1a96cc1e48 100644 --- a/app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt @@ -17,6 +17,7 @@ import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.outlined.QueryStats import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Storage +import androidx.compose.material.icons.outlined.VideoSettings import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -51,6 +52,7 @@ fun MoreScreen( onClickStats: () -> Unit, onClickStorage: () -> Unit, onClickDataAndStorage: () -> Unit, + onClickPlayerSettings: () -> Unit, onClickSettings: () -> Unit, onClickAbout: () -> Unit, ) { @@ -178,6 +180,13 @@ fun MoreScreen( onPreferenceClick = onClickSettings, ) } + item { + TextPreferenceWidget( + title = stringResource(MR.strings.label_player_settings), + icon = Icons.Outlined.VideoSettings, + onPreferenceClick = onClickPlayerSettings, + ) + } item { TextPreferenceWidget( title = stringResource(MR.strings.pref_category_about), diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/Preference.kt b/app/src/main/java/eu/kanade/presentation/more/settings/Preference.kt index 7846959a49..d6b404ae33 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/Preference.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/Preference.kt @@ -7,6 +7,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.graphics.vector.ImageVector import eu.kanade.core.preference.asState +import eu.kanade.presentation.more.settings.Preference.PreferenceItem import eu.kanade.tachiyomi.data.track.Tracker import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableMap @@ -199,6 +200,20 @@ sealed class Preference { val canBeBlank: Boolean = true, ) : PreferenceItem() + /** + * A [PreferenceItem] that shows a EditText with a subtitle in the dialog. + * Unlike [EditTextPreference], empty values can be set and a subtitle in the dialog can be show. + */ + data class EditTextInfoPreference( + val pref: PreferenceData, + val dialogSubtitle: String?, + override val title: String, + override val subtitle: String? = "%s", + override val icon: ImageVector? = null, + override val enabled: Boolean = true, + override val onValueChanged: suspend (newValue: String) -> Boolean = { true }, + ) : PreferenceItem() + /** * A [PreferenceItem] for individual tracker. */ diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceItem.kt b/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceItem.kt index 54651d6342..6e79956636 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceItem.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceItem.kt @@ -185,6 +185,23 @@ internal fun PreferenceItem( canBeBlank = item.canBeBlank, ) } + is Preference.PreferenceItem.EditTextInfoPreference -> { + val values by item.pref.collectAsState() + EditTextPreferenceWidget( + title = item.title, + subtitle = item.subtitle, + dialogSubtitle = item.dialogSubtitle, + icon = item.icon, + value = values, + onConfirm = { + val accepted = item.onValueChanged(it) + if (accepted) item.pref.set(it) + accepted + }, + singleLine = true, + canBeBlank = true, + ) + } is Preference.PreferenceItem.TrackerPreference -> { val isLoggedIn by item.tracker.let { tracker -> tracker.isLoggedInFlow.collectAsState(tracker.isLoggedIn) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsMainScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsMainScreen.kt index fe08f1985d..44b585995e 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsMainScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsMainScreen.kt @@ -15,7 +15,6 @@ import androidx.compose.material.icons.outlined.Explore import androidx.compose.material.icons.outlined.GetApp import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.outlined.Palette -import androidx.compose.material.icons.outlined.PlayCircleOutline import androidx.compose.material.icons.outlined.Search import androidx.compose.material.icons.outlined.Security import androidx.compose.material.icons.outlined.Storage @@ -186,12 +185,6 @@ object SettingsMainScreen : Screen() { icon = Icons.Outlined.CollectionsBookmark, screen = SettingsLibraryScreen, ), - Item( - titleRes = MR.strings.pref_category_player, - subtitleRes = MR.strings.pref_player_summary, - icon = Icons.Outlined.PlayCircleOutline, - screen = SettingsPlayerScreen, - ), Item( titleRes = MR.strings.pref_category_reader, subtitleRes = MR.strings.pref_reader_summary, diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSearchScreen.kt index 8a74ac531b..62b8522cb5 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSearchScreen.kt @@ -50,6 +50,12 @@ import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import eu.kanade.presentation.components.UpIcon import eu.kanade.presentation.more.settings.Preference +import eu.kanade.presentation.more.settings.screen.player.PlayerSettingsAdvancedScreen +import eu.kanade.presentation.more.settings.screen.player.PlayerSettingsAudioScreen +import eu.kanade.presentation.more.settings.screen.player.PlayerSettingsDecoderScreen +import eu.kanade.presentation.more.settings.screen.player.PlayerSettingsGesturesScreen +import eu.kanade.presentation.more.settings.screen.player.PlayerSettingsPlayerScreen +import eu.kanade.presentation.more.settings.screen.player.PlayerSettingsSubtitleScreen import eu.kanade.presentation.util.Screen import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.Scaffold @@ -58,7 +64,9 @@ import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.util.runOnEnterKeyPressed import cafe.adriel.voyager.core.screen.Screen as VoyagerScreen -class SettingsSearchScreen : Screen() { +class SettingsSearchScreen( + val isPlayer: Boolean = false, +) : Screen() { @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow @@ -115,7 +123,13 @@ class SettingsSearchScreen : Screen() { decorator = { if (textFieldState.text.isEmpty()) { Text( - text = stringResource(MR.strings.action_search_settings), + text = stringResource( + resource = if (isPlayer) { + MR.strings.action_search_player_settings + } else { + MR.strings.action_search_settings + }, + ), color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyLarge, ) @@ -142,6 +156,7 @@ class SettingsSearchScreen : Screen() { ) { contentPadding -> SearchResult( searchKey = textFieldState.text.toString(), + isPlayer = isPlayer, listState = listState, contentPadding = contentPadding, ) { result -> @@ -155,6 +170,7 @@ class SettingsSearchScreen : Screen() { @Composable private fun SearchResult( searchKey: String, + isPlayer: Boolean, modifier: Modifier = Modifier, listState: LazyListState = rememberLazyListState(), contentPadding: PaddingValues = PaddingValues(), @@ -164,7 +180,7 @@ private fun SearchResult( val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr - val index = getIndex() + val index = if (isPlayer) getPlayerIndex() else getIndex() val result by produceState?>(initialValue = null, searchKey) { value = index.asSequence() .flatMap { settingsData -> @@ -271,6 +287,17 @@ private fun getIndex() = settingScreens ) } +@Composable +@NonRestartableComposable +private fun getPlayerIndex() = playerSettingScreens + .map { screen -> + SettingsData( + title = stringResource(screen.getTitleRes()), + route = screen, + contents = screen.getPreferences(), + ) + } + private fun getLocalizedBreadcrumb(path: String, node: String?, isLtr: Boolean): String { return if (node == null) { path @@ -285,11 +312,19 @@ private fun getLocalizedBreadcrumb(path: String, node: String?, isLtr: Boolean): } } +private val playerSettingScreens = listOf( + PlayerSettingsPlayerScreen, + PlayerSettingsGesturesScreen, + PlayerSettingsDecoderScreen, + PlayerSettingsSubtitleScreen, + PlayerSettingsAudioScreen, + PlayerSettingsAdvancedScreen, +) + private val settingScreens = listOf( SettingsAppearanceScreen, SettingsLibraryScreen, SettingsReaderScreen, - SettingsPlayerScreen, SettingsDownloadScreen, SettingsTrackingScreen, SettingsBrowseScreen, diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/player/PlayerSettingsAdvancedScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/player/PlayerSettingsAdvancedScreen.kt new file mode 100644 index 0000000000..29c1dd21b8 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/player/PlayerSettingsAdvancedScreen.kt @@ -0,0 +1,70 @@ +package eu.kanade.presentation.more.settings.screen.player + +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.Settings +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.platform.LocalContext +import eu.kanade.presentation.more.settings.Preference +import eu.kanade.presentation.more.settings.screen.SearchableSettings +import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences +import tachiyomi.i18n.MR +import tachiyomi.presentation.core.i18n.stringResource +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +object PlayerSettingsAdvancedScreen : SearchableSettings { + + @ReadOnlyComposable + @Composable + override fun getTitleRes() = MR.strings.pref_player_advanced + + @Composable + override fun getPreferences(): List { + val playerPreferences = remember { Injekt.get() } + val scope = rememberCoroutineScope() + val context = LocalContext.current + + val enableScripts = playerPreferences.mpvScripts() + val mpvConf = playerPreferences.mpvConf() + val mpvInput = playerPreferences.mpvInput() + + return listOf( + Preference.PreferenceItem.SwitchPreference( + title = stringResource(MR.strings.pref_mpv_scripts), + subtitle = stringResource(MR.strings.pref_mpv_scripts_summary), + pref = enableScripts, + onValueChanged = { + // Ask for external storage permission + if (it) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) { + val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) + intent.data = Uri.fromParts("package", context.packageName, null) + context.startActivity(intent) + } + } + true + }, + ), + Preference.PreferenceItem.MPVConfPreference( + pref = mpvConf, + title = stringResource(MR.strings.pref_mpv_conf), + fileName = "mpv.conf", + scope = scope, + context = context, + ), + Preference.PreferenceItem.MPVConfPreference( + pref = mpvInput, + title = stringResource(MR.strings.pref_mpv_input), + fileName = "input.conf", + scope = scope, + context = context, + ), + ) + } +} diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/player/PlayerSettingsAudioScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/player/PlayerSettingsAudioScreen.kt new file mode 100644 index 0000000000..85c3d6dce2 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/player/PlayerSettingsAudioScreen.kt @@ -0,0 +1,65 @@ +package eu.kanade.presentation.more.settings.screen.player + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import eu.kanade.presentation.more.settings.Preference +import eu.kanade.presentation.more.settings.screen.SearchableSettings +import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences +import eu.kanade.tachiyomi.ui.player.viewer.AudioChannels +import kotlinx.collections.immutable.toImmutableMap +import tachiyomi.i18n.MR +import tachiyomi.presentation.core.i18n.stringResource +import tachiyomi.presentation.core.util.collectAsState +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +object PlayerSettingsAudioScreen : SearchableSettings { + + @ReadOnlyComposable + @Composable + override fun getTitleRes() = MR.strings.pref_player_audio + + @Composable + override fun getPreferences(): List { + val playerPreferences = remember { Injekt.get() } + + val prefLangs = playerPreferences.preferredAudioLanguages() + val pitchCorrection = playerPreferences.enablePitchCorrection() + val audioChannels = playerPreferences.audioChannels() + val boostCapPref = playerPreferences.volumeBoostCap() + val boostCap by boostCapPref.collectAsState() + + return listOf( + Preference.PreferenceItem.EditTextInfoPreference( + pref = prefLangs, + title = stringResource(MR.strings.pref_player_audio_lang), + dialogSubtitle = stringResource(MR.strings.pref_player_audio_lang_info), + ), + Preference.PreferenceItem.SwitchPreference( + pref = pitchCorrection, + title = stringResource(MR.strings.pref_player_audio_pitch_correction), + subtitle = stringResource(MR.strings.pref_player_audio_pitch_correction_summary), + ), + Preference.PreferenceItem.ListPreference( + pref = audioChannels, + title = stringResource(MR.strings.pref_player_audio_channels), + entries = AudioChannels.entries.associateWith { + stringResource(it.textRes) + }.toImmutableMap(), + ), + Preference.PreferenceItem.SliderPreference( + value = boostCap, + title = stringResource(MR.strings.pref_player_audio_boost_cap), + subtitle = boostCap.toString(), + min = 0, + max = 200, + onValueChanged = { + boostCapPref.set(it) + true + }, + ), + ) + } +} diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/player/PlayerSettingsDecoderScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/player/PlayerSettingsDecoderScreen.kt new file mode 100644 index 0000000000..95693a8bc2 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/player/PlayerSettingsDecoderScreen.kt @@ -0,0 +1,55 @@ +package eu.kanade.presentation.more.settings.screen.player + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.remember +import eu.kanade.presentation.more.settings.Preference +import eu.kanade.presentation.more.settings.screen.SearchableSettings +import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences +import eu.kanade.tachiyomi.ui.player.viewer.VideoDebanding +import kotlinx.collections.immutable.toImmutableMap +import tachiyomi.i18n.MR +import tachiyomi.presentation.core.i18n.stringResource +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +object PlayerSettingsDecoderScreen : SearchableSettings { + + @ReadOnlyComposable + @Composable + override fun getTitleRes() = MR.strings.pref_player_decoder + + @Composable + override fun getPreferences(): List { + val playerPreferences = remember { Injekt.get() } + + val tryHw = playerPreferences.tryHWDecoding() + val useGpuNext = playerPreferences.gpuNext() + val debanding = playerPreferences.videoDebanding() + val yuv420p = playerPreferences.useYUV420P() + + return listOf( + Preference.PreferenceItem.SwitchPreference( + pref = tryHw, + title = stringResource(MR.strings.pref_try_hw), + ), + Preference.PreferenceItem.SwitchPreference( + pref = useGpuNext, + title = stringResource(MR.strings.pref_gpu_next_title), + subtitle = stringResource(MR.strings.pref_gpu_next_subtitle), + ), + Preference.PreferenceItem.ListPreference( + pref = debanding, + title = stringResource(MR.strings.pref_debanding_title), + entries = VideoDebanding.entries.associateWith { + stringResource(it.stringRes) + }.toImmutableMap(), + ), + Preference.PreferenceItem.SwitchPreference( + pref = yuv420p, + title = stringResource(MR.strings.pref_use_yuv420p_title), + subtitle = stringResource(MR.strings.pref_use_yuv420p_subtitle), + ), + ) + } +} diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/player/PlayerSettingsGesturesScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/player/PlayerSettingsGesturesScreen.kt new file mode 100644 index 0000000000..82829d707b --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/player/PlayerSettingsGesturesScreen.kt @@ -0,0 +1,285 @@ +package eu.kanade.presentation.more.settings.screen.player + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import eu.kanade.presentation.more.settings.Preference +import eu.kanade.presentation.more.settings.screen.SearchableSettings +import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences +import eu.kanade.tachiyomi.ui.player.viewer.SingleActionGesture +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentMapOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentMap +import tachiyomi.i18n.MR +import tachiyomi.presentation.core.components.WheelTextPicker +import tachiyomi.presentation.core.i18n.stringResource +import tachiyomi.presentation.core.util.collectAsState +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +object PlayerSettingsGesturesScreen : SearchableSettings { + + @ReadOnlyComposable + @Composable + override fun getTitleRes() = MR.strings.pref_player_gestures + + @Composable + override fun getPreferences(): List { + val playerPreferences = remember { Injekt.get() } + + return listOf( + getSeekingGroup(playerPreferences = playerPreferences), + getDoubleTapGroup(playerPreferences = playerPreferences), + getMediaControlsGroup(playerPreferences = playerPreferences), + ) + } + + @Composable + private fun getSeekingGroup(playerPreferences: PlayerPreferences): Preference.PreferenceGroup { + val scope = rememberCoroutineScope() + val enableHorizontalSeekGesture = playerPreferences.gestureHorizontalSeek() + val defaultSkipIntroLength by playerPreferences.defaultIntroLength().stateIn(scope).collectAsState() + val skipLengthPreference = playerPreferences.skipLengthPreference() + val playerSmoothSeek = playerPreferences.playerSmoothSeek() + val mediaChapterSeek = playerPreferences.mediaChapterSeek() + + var showDialog by rememberSaveable { mutableStateOf(false) } + if (showDialog) { + SkipIntroLengthDialog( + initialSkipIntroLength = defaultSkipIntroLength, + onDismissRequest = { showDialog = false }, + onValueChanged = { skipIntroLength -> + playerPreferences.defaultIntroLength().set(skipIntroLength) + showDialog = false + }, + ) + } + + // Aniskip + val enableAniSkip = playerPreferences.aniSkipEnabled() + val enableAutoAniSkip = playerPreferences.autoSkipAniSkip() + val enableNetflixAniSkip = playerPreferences.enableNetflixStyleAniSkip() + val waitingTimeAniSkip = playerPreferences.waitingTimeAniSkip() + + val isAniSkipEnabled by enableAniSkip.collectAsState() + + return Preference.PreferenceGroup( + title = stringResource(MR.strings.pref_category_player_seeking), + preferenceItems = persistentListOf( + Preference.PreferenceItem.SwitchPreference( + pref = enableHorizontalSeekGesture, + title = stringResource(MR.strings.enable_horizontal_seek_gesture), + ), + Preference.PreferenceItem.TextPreference( + title = stringResource(MR.strings.pref_default_intro_length), + subtitle = "${defaultSkipIntroLength}s", + onClick = { showDialog = true }, + ), + Preference.PreferenceItem.ListPreference( + pref = skipLengthPreference, + title = stringResource(MR.strings.pref_skip_length), + entries = persistentMapOf( + 30 to stringResource(MR.strings.pref_skip_30), + 20 to stringResource(MR.strings.pref_skip_20), + 10 to stringResource(MR.strings.pref_skip_10), + 5 to stringResource(MR.strings.pref_skip_5), + 3 to stringResource(MR.strings.pref_skip_3), + 0 to stringResource(MR.strings.pref_skip_disable), + ), + ), + Preference.PreferenceItem.SwitchPreference( + pref = playerSmoothSeek, + title = stringResource(MR.strings.pref_player_smooth_seek), + subtitle = stringResource(MR.strings.pref_player_smooth_seek_summary), + ), + Preference.PreferenceItem.SwitchPreference( + pref = mediaChapterSeek, + title = stringResource(MR.strings.pref_media_control_chapter_seeking), + subtitle = stringResource(MR.strings.pref_media_control_chapter_seeking_summary), + ), + Preference.PreferenceItem.InfoPreference( + title = stringResource(MR.strings.pref_category_player_aniskip_info), + ), + Preference.PreferenceItem.SwitchPreference( + pref = enableAniSkip, + title = stringResource(MR.strings.pref_enable_aniskip), + ), + Preference.PreferenceItem.SwitchPreference( + pref = enableAutoAniSkip, + title = stringResource(MR.strings.pref_enable_auto_skip_ani_skip), + enabled = isAniSkipEnabled, + ), + Preference.PreferenceItem.SwitchPreference( + pref = enableNetflixAniSkip, + title = stringResource(MR.strings.pref_enable_netflix_style_aniskip), + enabled = isAniSkipEnabled, + ), + Preference.PreferenceItem.ListPreference( + pref = waitingTimeAniSkip, + title = stringResource(MR.strings.pref_waiting_time_aniskip), + entries = persistentMapOf( + 5 to stringResource(MR.strings.pref_waiting_time_aniskip_5), + 6 to stringResource(MR.strings.pref_waiting_time_aniskip_6), + 7 to stringResource(MR.strings.pref_waiting_time_aniskip_7), + 8 to stringResource(MR.strings.pref_waiting_time_aniskip_8), + 9 to stringResource(MR.strings.pref_waiting_time_aniskip_9), + 10 to stringResource(MR.strings.pref_waiting_time_aniskip_10), + ), + enabled = isAniSkipEnabled, + ), + ), + ) + } + + @Composable + private fun getDoubleTapGroup(playerPreferences: PlayerPreferences): Preference.PreferenceGroup { + val leftDoubleTap = playerPreferences.leftDoubleTapGesture() + val centerDoubleTap = playerPreferences.centerDoubleTapGesture() + val rightDoubleTap = playerPreferences.rightDoubleTapGesture() + + return Preference.PreferenceGroup( + title = stringResource(MR.strings.pref_category_double_tap), + preferenceItems = persistentListOf( + Preference.PreferenceItem.ListPreference( + pref = leftDoubleTap, + title = stringResource(MR.strings.pref_left_double_tap), + entries = listOf( + SingleActionGesture.None, + SingleActionGesture.Seek, + SingleActionGesture.PlayPause, + SingleActionGesture.Switch, + SingleActionGesture.Custom, + ).associateWith { stringResource(it.stringRes) }.toPersistentMap(), + ), + Preference.PreferenceItem.ListPreference( + pref = centerDoubleTap, + title = stringResource(MR.strings.pref_center_double_tap), + entries = listOf( + SingleActionGesture.None, + SingleActionGesture.PlayPause, + SingleActionGesture.Custom, + ).associateWith { stringResource(it.stringRes) }.toPersistentMap(), + ), + Preference.PreferenceItem.ListPreference( + pref = rightDoubleTap, + title = stringResource(MR.strings.pref_right_double_tap), + entries = listOf( + SingleActionGesture.None, + SingleActionGesture.Seek, + SingleActionGesture.PlayPause, + SingleActionGesture.Switch, + SingleActionGesture.Custom, + ).associateWith { stringResource(it.stringRes) }.toPersistentMap(), + ), + Preference.PreferenceItem.InfoPreference( + title = stringResource(MR.strings.pref_double_tap_info), + ), + ), + ) + } + + @Composable + private fun getMediaControlsGroup(playerPreferences: PlayerPreferences): Preference.PreferenceGroup { + val mediaPrevious = playerPreferences.mediaPreviousGesture() + val mediaPlayPause = playerPreferences.mediaPlayPauseGesture() + val mediaNext = playerPreferences.mediaNextGesture() + + return Preference.PreferenceGroup( + title = stringResource(MR.strings.pref_category_media_controls), + preferenceItems = persistentListOf( + Preference.PreferenceItem.ListPreference( + pref = mediaPrevious, + title = stringResource(MR.strings.pref_media_previous), + entries = listOf( + SingleActionGesture.None, + SingleActionGesture.Seek, + SingleActionGesture.PlayPause, + SingleActionGesture.Switch, + SingleActionGesture.Custom, + ).associateWith { stringResource(it.stringRes) }.toPersistentMap(), + ), + Preference.PreferenceItem.ListPreference( + pref = mediaPlayPause, + title = stringResource(MR.strings.pref_media_playpause), + entries = listOf( + SingleActionGesture.None, + SingleActionGesture.PlayPause, + SingleActionGesture.Custom, + ).associateWith { stringResource(it.stringRes) }.toPersistentMap(), + ), + Preference.PreferenceItem.ListPreference( + pref = mediaNext, + title = stringResource(MR.strings.pref_media_next), + entries = listOf( + SingleActionGesture.None, + SingleActionGesture.Seek, + SingleActionGesture.PlayPause, + SingleActionGesture.Switch, + SingleActionGesture.Custom, + ).associateWith { stringResource(it.stringRes) }.toPersistentMap(), + ), + Preference.PreferenceItem.InfoPreference( + title = stringResource(MR.strings.pref_media_info), + ), + ), + ) + } + + @Composable + private fun SkipIntroLengthDialog( + initialSkipIntroLength: Int, + onDismissRequest: () -> Unit, + onValueChanged: (skipIntroLength: Int) -> Unit, + ) { + val skipIntroLengthValue by rememberSaveable { mutableStateOf(initialSkipIntroLength) } + var newLength = 0 + AlertDialog( + onDismissRequest = onDismissRequest, + title = { Text(text = stringResource(MR.strings.pref_intro_length)) }, + text = { + Box( + modifier = Modifier.fillMaxWidth(), + content = { + WheelTextPicker( + modifier = Modifier.align(Alignment.Center), + items = remember { 0..255 }.map { + stringResource( + MR.strings.seconds_short, + it, + ) + }.toImmutableList(), + onSelectionChanged = { + newLength = it + }, + startIndex = skipIntroLengthValue, + ) + }, + ) + }, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(MR.strings.action_cancel)) + } + }, + confirmButton = { + TextButton(onClick = { onValueChanged(newLength) }) { + Text(text = stringResource(MR.strings.action_ok)) + } + }, + ) + } +} diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/player/PlayerSettingsMainScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/player/PlayerSettingsMainScreen.kt new file mode 100644 index 0000000000..41d0f67743 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/player/PlayerSettingsMainScreen.kt @@ -0,0 +1,209 @@ +package eu.kanade.presentation.more.settings.screen.player + +import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Audiotrack +import androidx.compose.material.icons.outlined.Code +import androidx.compose.material.icons.outlined.Gesture +import androidx.compose.material.icons.outlined.Memory +import androidx.compose.material.icons.outlined.PlayCircleOutline +import androidx.compose.material.icons.outlined.Search +import androidx.compose.material.icons.outlined.Subtitles +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp +import androidx.core.graphics.ColorUtils +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.Navigator +import cafe.adriel.voyager.navigator.currentOrThrow +import dev.icerock.moko.resources.StringResource +import eu.kanade.presentation.components.AppBar +import eu.kanade.presentation.components.AppBarActions +import eu.kanade.presentation.more.settings.screen.SettingsSearchScreen +import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget +import eu.kanade.presentation.util.LocalBackPress +import eu.kanade.presentation.util.Screen +import kotlinx.collections.immutable.persistentListOf +import tachiyomi.i18n.MR +import tachiyomi.presentation.core.components.material.Scaffold +import tachiyomi.presentation.core.i18n.stringResource +import cafe.adriel.voyager.core.screen.Screen as VoyagerScreen + +object PlayerSettingsMainScreen : Screen() { + @Composable + override fun Content() { + Content(twoPane = false) + } + + @Composable + private fun getPalerSurface(): Color { + val surface = MaterialTheme.colorScheme.surface + val dark = isSystemInDarkTheme() + return remember(surface, dark) { + val arr = FloatArray(3) + ColorUtils.colorToHSL(surface.toArgb(), arr) + arr[2] = if (dark) { + arr[2] - 0.05f + } else { + arr[2] + 0.02f + }.coerceIn(0f, 1f) + Color.hsl(arr[0], arr[1], arr[2]) + } + } + + @Composable + fun Content(twoPane: Boolean) { + val navigator = LocalNavigator.currentOrThrow + val backPress = LocalBackPress.currentOrThrow + val containerColor = if (twoPane) getPalerSurface() else MaterialTheme.colorScheme.surface + val topBarState = rememberTopAppBarState() + + Scaffold( + topBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(topBarState), + topBar = { scrollBehavior -> + AppBar( + title = stringResource(MR.strings.label_player_settings), + navigateUp = backPress::invoke, + actions = { + AppBarActions( + persistentListOf( + AppBar.Action( + title = stringResource(MR.strings.action_search), + icon = Icons.Outlined.Search, + onClick = { navigator.navigate(SettingsSearchScreen(true), twoPane) }, + ), + ), + ) + }, + scrollBehavior = scrollBehavior, + ) + }, + containerColor = containerColor, + content = { contentPadding -> + val state = rememberLazyListState() + val indexSelected = if (twoPane) { + items.indexOfFirst { it.screen::class == navigator.items.first()::class } + .also { + LaunchedEffect(Unit) { + state.animateScrollToItem(it) + if (it > 0) { + // Lift scroll + topBarState.contentOffset = topBarState.heightOffsetLimit + } + } + } + } else { + null + } + + LazyColumn( + state = state, + contentPadding = contentPadding, + ) { + itemsIndexed( + items = items, + key = { _, item -> item.hashCode() }, + ) { index, item -> + val selected = indexSelected == index + var modifier: Modifier = Modifier + var contentColor = LocalContentColor.current + if (twoPane) { + modifier = Modifier + .padding(horizontal = 8.dp) + .clip(RoundedCornerShape(24.dp)) + .then( + if (selected) { + Modifier.background( + MaterialTheme.colorScheme.surfaceVariant, + ) + } else { + Modifier + }, + ) + if (selected) { + contentColor = MaterialTheme.colorScheme.onSurfaceVariant + } + } + CompositionLocalProvider(LocalContentColor provides contentColor) { + TextPreferenceWidget( + modifier = modifier, + title = stringResource(item.titleRes), + subtitle = item.formatSubtitle(), + icon = item.icon, + onPreferenceClick = { navigator.navigate(item.screen, twoPane) }, + ) + } + } + } + }, + ) + } + + private fun Navigator.navigate(screen: VoyagerScreen, twoPane: Boolean) { + if (twoPane) replaceAll(screen) else push(screen) + } + + private data class Item( + val titleRes: StringResource, + val subtitleRes: StringResource? = null, + val formatSubtitle: @Composable () -> String? = { subtitleRes?.let { stringResource(it) } }, + val icon: ImageVector, + val screen: VoyagerScreen, + ) + + private val items = listOf( + Item( + titleRes = MR.strings.pref_player_internal, + subtitleRes = MR.strings.pref_player_internal_summary, + icon = Icons.Outlined.PlayCircleOutline, + screen = PlayerSettingsPlayerScreen, + ), + Item( + titleRes = MR.strings.pref_player_gestures, + subtitleRes = MR.strings.pref_player_gestures_summary, + icon = Icons.Outlined.Gesture, + screen = PlayerSettingsGesturesScreen, + ), + Item( + titleRes = MR.strings.pref_player_decoder, + subtitleRes = MR.strings.pref_player_decoder_summary, + icon = Icons.Outlined.Memory, + screen = PlayerSettingsDecoderScreen, + ), + Item( + titleRes = MR.strings.pref_player_subtitle, + subtitleRes = MR.strings.pref_player_subtitle_summary, + icon = Icons.Outlined.Subtitles, + screen = PlayerSettingsSubtitleScreen, + ), + Item( + titleRes = MR.strings.pref_player_audio, + subtitleRes = MR.strings.pref_player_audio_summary, + icon = Icons.Outlined.Audiotrack, + screen = PlayerSettingsAudioScreen, + ), + Item( + titleRes = MR.strings.pref_player_advanced, + subtitleRes = MR.strings.pref_player_advanced_summary, + icon = Icons.Outlined.Code, + screen = PlayerSettingsAdvancedScreen, + ), + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsPlayerScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/player/PlayerSettingsPlayerScreen.kt similarity index 54% rename from app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsPlayerScreen.kt rename to app/src/main/java/eu/kanade/presentation/more/settings/screen/player/PlayerSettingsPlayerScreen.kt index 98b9902b65..cbd1ce3399 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsPlayerScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/player/PlayerSettingsPlayerScreen.kt @@ -1,27 +1,14 @@ -package eu.kanade.presentation.more.settings.screen +package eu.kanade.presentation.more.settings.screen.player import android.content.pm.ActivityInfo import android.os.Build -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import cafe.adriel.voyager.navigator.LocalNavigator -import cafe.adriel.voyager.navigator.currentOrThrow import eu.kanade.domain.base.BasePreferences import eu.kanade.presentation.more.settings.Preference +import eu.kanade.presentation.more.settings.screen.SearchableSettings import eu.kanade.tachiyomi.ui.player.JUST_PLAYER import eu.kanade.tachiyomi.ui.player.MPV_KT import eu.kanade.tachiyomi.ui.player.MPV_KT_PREVIEW @@ -35,23 +22,20 @@ import eu.kanade.tachiyomi.ui.player.VLC_PLAYER import eu.kanade.tachiyomi.ui.player.WEB_VIDEO_CASTER import eu.kanade.tachiyomi.ui.player.X_PLAYER import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences -import eu.kanade.tachiyomi.ui.player.viewer.AudioChannels import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentMapOf -import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toPersistentMap import tachiyomi.i18n.MR -import tachiyomi.presentation.core.components.WheelTextPicker import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.util.collectAsState import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -object SettingsPlayerScreen : SearchableSettings { +object PlayerSettingsPlayerScreen : SearchableSettings { @ReadOnlyComposable @Composable - override fun getTitleRes() = MR.strings.pref_category_player + override fun getTitleRes() = MR.strings.pref_player_internal @Composable override fun getPreferences(): List { @@ -77,10 +61,17 @@ object SettingsPlayerScreen : SearchableSettings { pref = playerPreferences.preserveWatchingPosition(), title = stringResource(MR.strings.pref_preserve_watching_position), ), - getInternalPlayerGroup(playerPreferences = playerPreferences), + Preference.PreferenceItem.SwitchPreference( + pref = playerPreferences.playerFullscreen(), + title = stringResource(MR.strings.pref_player_fullscreen), + enabled = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P, + ), + Preference.PreferenceItem.SwitchPreference( + pref = playerPreferences.hideControls(), + title = stringResource(MR.strings.pref_player_hide_controls), + ), getVolumeAndBrightnessGroup(playerPreferences = playerPreferences), getOrientationGroup(playerPreferences = playerPreferences), - getSeekingGroup(playerPreferences = playerPreferences), if (deviceSupportsPip) getPipGroup(playerPreferences = playerPreferences) else null, getExternalPlayerGroup( playerPreferences = playerPreferences, @@ -89,45 +80,6 @@ object SettingsPlayerScreen : SearchableSettings { ) } - @Composable - private fun getInternalPlayerGroup(playerPreferences: PlayerPreferences): Preference.PreferenceGroup { - val playerFullscreen = playerPreferences.playerFullscreen() - val playerHideControls = playerPreferences.hideControls() - val playerAudioChannels = playerPreferences.audioChannels() - val navigator = LocalNavigator.currentOrThrow - - return Preference.PreferenceGroup( - title = stringResource(MR.strings.pref_category_internal_player), - preferenceItems = persistentListOf( - Preference.PreferenceItem.SwitchPreference( - pref = playerFullscreen, - title = stringResource(MR.strings.pref_player_fullscreen), - enabled = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P, - ), - Preference.PreferenceItem.SwitchPreference( - pref = playerHideControls, - title = stringResource(MR.strings.pref_player_hide_controls), - ), - Preference.PreferenceItem.ListPreference( - pref = playerAudioChannels, - title = stringResource(MR.strings.pref_player_audio_channels), - entries = persistentMapOf( - AudioChannels.AutoSafe to stringResource(AudioChannels.AutoSafe.textRes), - AudioChannels.Auto to stringResource(AudioChannels.Auto.textRes), - AudioChannels.Mono to stringResource(AudioChannels.Mono.textRes), - AudioChannels.Stereo to stringResource(AudioChannels.Stereo.textRes), - AudioChannels.ReverseStereo to stringResource(AudioChannels.ReverseStereo.textRes), - ), - ), - Preference.PreferenceItem.TextPreference( - title = stringResource(MR.strings.pref_category_player_advanced), - subtitle = stringResource(MR.strings.pref_category_player_advanced_subtitle), - onClick = { navigator.push(AdvancedPlayerSettingsScreen) }, - ), - ), - ) - } - @Composable private fun getVolumeAndBrightnessGroup(playerPreferences: PlayerPreferences): Preference.PreferenceGroup { val enableVolumeBrightnessGestures = playerPreferences.gestureVolumeBrightness() @@ -228,103 +180,6 @@ object SettingsPlayerScreen : SearchableSettings { ) } - @Composable - private fun getSeekingGroup(playerPreferences: PlayerPreferences): Preference.PreferenceGroup { - val scope = rememberCoroutineScope() - val enableHorizontalSeekGesture = playerPreferences.gestureHorizontalSeek() - val defaultSkipIntroLength by playerPreferences.defaultIntroLength().stateIn(scope).collectAsState() - val skipLengthPreference = playerPreferences.skipLengthPreference() - val playerSmoothSeek = playerPreferences.playerSmoothSeek() - val mediaChapterSeek = playerPreferences.mediaChapterSeek() - - var showDialog by rememberSaveable { mutableStateOf(false) } - if (showDialog) { - SkipIntroLengthDialog( - initialSkipIntroLength = defaultSkipIntroLength, - onDismissRequest = { showDialog = false }, - onValueChanged = { skipIntroLength -> - playerPreferences.defaultIntroLength().set(skipIntroLength) - showDialog = false - }, - ) - } - - // Aniskip - val enableAniSkip = playerPreferences.aniSkipEnabled() - val enableAutoAniSkip = playerPreferences.autoSkipAniSkip() - val enableNetflixAniSkip = playerPreferences.enableNetflixStyleAniSkip() - val waitingTimeAniSkip = playerPreferences.waitingTimeAniSkip() - - val isAniSkipEnabled by enableAniSkip.collectAsState() - - return Preference.PreferenceGroup( - title = stringResource(MR.strings.pref_category_player_seeking), - preferenceItems = persistentListOf( - Preference.PreferenceItem.SwitchPreference( - pref = enableHorizontalSeekGesture, - title = stringResource(MR.strings.enable_horizontal_seek_gesture), - ), - Preference.PreferenceItem.TextPreference( - title = stringResource(MR.strings.pref_default_intro_length), - subtitle = "${defaultSkipIntroLength}s", - onClick = { showDialog = true }, - ), - Preference.PreferenceItem.ListPreference( - pref = skipLengthPreference, - title = stringResource(MR.strings.pref_skip_length), - entries = persistentMapOf( - 30 to stringResource(MR.strings.pref_skip_30), - 20 to stringResource(MR.strings.pref_skip_20), - 10 to stringResource(MR.strings.pref_skip_10), - 5 to stringResource(MR.strings.pref_skip_5), - 3 to stringResource(MR.strings.pref_skip_3), - 0 to stringResource(MR.strings.pref_skip_disable), - ), - ), - Preference.PreferenceItem.SwitchPreference( - pref = playerSmoothSeek, - title = stringResource(MR.strings.pref_player_smooth_seek), - subtitle = stringResource(MR.strings.pref_player_smooth_seek_summary), - ), - Preference.PreferenceItem.SwitchPreference( - pref = mediaChapterSeek, - title = stringResource(MR.strings.pref_media_control_chapter_seeking), - subtitle = stringResource(MR.strings.pref_media_control_chapter_seeking_summary), - ), - Preference.PreferenceItem.InfoPreference( - title = stringResource(MR.strings.pref_category_player_aniskip_info), - ), - Preference.PreferenceItem.SwitchPreference( - pref = enableAniSkip, - title = stringResource(MR.strings.pref_enable_aniskip), - ), - Preference.PreferenceItem.SwitchPreference( - pref = enableAutoAniSkip, - title = stringResource(MR.strings.pref_enable_auto_skip_ani_skip), - enabled = isAniSkipEnabled, - ), - Preference.PreferenceItem.SwitchPreference( - pref = enableNetflixAniSkip, - title = stringResource(MR.strings.pref_enable_netflix_style_aniskip), - enabled = isAniSkipEnabled, - ), - Preference.PreferenceItem.ListPreference( - pref = waitingTimeAniSkip, - title = stringResource(MR.strings.pref_waiting_time_aniskip), - entries = persistentMapOf( - 5 to stringResource(MR.strings.pref_waiting_time_aniskip_5), - 6 to stringResource(MR.strings.pref_waiting_time_aniskip_6), - 7 to stringResource(MR.strings.pref_waiting_time_aniskip_7), - 8 to stringResource(MR.strings.pref_waiting_time_aniskip_8), - 9 to stringResource(MR.strings.pref_waiting_time_aniskip_9), - 10 to stringResource(MR.strings.pref_waiting_time_aniskip_10), - ), - enabled = isAniSkipEnabled, - ), - ), - ) - } - @Composable private fun getPipGroup(playerPreferences: PlayerPreferences): Preference.PreferenceGroup { val enablePip = playerPreferences.enablePip() @@ -395,50 +250,6 @@ object SettingsPlayerScreen : SearchableSettings { ), ) } - - @Composable - private fun SkipIntroLengthDialog( - initialSkipIntroLength: Int, - onDismissRequest: () -> Unit, - onValueChanged: (skipIntroLength: Int) -> Unit, - ) { - val skipIntroLengthValue by rememberSaveable { mutableStateOf(initialSkipIntroLength) } - var newLength = 0 - AlertDialog( - onDismissRequest = onDismissRequest, - title = { Text(text = stringResource(MR.strings.pref_intro_length)) }, - text = { - Box( - modifier = Modifier.fillMaxWidth(), - content = { - WheelTextPicker( - modifier = Modifier.align(Alignment.Center), - items = remember { 0..255 }.map { - stringResource( - MR.strings.seconds_short, - it, - ) - }.toImmutableList(), - onSelectionChanged = { - newLength = it - }, - startIndex = skipIntroLengthValue, - ) - }, - ) - }, - dismissButton = { - TextButton(onClick = onDismissRequest) { - Text(text = stringResource(MR.strings.action_cancel)) - } - }, - confirmButton = { - TextButton(onClick = { onValueChanged(newLength) }) { - Text(text = stringResource(MR.strings.action_ok)) - } - }, - ) - } } val externalPlayers = listOf( diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/player/PlayerSettingsSubtitleScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/player/PlayerSettingsSubtitleScreen.kt new file mode 100644 index 0000000000..cfc408ff2a --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/player/PlayerSettingsSubtitleScreen.kt @@ -0,0 +1,46 @@ +package eu.kanade.presentation.more.settings.screen.player + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.remember +import eu.kanade.presentation.more.settings.Preference +import eu.kanade.presentation.more.settings.screen.SearchableSettings +import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences +import tachiyomi.i18n.MR +import tachiyomi.presentation.core.i18n.stringResource +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +object PlayerSettingsSubtitleScreen : SearchableSettings { + + @ReadOnlyComposable + @Composable + override fun getTitleRes() = MR.strings.pref_player_subtitle + + @Composable + override fun getPreferences(): List { + val playerPreferences = remember { Injekt.get() } + + val langPref = playerPreferences.preferredSubLanguages() + val whitelist = playerPreferences.subtitleWhitelist() + val blacklist = playerPreferences.subtitleBlacklist() + + return listOf( + Preference.PreferenceItem.EditTextInfoPreference( + pref = langPref, + title = stringResource(MR.strings.pref_player_subtitle_lang), + dialogSubtitle = stringResource(MR.strings.pref_player_subtitle_lang_info), + ), + Preference.PreferenceItem.EditTextInfoPreference( + pref = whitelist, + title = stringResource(MR.strings.pref_player_subtitle_whitelist), + dialogSubtitle = stringResource(MR.strings.pref_player_subtitle_whitelist_info), + ), + Preference.PreferenceItem.EditTextInfoPreference( + pref = blacklist, + title = stringResource(MR.strings.pref_player_subtitle_blacklist), + dialogSubtitle = stringResource(MR.strings.pref_player_subtitle_blacklist_info), + ), + ) + } +} diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/widget/EditTextPreferenceWidget.kt b/app/src/main/java/eu/kanade/presentation/more/settings/widget/EditTextPreferenceWidget.kt index ac31efe29b..072874f426 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/widget/EditTextPreferenceWidget.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/widget/EditTextPreferenceWidget.kt @@ -1,5 +1,6 @@ package eu.kanade.presentation.more.settings.widget +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Cancel @@ -7,6 +8,7 @@ import androidx.compose.material.icons.filled.Error import androidx.compose.material3.AlertDialog import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -29,6 +31,7 @@ import tachiyomi.presentation.core.i18n.stringResource fun EditTextPreferenceWidget( title: String, subtitle: String?, + dialogSubtitle: String? = null, icon: ImageVector?, value: String, onConfirm: suspend (String) -> Boolean, @@ -52,7 +55,14 @@ fun EditTextPreferenceWidget( } AlertDialog( onDismissRequest = onDismissRequest, - title = { Text(text = title) }, + title = { + Column { + Text(text = title) + if (dialogSubtitle != null) { + Text(text = dialogSubtitle, style = MaterialTheme.typography.bodyMedium) + } + } + }, text = { OutlinedTextField( value = textFieldValue, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreTab.kt index 1afa700b90..0d617c7490 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreTab.kt @@ -25,6 +25,7 @@ import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadManager import eu.kanade.tachiyomi.data.download.manga.MangaDownloadManager import eu.kanade.tachiyomi.ui.category.CategoriesTab import eu.kanade.tachiyomi.ui.download.DownloadsTab +import eu.kanade.tachiyomi.ui.setting.PlayerSettingsScreen import eu.kanade.tachiyomi.ui.setting.SettingsScreen import eu.kanade.tachiyomi.ui.stats.StatsTab import eu.kanade.tachiyomi.ui.storage.StorageTab @@ -79,6 +80,7 @@ data object MoreTab : Tab { onClickStats = { navigator.push(StatsTab) }, onClickStorage = { navigator.push(StorageTab) }, onClickDataAndStorage = { navigator.push(SettingsScreen(SettingsScreen.Destination.DataAndStorage)) }, + onClickPlayerSettings = { navigator.push(PlayerSettingsScreen) }, onClickSettings = { navigator.push(SettingsScreen()) }, onClickAbout = { navigator.push(SettingsScreen(SettingsScreen.Destination.About)) }, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt index 940995035d..75b93eb4fc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt @@ -624,8 +624,7 @@ class PlayerActivity : BaseActivity() { when (playerPreferences.videoDebanding().get()) { VideoDebanding.CPU -> MPVLib.setOptionString("vf", "gradfun=radius=12") VideoDebanding.GPU -> MPVLib.setOptionString("deband", "yes") - VideoDebanding.YUV420P -> MPVLib.setOptionString("vf", "format=yuv420p") - VideoDebanding.DISABLED -> {} + VideoDebanding.NONE -> {} } val currentPlayerStatisticsPage = playerPreferences.playerStatisticsPage().get() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/PlayerPreferences.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/PlayerPreferences.kt index 40711f4192..25d61bb884 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/PlayerPreferences.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/settings/PlayerPreferences.kt @@ -4,6 +4,7 @@ import eu.kanade.tachiyomi.ui.player.viewer.AspectState import eu.kanade.tachiyomi.ui.player.viewer.AudioChannels import eu.kanade.tachiyomi.ui.player.viewer.HwDecState import eu.kanade.tachiyomi.ui.player.viewer.InvertedPlayback +import eu.kanade.tachiyomi.ui.player.viewer.SingleActionGesture import eu.kanade.tachiyomi.ui.player.viewer.VideoDebanding import tachiyomi.core.common.preference.PreferenceStore import tachiyomi.core.common.preference.getEnum @@ -11,33 +12,29 @@ import tachiyomi.core.common.preference.getEnum class PlayerPreferences( private val preferenceStore: PreferenceStore, ) { + // ==== Internal player ==== + fun preserveWatchingPosition() = preferenceStore.getBoolean( "pref_preserve_watching_position", false, ) + fun progressPreference() = preferenceStore.getFloat("pref_progress_preference", 0.85F) - fun enablePip() = preferenceStore.getBoolean("pref_enable_pip", true) - fun pipEpisodeToasts() = preferenceStore.getBoolean("pref_pip_episode_toasts", true) - fun pipOnExit() = preferenceStore.getBoolean("pref_pip_on_exit", false) - fun pipReplaceWithPrevious() = preferenceStore.getBoolean("pip_replace_with_previous", false) + fun playerFullscreen() = preferenceStore.getBoolean("player_fullscreen", true) + fun hideControls() = preferenceStore.getBoolean("player_hide_controls", false) + + // Internal player - Volume and brightness + fun gestureVolumeBrightness() = preferenceStore.getBoolean( + "pref_gesture_volume_brightness", + true, + ) fun rememberPlayerBrightness() = preferenceStore.getBoolean("pref_remember_brightness", false) fun playerBrightnessValue() = preferenceStore.getFloat("player_brightness_value", -1.0F) - fun rememberPlayerVolume() = preferenceStore.getBoolean("pref_remember_volume", false) fun playerVolumeValue() = preferenceStore.getFloat("player_volume_value", -1.0F) - fun audioChannels() = preferenceStore.getEnum("pref_audio_config", AudioChannels.AutoSafe) - - fun autoplayEnabled() = preferenceStore.getBoolean("pref_auto_play_enabled", false) - - fun invertedPlayback() = preferenceStore.getEnum("pref_inverted_playback", InvertedPlayback.NONE) - - fun mpvConf() = preferenceStore.getString("pref_mpv_conf", "") - - fun mpvInput() = preferenceStore.getString("pref_mpv_input", "") - - fun subSelectConf() = preferenceStore.getString("pref_sub_select_conf", "") + // Internal player - Orientation fun defaultPlayerOrientationType() = preferenceStore.getInt( "pref_default_player_orientation_type_key", @@ -47,36 +44,23 @@ class PlayerPreferences( "pref_adjust_orientation_video_dimensions", true, ) - - fun defaultPlayerOrientationLandscape() = preferenceStore.getInt( - "pref_default_player_orientation_landscape_key", - 6, - ) fun defaultPlayerOrientationPortrait() = preferenceStore.getInt( "pref_default_player_orientation_portrait_key", 7, ) + fun defaultPlayerOrientationLandscape() = preferenceStore.getInt( + "pref_default_player_orientation_landscape_key", + 6, + ) - fun playerSpeed() = preferenceStore.getFloat("pref_player_speed", 1F) - - fun playerSmoothSeek() = preferenceStore.getBoolean("pref_player_smooth_seek", false) - - fun mediaChapterSeek() = preferenceStore.getBoolean("pref_media_control_chapter_seeking", false) - - fun aspectState() = preferenceStore.getEnum("pref_player_aspect_state", AspectState.FIT) - - fun playerFullscreen() = preferenceStore.getBoolean("player_fullscreen", true) + // Internal player - PiP - fun hideControls() = preferenceStore.getBoolean("player_hide_controls", false) + fun enablePip() = preferenceStore.getBoolean("pref_enable_pip", true) + fun pipEpisodeToasts() = preferenceStore.getBoolean("pref_pip_episode_toasts", true) + fun pipOnExit() = preferenceStore.getBoolean("pref_pip_on_exit", false) + fun pipReplaceWithPrevious() = preferenceStore.getBoolean("pip_replace_with_previous", false) - fun screenshotSubtitles() = preferenceStore.getBoolean("pref_screenshot_subtitles", false) - - fun gestureVolumeBrightness() = preferenceStore.getBoolean( - "pref_gesture_volume_brightness", - true, - ) - fun gestureHorizontalSeek() = preferenceStore.getBoolean("pref_gesture_horizontal_seek", true) - fun playerStatisticsPage() = preferenceStore.getInt("pref_player_statistics_page", 0) + // Internal player - External player fun alwaysUseExternalPlayer() = preferenceStore.getBoolean( "pref_always_use_external_player", @@ -84,22 +68,78 @@ class PlayerPreferences( ) fun externalPlayerPreference() = preferenceStore.getString("external_player_preference", "") - fun progressPreference() = preferenceStore.getFloat("pref_progress_preference", 0.85F) + // ==== Gestures ==== + // Gestures - Seeking - fun defaultIntroLength() = preferenceStore.getInt("pref_default_intro_length", 85) fun skipLengthPreference() = preferenceStore.getInt("pref_skip_length_preference", 10) + fun gestureHorizontalSeek() = preferenceStore.getBoolean("pref_gesture_horizontal_seek", true) + fun defaultIntroLength() = preferenceStore.getInt("pref_default_intro_length", 85) + fun playerSmoothSeek() = preferenceStore.getBoolean("pref_player_smooth_seek", false) + fun mediaChapterSeek() = preferenceStore.getBoolean("pref_media_control_chapter_seeking", false) fun aniSkipEnabled() = preferenceStore.getBoolean("pref_enable_ani_skip", false) fun autoSkipAniSkip() = preferenceStore.getBoolean("pref_enable_auto_skip_ani_skip", false) - fun waitingTimeAniSkip() = preferenceStore.getInt("pref_waiting_time_aniskip", 5) fun enableNetflixStyleAniSkip() = preferenceStore.getBoolean( "pref_enable_netflixStyle_aniskip", false, ) + fun waitingTimeAniSkip() = preferenceStore.getInt("pref_waiting_time_aniskip", 5) - fun hardwareDecoding() = preferenceStore.getEnum("pref_hardware_decoding", HwDecState.defaultHwDec) - fun videoDebanding() = preferenceStore.getEnum("pref_video_debanding", VideoDebanding.DISABLED) + // Gestures - Double tap + + fun leftDoubleTapGesture() = preferenceStore.getEnum("pref_left_double_tap", SingleActionGesture.Seek) + fun centerDoubleTapGesture() = preferenceStore.getEnum("pref_center_double_tap", SingleActionGesture.PlayPause) + fun rightDoubleTapGesture() = preferenceStore.getEnum("pref_right_double_tap", SingleActionGesture.Seek) + + // Gestures - Media controls + + fun mediaPreviousGesture() = preferenceStore.getEnum("pref_media_previous", SingleActionGesture.Switch) + fun mediaPlayPauseGesture() = preferenceStore.getEnum("pref_media_playpause", SingleActionGesture.PlayPause) + fun mediaNextGesture() = preferenceStore.getEnum("pref_media_next", SingleActionGesture.Switch) + + // ==== Decoder ==== + + fun tryHWDecoding() = preferenceStore.getBoolean("pref_try_hwdec", true) fun gpuNext() = preferenceStore.getBoolean("pref_gpu_next", false) + fun videoDebanding() = preferenceStore.getEnum("pref_video_debanding", VideoDebanding.NONE) + fun useYUV420P() = preferenceStore.getBoolean("use_yuv420p", true) + + // ==== Subtitle ==== + + fun preferredSubLanguages() = preferenceStore.getString("pref_subtitle_lang", "") + fun subtitleWhitelist() = preferenceStore.getString("pref_subtitle_whitelist", "") + fun subtitleBlacklist() = preferenceStore.getString("pref_subtitle_blacklist", "") + + // ==== Audio ==== + + fun preferredAudioLanguages() = preferenceStore.getString("pref_audio_lang", "") + fun enablePitchCorrection() = preferenceStore.getBoolean("pref_audio_pitch_correction", true) + fun audioChannels() = preferenceStore.getEnum("pref_audio_config", AudioChannels.AutoSafe) + fun volumeBoostCap() = preferenceStore.getInt("pref_audio_volume_boost_cap", 30) + + // ==== Advanced ==== + + fun mpvScripts() = preferenceStore.getBoolean("mpv_scripts", false) + fun mpvConf() = preferenceStore.getString("pref_mpv_conf", "") + fun mpvInput() = preferenceStore.getString("pref_mpv_input", "") + + // ==== Non-preferences ==== + + fun autoplayEnabled() = preferenceStore.getBoolean("pref_auto_play_enabled", false) + + fun invertedPlayback() = preferenceStore.getEnum("pref_inverted_playback", InvertedPlayback.NONE) + + fun subSelectConf() = preferenceStore.getString("pref_sub_select_conf", "") + + fun playerSpeed() = preferenceStore.getFloat("pref_player_speed", 1F) + + fun aspectState() = preferenceStore.getEnum("pref_player_aspect_state", AspectState.FIT) + + fun screenshotSubtitles() = preferenceStore.getBoolean("pref_screenshot_subtitles", false) + + fun playerStatisticsPage() = preferenceStore.getInt("pref_player_statistics_page", 0) + + fun hardwareDecoding() = preferenceStore.getEnum("pref_hardware_decoding", HwDecState.defaultHwDec) fun rememberAudioDelay() = preferenceStore.getBoolean("pref_remember_audio_delay", false) fun audioDelay() = preferenceStore.getInt("pref_audio_delay", 0) @@ -122,8 +162,6 @@ class PlayerPreferences( fun borderColorSubtitles() = preferenceStore.getInt("pref_border_color_subtitles", -16777216) fun backgroundColorSubtitles() = preferenceStore.getInt("pref_background_color_subtitles", 0) - fun mpvScripts() = preferenceStore.getBoolean("mpv_scripts", false) - fun brightnessFilter() = preferenceStore.getInt("pref_player_filter_brightness") fun saturationFilter() = preferenceStore.getInt("pref_player_filter_saturation") fun contrastFilter() = preferenceStore.getInt("pref_player_filter_contrast") diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/viewer/PlayerEnums.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/viewer/PlayerEnums.kt index 80e9b6d2c9..414147a346 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/viewer/PlayerEnums.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/viewer/PlayerEnums.kt @@ -103,10 +103,32 @@ enum class PlayerStatsPage(val stringRes: StringResource) { * Player's debanding handler */ enum class VideoDebanding(val stringRes: StringResource) { - DISABLED(stringRes = MR.strings.pref_debanding_disabled), + NONE(stringRes = MR.strings.pref_debanding_none), CPU(stringRes = MR.strings.pref_debanding_cpu), GPU(stringRes = MR.strings.pref_debanding_gpu), - YUV420P(stringRes = MR.strings.pref_debanding_yuv420p), +} + +/** + * Action performed by a button, like double tap or media controls + */ +enum class SingleActionGesture(val stringRes: StringResource) { + None(stringRes = MR.strings.single_action_none), + Seek(stringRes = MR.strings.single_action_seek), + PlayPause(stringRes = MR.strings.single_action_playpause), + Switch(stringRes = MR.strings.single_action_switch), + Custom(stringRes = MR.strings.single_action_custom), +} + +/** + * Key codes sent through the `Custom` option in gestures + */ +enum class CustomKeyCodes(val keyCode: String) { + DoubleTapLeft("0x10001"), + DoubleTapCenter("0x10002"), + DoubleTapRight("0x10003"), + MediaPrevious("0x10004"), + MediaPlay("0x10005"), + MediaNext("0x10006"), } enum class AudioChannels(val propertyName: String, val propertyValue: String, val textRes: StringResource) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/PlayerSettingsScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/PlayerSettingsScreen.kt new file mode 100644 index 0000000000..488eb5470e --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/PlayerSettingsScreen.kt @@ -0,0 +1,61 @@ +package eu.kanade.tachiyomi.ui.setting + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Modifier +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.Navigator +import cafe.adriel.voyager.navigator.currentOrThrow +import eu.kanade.presentation.more.settings.screen.player.PlayerSettingsMainScreen +import eu.kanade.presentation.util.DefaultNavigatorScreenTransition +import eu.kanade.presentation.util.LocalBackPress +import eu.kanade.presentation.util.Screen +import eu.kanade.presentation.util.isTabletUi +import tachiyomi.presentation.core.components.TwoPanelBox + +object PlayerSettingsScreen : Screen() { + @Composable + override fun Content() { + val parentNavigator = LocalNavigator.currentOrThrow + if (!isTabletUi()) { + Navigator( + screen = PlayerSettingsMainScreen, + content = { + val pop: () -> Unit = { + if (it.canPop) { + it.pop() + } else { + parentNavigator.pop() + } + } + CompositionLocalProvider(LocalBackPress provides pop) { + DefaultNavigatorScreenTransition(navigator = it) + } + }, + ) + } else { + Navigator( + screen = PlayerSettingsMainScreen, + ) { + val insets = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal) + TwoPanelBox( + modifier = Modifier + .windowInsetsPadding(insets) + .consumeWindowInsets(insets), + startContent = { + CompositionLocalProvider(LocalBackPress provides parentNavigator::pop) { + PlayerSettingsMainScreen.Content(twoPane = true) + } + }, + endContent = { DefaultNavigatorScreenTransition(navigator = it) }, + ) + } + } + } +} diff --git a/app/src/main/java/mihon/core/migration/migrations/EnumsMigration.kt b/app/src/main/java/mihon/core/migration/migrations/EnumsMigration.kt index dde979b2e5..3ba7e2b94f 100644 --- a/app/src/main/java/mihon/core/migration/migrations/EnumsMigration.kt +++ b/app/src/main/java/mihon/core/migration/migrations/EnumsMigration.kt @@ -47,7 +47,7 @@ class EnumsMigration : Migration { preferenceStore.getEnum("pref_inverted_playback", InvertedPlayback.NONE).set(invertedPlayback) preferenceStore.getEnum("pref_hardware_decoding", HwDecState.defaultHwDec).set(hardwareDecoding) - preferenceStore.getEnum("pref_video_debanding", VideoDebanding.DISABLED).set(videoDebanding) + preferenceStore.getEnum("pref_video_debanding", VideoDebanding.NONE).set(videoDebanding) preferenceStore.getEnum("pref_player_aspect_state", AspectState.FIT).set(aspectState) preferenceStore.getBoolean("pref_gpu_next", false).set(gpuNext.get()) } diff --git a/app/src/main/java/mihon/core/migration/migrations/Migrations.kt b/app/src/main/java/mihon/core/migration/migrations/Migrations.kt index 00b8523444..7e0eaae921 100644 --- a/app/src/main/java/mihon/core/migration/migrations/Migrations.kt +++ b/app/src/main/java/mihon/core/migration/migrations/Migrations.kt @@ -44,4 +44,5 @@ val migrations: List LogOutMALMigration(), EnumsMigration(), TrustExtensionRepositoryMigration(), + VideoPlayerPreferenceMigration(), ) diff --git a/app/src/main/java/mihon/core/migration/migrations/VideoPlayerPreferenceMigration.kt b/app/src/main/java/mihon/core/migration/migrations/VideoPlayerPreferenceMigration.kt new file mode 100644 index 0000000000..97f504d4f3 --- /dev/null +++ b/app/src/main/java/mihon/core/migration/migrations/VideoPlayerPreferenceMigration.kt @@ -0,0 +1,46 @@ +package mihon.core.migration.migrations + +import android.app.Application +import androidx.core.content.edit +import androidx.preference.PreferenceManager +import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json +import mihon.core.migration.Migration +import mihon.core.migration.MigrationContext +import uy.kohesive.injekt.injectLazy + +class VideoPlayerPreferenceMigration : Migration { + override val version = 126f + + private val json: Json by injectLazy() + + override suspend fun invoke(migrationContext: MigrationContext): Boolean { + val context = migrationContext.get() ?: return false + val playerPreferences = migrationContext.get() ?: return false + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + + val subtitleConf = prefs.getString("pref_sub_select_conf", "")!! + val subtitleData = try { + json.decodeFromString(subtitleConf) + } catch (e: SerializationException) { + return false + } + + prefs.edit { + putString(playerPreferences.preferredSubLanguages().key(), subtitleData.lang.joinToString(",")) + putString(playerPreferences.subtitleWhitelist().key(), subtitleData.whitelist.joinToString(",")) + putString(playerPreferences.subtitleBlacklist().key(), subtitleData.blacklist.joinToString(",")) + } + + return true + } + + @Serializable + data class SubConfig( + val lang: List = emptyList(), + val blacklist: List = emptyList(), + val whitelist: List = emptyList(), + ) +} diff --git a/i18n/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml index 783ff6ce4c..b54e7f0dc4 100644 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -178,6 +178,119 @@ New to %s? We recommend checking out the getting started guide. Reinstalling %s? + + + Player settings + Search player settings + + + Internal player + Progress, controls, orientation + At what point to mark the episode as seen + 70% + 75% + 80% + 85% + 90% + 95% + 100% + Preserve watch position on seen episodes + Show content in display cutout + Hide player controls when opening the player + + Volume and Brightness + Enable Volume and Brightness Gestures + Remember and switch to the last used brightness + Remember and switch to the last used volume + + Orientation + Default orientation + Adjust the orientation based on a video\'s dimensions + Default portrait + Default landscape + Reverse landscape + Sensor portrait + Sensor landscape + + Picture-in-Picture (PiP) + Enable the use of PiP mode + Show episode toasts when switching episodes in PiP mode + Automatically switch to PiP mode on exiting the player + Replaces the "Skip 10 seconds" option with "Previous episode" + + External player + Always use external player + External player preference + + + Gestures + Seeking, double tap, media controls + + Double tap + Double tap (left) + Double tap (center) + Double tap (right) + When a tap gesture is set to "Custom", it can be bound through input.conf. The key codes are 0x10001 for left, 0x10002 for center, and 0x10003 for right. + None + Seek + Play/Pause + Switch episode + Custom + + Media controls + Previous + Play/Pause + Next + When a media control is set to "Custom", it can be bound through input.conf. The key codes are 0x10004 for previous, 0x10005 for play/pause, and 0x10006 for next. + + + Decoder + Hardware decoding, pixel format, debanding + Try hardware decoding + Enable gpu-next + A new video rendering backend + Debanding + None + CPU + GPU + YUV420P + Use YUV420P pixel format + May fix black screens on some video codecs, can also improve performance at the cost of quality + + + Subtitles + Preferred languages, whitelist, blacklist + Preferred languages + Subtitle language(s) to be selected by default on a video with multiple subtitles, Two- or three-letter languages codes work. Multiple values can be delimited by a comma. + Whitelist + Whitelist for subtitles. If a whitelist is defined, the first subtitle that contains a whitelisted word will be used. Multiple values can be delimited by a comma. + Blacklist + Blacklist for subtitles. If a blacklist is defined, all subtitles that contains a blacklisted word will be filtered out. Multiple values can be delimited by a comma. + + + Audio + Preferred languages, pitch correction, audio channels + Preferred languages + Audio language(s) to be selected by default on a video with multiple audio streams, Two- or three-letter languages codes work. Multiple values can be delimited by a comma. + Enable audio pitch correction + Prevents the audio from becoming high-pitched at faster speeds and low-pitched at slower speeds + Audio channels + Auto-safe + Auto + Mono + Stereo + Reverse stereo + Volume boost cap + + + Advanced + Scripts, mpv.conf, input.conf + Enable MPV scripts + Needs external storage permission. + Edit MPV configuration file for further player settings + Reset MPV configuration file + Edit MPV input file for keyboard mapping configuration + General Appearance @@ -900,28 +1013,10 @@ Manga in excluded categories will not be updated even if they are also in included categories. Anime in excluded categories will not be updated even if they are also in included categories. This extension is not from the official list. - Reverse landscape - Sensor portrait - Sensor landscape This extension is not from the official list. Player Progress - At what point to mark the episode as seen - 70% - 75% - 80% - 85% - 90% - 95% - 100% - Preserve watch position on seen episodes - Orientation - Default orientation - Adjust the orientation based on a video\'s dimensions - Default portrait - Default landscape Internal player - Volume and Brightness Seeking Default skip intro length Skip intro length @@ -940,28 +1035,7 @@ %1$s - E%2$s - %3$s Enable precise seeking When enabled, seeking will not focus on keyframes, leading to slower but precise seeking - Show content in display cutout - Hide player controls when opening the player - Audio channels - Auto-safe - Auto - Mono - Stereo - Reverse stereo - Picture-in-Picture (PiP) - Enable the use of PiP mode - Show episode toasts when switching episodes in PiP mode - Automatically switch to PiP mode on exiting the player - Replaces the "Skip 10 seconds" option with "Previous episode" - Remember and switch to the last used brightness - Remember and switch to the last used volume - Edit MPV configuration file for further player settings - Reset MPV configuration file - Edit MPV input file for keyboard mapping configuration Edit advanced subtitle track select configuration - External player - Always use external player - External player preference %1$s - %2$s %1$d%% Delete chapters/episodes @@ -1032,7 +1106,6 @@ Progress: %1$s Take screenshot Include Subtitles - Enable Volume and Brightness Gestures Enable Horizontal Seek Gesture Toggle statistics page Page 1 @@ -1040,13 +1113,6 @@ Page 3 Advanced player settings Debanding, mpv.conf… etc - Enable gpu-next - A new video rendering backend - Debanding - Disabled - CPU - GPU - YUV420P Ep. %1$s - %2$s Couldn\'t download due to low storage space Warning: large bulk downloads may lead to sources becoming slower and/or blocking Aniyomi. Tap to learn more. @@ -1182,6 +1248,4 @@ Number of threads to use for downloading, might get your IP blocked if too high, usually 4 is a good number to avoid heavy load on source servers. Download speed limit Set to 0 to disable the speed limit. - Enable MPV scripts - Needs external storage permission.