feat: Move player preferences into their own menu (#1819)

This commit is contained in:
Secozzi 2024-11-09 13:39:45 +01:00 committed by GitHub
parent 37e1834e2a
commit 33c813792b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 1169 additions and 316 deletions

View file

@ -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),

View file

@ -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<String>()
/**
* 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<String>,
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<String>()
/**
* A [PreferenceItem] for individual tracker.
*/

View file

@ -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)

View file

@ -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,

View file

@ -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<List<SearchResultItem>?>(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,

View file

@ -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<Preference> {
val playerPreferences = remember { Injekt.get<PlayerPreferences>() }
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,
),
)
}
}

View file

@ -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<Preference> {
val playerPreferences = remember { Injekt.get<PlayerPreferences>() }
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
},
),
)
}
}

View file

@ -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<Preference> {
val playerPreferences = remember { Injekt.get<PlayerPreferences>() }
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),
),
)
}
}

View file

@ -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<Preference> {
val playerPreferences = remember { Injekt.get<PlayerPreferences>() }
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))
}
},
)
}
}

View file

@ -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,
),
)
}

View file

@ -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<Preference> {
@ -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(

View file

@ -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<Preference> {
val playerPreferences = remember { Injekt.get<PlayerPreferences>() }
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),
),
)
}
}

View file

@ -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,

View file

@ -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)) },
)

View file

@ -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()

View file

@ -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 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)
fun hideControls() = preferenceStore.getBoolean("player_hide_controls", false)
fun screenshotSubtitles() = preferenceStore.getBoolean("pref_screenshot_subtitles", false)
fun gestureVolumeBrightness() = preferenceStore.getBoolean(
"pref_gesture_volume_brightness",
true,
fun defaultPlayerOrientationLandscape() = preferenceStore.getInt(
"pref_default_player_orientation_landscape_key",
6,
)
fun gestureHorizontalSeek() = preferenceStore.getBoolean("pref_gesture_horizontal_seek", true)
fun playerStatisticsPage() = preferenceStore.getInt("pref_player_statistics_page", 0)
// Internal player - PiP
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)
// 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)
// 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 videoDebanding() = preferenceStore.getEnum("pref_video_debanding", VideoDebanding.DISABLED)
fun gpuNext() = preferenceStore.getBoolean("pref_gpu_next", false)
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")

View file

@ -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) {

View file

@ -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) },
)
}
}
}
}

View file

@ -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())
}

View file

@ -44,4 +44,5 @@ val migrations: List<Migration>
LogOutMALMigration(),
EnumsMigration(),
TrustExtensionRepositoryMigration(),
VideoPlayerPreferenceMigration(),
)

View file

@ -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<Application>() ?: return false
val playerPreferences = migrationContext.get<PlayerPreferences>() ?: return false
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val subtitleConf = prefs.getString("pref_sub_select_conf", "")!!
val subtitleData = try {
json.decodeFromString<SubConfig>(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<String> = emptyList(),
val blacklist: List<String> = emptyList(),
val whitelist: List<String> = emptyList(),
)
}

View file

@ -178,6 +178,119 @@
<string name="onboarding_guides_new_user">New to %s? We recommend checking out the getting started guide.</string>
<string name="onboarding_guides_returning_user">Reinstalling %s?</string>
<!-- Preferences -->
<!-- Player settings -->
<string name="label_player_settings">Player settings</string>
<string name="action_search_player_settings">Search player settings</string>
<!-- Player settings - internal player -->
<string name="pref_player_internal">Internal player</string>
<string name="pref_player_internal_summary">Progress, controls, orientation</string>
<string name="pref_progress_mark_as_seen">At what point to mark the episode as seen</string>
<string name="pref_progress_70" translatable="false">70%</string>
<string name="pref_progress_75" translatable="false">75%</string>
<string name="pref_progress_80" translatable="false">80%</string>
<string name="pref_progress_85" translatable="false">85%</string>
<string name="pref_progress_90" translatable="false">90%</string>
<string name="pref_progress_95" translatable="false">95%</string>
<string name="pref_progress_100" translatable="false">100%</string>
<string name="pref_preserve_watching_position">Preserve watch position on seen episodes</string>
<string name="pref_player_fullscreen">Show content in display cutout</string>
<string name="pref_player_hide_controls">Hide player controls when opening the player</string>
<string name="pref_category_volume_brightness">Volume and Brightness</string>
<string name="enable_volume_brightness_gestures">Enable Volume and Brightness Gestures</string>
<string name="pref_remember_brightness">Remember and switch to the last used brightness</string>
<string name="pref_remember_volume">Remember and switch to the last used volume</string>
<string name="pref_category_player_orientation">Orientation</string>
<string name="pref_default_player_orientation">Default orientation</string>
<string name="pref_adjust_orientation_video_dimensions">Adjust the orientation based on a video\'s dimensions</string>
<string name="pref_default_portrait_orientation">Default portrait</string>
<string name="pref_default_landscape_orientation">Default landscape</string>
<string name="rotation_reverse_landscape">Reverse landscape</string>
<string name="rotation_sensor_portrait">Sensor portrait</string>
<string name="rotation_sensor_landscape">Sensor landscape</string>
<string name="pref_category_pip">Picture-in-Picture (PiP)</string>
<string name="pref_enable_pip">Enable the use of PiP mode</string>
<string name="pref_pip_episode_toasts">Show episode toasts when switching episodes in PiP mode</string>
<string name="pref_pip_on_exit">Automatically switch to PiP mode on exiting the player</string>
<string name="pref_pip_replace_with_previous">Replaces the "Skip 10 seconds" option with "Previous episode"</string>
<string name="pref_category_external_player">External player</string>
<string name="pref_always_use_external_player">Always use external player</string>
<string name="pref_external_player_preference">External player preference</string>
<!-- Player Settings - Gestures -->
<string name="pref_player_gestures">Gestures</string>
<string name="pref_player_gestures_summary">Seeking, double tap, media controls</string>
<string name="pref_category_double_tap">Double tap</string>
<string name="pref_left_double_tap">Double tap (left)</string>
<string name="pref_center_double_tap">Double tap (center)</string>
<string name="pref_right_double_tap">Double tap (right)</string>
<string name="pref_double_tap_info">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.</string>
<string name="single_action_none">None</string>
<string name="single_action_seek">Seek</string>
<string name="single_action_playpause">Play/Pause</string>
<string name="single_action_switch">Switch episode</string>
<string name="single_action_custom">Custom</string>
<string name="pref_category_media_controls">Media controls</string>
<string name="pref_media_previous">Previous</string>
<string name="pref_media_playpause">Play/Pause</string>
<string name="pref_media_next">Next</string>
<string name="pref_media_info">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.</string>
<!-- Player Settings - Decoder -->
<string name="pref_player_decoder">Decoder</string>
<string name="pref_player_decoder_summary">Hardware decoding, pixel format, debanding</string>
<string name="pref_try_hw">Try hardware decoding</string>
<string name="pref_gpu_next_title">Enable gpu-next</string>
<string name="pref_gpu_next_subtitle">A new video rendering backend</string>
<string name="pref_debanding_title">Debanding</string>
<string name="pref_debanding_none">None</string>
<string name="pref_debanding_cpu">CPU</string>
<string name="pref_debanding_gpu">GPU</string>
<string name="pref_debanding_yuv420p">YUV420P</string>
<string name="pref_use_yuv420p_title">Use YUV420P pixel format</string>
<string name="pref_use_yuv420p_subtitle">May fix black screens on some video codecs, can also improve performance at the cost of quality</string>
<!-- Player settings - Subtitles -->
<string name="pref_player_subtitle">Subtitles</string>
<string name="pref_player_subtitle_summary">Preferred languages, whitelist, blacklist</string>
<string name="pref_player_subtitle_lang">Preferred languages</string>
<string name="pref_player_subtitle_lang_info">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.</string>
<string name="pref_player_subtitle_whitelist">Whitelist</string>
<string name="pref_player_subtitle_whitelist_info">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.</string>
<string name="pref_player_subtitle_blacklist">Blacklist</string>
<string name="pref_player_subtitle_blacklist_info">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.</string>
<!-- Player settings - Audio -->
<string name="pref_player_audio">Audio</string>
<string name="pref_player_audio_summary">Preferred languages, pitch correction, audio channels</string>
<string name="pref_player_audio_lang">Preferred languages</string>
<string name="pref_player_audio_lang_info">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.</string>
<string name="pref_player_audio_pitch_correction">Enable audio pitch correction</string>
<string name="pref_player_audio_pitch_correction_summary">Prevents the audio from becoming high-pitched at faster speeds and low-pitched at slower speeds</string>
<string name="pref_player_audio_channels">Audio channels</string>
<string name="pref_player_audio_channels_auto_safe">Auto-safe</string>
<string name="pref_player_audio_channels_auto">Auto</string>
<string name="pref_player_audio_channels_mono">Mono</string>
<string name="pref_player_audio_channels_stereo">Stereo</string>
<string name="pref_player_audio_channels_reverse_stereo">Reverse stereo</string>
<string name="pref_player_audio_boost_cap">Volume boost cap</string>
<!-- Player settings - Advanced -->
<string name="pref_player_advanced">Advanced</string>
<string name="pref_player_advanced_summary">Scripts, mpv.conf, input.conf</string>
<string name="pref_mpv_scripts">Enable MPV scripts</string>
<string name="pref_mpv_scripts_summary">Needs external storage permission.</string>
<string name="pref_mpv_conf">Edit MPV configuration file for further player settings</string>
<string name="pref_reset_mpv_conf">Reset MPV configuration file</string>
<string name="pref_mpv_input">Edit MPV input file for keyboard mapping configuration</string>
<!-- Subsections -->
<string name="pref_category_general">General</string>
<string name="pref_category_appearance">Appearance</string>
@ -900,28 +1013,10 @@
<string name="pref_manga_library_update_categories_details">Manga in excluded categories will not be updated even if they are also in included categories.</string>
<string name="pref_anime_library_update_categories_details">Anime in excluded categories will not be updated even if they are also in included categories.</string>
<string name="unofficial_extension_message_aniyomi">This extension is not from the official list.</string>
<string name="rotation_reverse_landscape">Reverse landscape</string>
<string name="rotation_sensor_portrait">Sensor portrait</string>
<string name="rotation_sensor_landscape">Sensor landscape</string>
<string name="unofficial_anime_extension_message">This extension is not from the official list.</string>
<string name="pref_category_player">Player</string>
<string name="pref_category_progress">Progress</string>
<string name="pref_progress_mark_as_seen">At what point to mark the episode as seen</string>
<string name="pref_progress_70" translatable="false">70%</string>
<string name="pref_progress_75" translatable="false">75%</string>
<string name="pref_progress_80" translatable="false">80%</string>
<string name="pref_progress_85" translatable="false">85%</string>
<string name="pref_progress_90" translatable="false">90%</string>
<string name="pref_progress_95" translatable="false">95%</string>
<string name="pref_progress_100" translatable="false">100%</string>
<string name="pref_preserve_watching_position">Preserve watch position on seen episodes</string>
<string name="pref_category_player_orientation">Orientation</string>
<string name="pref_default_player_orientation">Default orientation</string>
<string name="pref_adjust_orientation_video_dimensions">Adjust the orientation based on a video\'s dimensions</string>
<string name="pref_default_portrait_orientation">Default portrait</string>
<string name="pref_default_landscape_orientation">Default landscape</string>
<string name="pref_category_internal_player">Internal player</string>
<string name="pref_category_volume_brightness">Volume and Brightness</string>
<string name="pref_category_player_seeking">Seeking</string>
<string name="pref_default_intro_length">Default skip intro length</string>
<string name="pref_intro_length">Skip intro length</string>
@ -940,28 +1035,7 @@
<string name="mpv_media_title">%1$s - E%2$s - %3$s</string>
<string name="pref_player_smooth_seek">Enable precise seeking</string>
<string name="pref_player_smooth_seek_summary">When enabled, seeking will not focus on keyframes, leading to slower but precise seeking</string>
<string name="pref_player_fullscreen">Show content in display cutout</string>
<string name="pref_player_hide_controls">Hide player controls when opening the player</string>
<string name="pref_player_audio_channels">Audio channels</string>
<string name="pref_player_audio_channels_auto_safe">Auto-safe</string>
<string name="pref_player_audio_channels_auto">Auto</string>
<string name="pref_player_audio_channels_mono">Mono</string>
<string name="pref_player_audio_channels_stereo">Stereo</string>
<string name="pref_player_audio_channels_reverse_stereo">Reverse stereo</string>
<string name="pref_category_pip">Picture-in-Picture (PiP)</string>
<string name="pref_enable_pip">Enable the use of PiP mode</string>
<string name="pref_pip_episode_toasts">Show episode toasts when switching episodes in PiP mode</string>
<string name="pref_pip_on_exit">Automatically switch to PiP mode on exiting the player</string>
<string name="pref_pip_replace_with_previous">Replaces the "Skip 10 seconds" option with "Previous episode"</string>
<string name="pref_remember_brightness">Remember and switch to the last used brightness</string>
<string name="pref_remember_volume">Remember and switch to the last used volume</string>
<string name="pref_mpv_conf">Edit MPV configuration file for further player settings</string>
<string name="pref_reset_mpv_conf">Reset MPV configuration file</string>
<string name="pref_mpv_input">Edit MPV input file for keyboard mapping configuration</string>
<string name="pref_sub_select_conf">Edit advanced subtitle track select configuration</string>
<string name="pref_category_external_player">External player</string>
<string name="pref_always_use_external_player">Always use external player</string>
<string name="pref_external_player_preference">External player preference</string>
<string name="player_title">%1$s - %2$s</string>
<string name="episode_download_progress">%1$d%%</string>
<string name="pref_category_delete_chapters">Delete chapters/episodes</string>
@ -1032,7 +1106,6 @@
<string name="episode_progress_no_total">Progress: %1$s</string>
<string name="screenshot_header">Take screenshot</string>
<string name="screenshot_show_subs">Include Subtitles</string>
<string name="enable_volume_brightness_gestures">Enable Volume and Brightness Gestures</string>
<string name="enable_horizontal_seek_gesture">Enable Horizontal Seek Gesture</string>
<string name="toggle_player_statistics_page">Toggle statistics page</string>
<string name="player_statistics_page_1">Page 1</string>
@ -1040,13 +1113,6 @@
<string name="player_statistics_page_3">Page 3</string>
<string name="pref_category_player_advanced">Advanced player settings</string>
<string name="pref_category_player_advanced_subtitle">Debanding, mpv.conf… etc</string>
<string name="pref_gpu_next_title">Enable gpu-next</string>
<string name="pref_gpu_next_subtitle">A new video rendering backend</string>
<string name="pref_debanding_title">Debanding</string>
<string name="pref_debanding_disabled">Disabled</string>
<string name="pref_debanding_cpu">CPU</string>
<string name="pref_debanding_gpu">GPU</string>
<string name="pref_debanding_yuv420p">YUV420P</string>
<string name="recent_anime_time">Ep. %1$s - %2$s</string>
<string name="download_insufficient_space">Couldn\'t download due to low storage space</string>
<string name="download_queue_size_warning">Warning: large bulk downloads may lead to sources becoming slower and/or blocking Aniyomi. Tap to learn more.</string>
@ -1182,6 +1248,4 @@
<string name="download_threads_number_summary">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.</string>
<string name="download_speed_limit">Download speed limit</string>
<string name="download_speed_limit_hint">Set to 0 to disable the speed limit.</string>
<string name="pref_mpv_scripts">Enable MPV scripts</string>
<string name="pref_mpv_scripts_summary">Needs external storage permission.</string>
</resources>