feat(player): Subtitle settings + refactor + crash fixes (#1152)

Co-authored-by: jmir1 <jhmiramon@gmail.com>
Co-authored-by: Abdallah <54363735+abdallahmehiz@users.noreply.github.com>
This commit is contained in:
Quickdev 2023-10-27 15:03:11 -04:00 committed by GitHub
parent afb921a5a2
commit 7f9255b513
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
61 changed files with 1897 additions and 1070 deletions

View file

@ -16,9 +16,11 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.core.view.WindowInsetsControllerCompat
import cafe.adriel.voyager.core.lifecycle.DisposableEffectIgnoringConfiguration
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.transitions.ScreenTransition
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import eu.kanade.presentation.util.Screen
import eu.kanade.presentation.util.isTabletUi
import tachiyomi.presentation.core.components.AdaptiveSheet as AdaptiveSheetImpl
@ -74,6 +76,7 @@ fun NavigatorAdaptiveSheet(
*/
@Composable
fun AdaptiveSheet(
hideSystemBars: Boolean = false,
tonalElevation: Dp = 1.dp,
enableSwipeDismiss: Boolean = true,
onDismissRequest: () -> Unit,
@ -87,13 +90,19 @@ fun AdaptiveSheet(
decorFitsSystemWindows = false,
),
) {
if (hideSystemBars) {
rememberSystemUiController().apply {
isSystemBarsVisible = false
systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
}
AdaptiveSheetImpl(
isTabletUi = isTabletUi,
tonalElevation = tonalElevation,
enableSwipeDismiss = enableSwipeDismiss,
onDismissRequest = onDismissRequest,
) {
val contentPadding = if (isTabletUi) {
val contentPadding = if (isTabletUi || hideSystemBars) {
PaddingValues()
} else {
WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()

View file

@ -1,12 +1,18 @@
package eu.kanade.presentation.components
import android.view.MotionEvent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.ContentAlpha
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.AddCircle
import androidx.compose.material.icons.outlined.RemoveCircle
import androidx.compose.material.icons.rounded.CheckBox
import androidx.compose.material.icons.rounded.CheckBoxOutlineBlank
import androidx.compose.material.icons.rounded.DisabledByDefault
@ -14,17 +20,23 @@ import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
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.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInteropFilter
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import tachiyomi.domain.entries.TriStateFilter
import tachiyomi.presentation.core.components.SettingsItemsPaddings
@ -126,3 +138,96 @@ fun SelectItem(
}
}
}
@Composable
fun RepeatingIconButton(
modifier: Modifier = Modifier,
onClick: () -> Unit,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
maxDelayMillis: Long = 750,
minDelayMillis: Long = 5,
delayDecayFactor: Float = .25f,
content: @Composable () -> Unit,
) {
val currentClickListener by rememberUpdatedState(onClick)
var pressed by remember { mutableStateOf(false) }
IconButton(
modifier = modifier.pointerInteropFilter {
pressed = when (it.action) {
MotionEvent.ACTION_DOWN -> true
else -> false
}
true
},
onClick = {},
enabled = enabled,
interactionSource = interactionSource,
content = content,
)
LaunchedEffect(pressed, enabled) {
var currentDelayMillis = maxDelayMillis
while (enabled && pressed) {
currentClickListener()
delay(currentDelayMillis)
currentDelayMillis =
(currentDelayMillis - (currentDelayMillis * delayDecayFactor))
.toLong().coerceAtLeast(minDelayMillis)
}
}
}
@Composable
fun OutlinedNumericChooser(
label: String,
placeholder: String,
suffix: String,
value: Int,
step: Int,
min: Int? = null,
onValueChanged: (Int) -> Unit,
) {
var currentValue = value
val updateValue: (Boolean) -> Unit = {
currentValue += if (it) step else -step
if (min != null) currentValue = if (currentValue < min) min else currentValue
onValueChanged(currentValue)
}
Row(verticalAlignment = Alignment.CenterVertically) {
RepeatingIconButton(
onClick = { updateValue(false) },
) { Icon(imageVector = Icons.Outlined.RemoveCircle, contentDescription = null) }
OutlinedTextField(
value = "%d".format(currentValue),
modifier = Modifier.widthIn(min = 140.dp),
onValueChange = {
// Don't allow multiple decimal points, non-numeric characters, or leading zeros
currentValue = it.trim().replace(Regex("[^-\\d.]"), "").toIntOrNull()
?: currentValue
onValueChanged(currentValue)
},
label = { Text(text = label) },
placeholder = { Text(text = placeholder) },
suffix = { Text(text = suffix) },
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
)
RepeatingIconButton(
onClick = { updateValue(true) },
) { Icon(imageVector = Icons.Outlined.AddCircle, contentDescription = null) }
}
}

View file

@ -23,6 +23,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
@ -43,9 +44,13 @@ fun TabbedDialog(
onDismissRequest: () -> Unit,
tabTitles: List<String>,
tabOverflowMenuContent: (@Composable ColumnScope.(() -> Unit) -> Unit)? = null,
onOverflowMenuClicked: (() -> Unit)? = null,
overflowIcon: ImageVector? = null,
hideSystemBars: Boolean = false,
content: @Composable (PaddingValues, Int) -> Unit,
) {
AdaptiveSheet(
hideSystemBars = hideSystemBars,
onDismissRequest = onDismissRequest,
) { contentPadding ->
val scope = rememberCoroutineScope()
@ -78,7 +83,7 @@ fun TabbedDialog(
}
}
tabOverflowMenuContent?.let { MoreMenu(it) }
MoreMenu(onOverflowMenuClicked, tabOverflowMenuContent, overflowIcon)
}
Divider()
@ -96,21 +101,29 @@ fun TabbedDialog(
@Composable
private fun MoreMenu(
content: @Composable ColumnScope.(() -> Unit) -> Unit,
onClickIcon: (() -> Unit)?,
content: @Composable (ColumnScope.(() -> Unit) -> Unit)?,
overflowIcon: ImageVector? = null,
) {
if (onClickIcon == null && content == null) return
var expanded by remember { mutableStateOf(false) }
val onClick = onClickIcon ?: { expanded = true }
Box(modifier = Modifier.wrapContentSize(Alignment.TopStart)) {
IconButton(onClick = { expanded = true }) {
IconButton(onClick = onClick) {
Icon(
imageVector = Icons.Default.MoreVert,
imageVector = overflowIcon ?: Icons.Default.MoreVert,
contentDescription = stringResource(R.string.label_more),
)
}
if (onClickIcon == null) {
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
) {
content { expanded = false }
content!! { expanded = false }
}
}
}
}

View file

@ -18,10 +18,10 @@ object AdvancedPlayerSettingsScreen : SearchableSettings {
@Composable
override fun getPreferences(): List<Preference> {
val playerPreferences = remember { Injekt.get<PlayerPreferences>() }
val scope = rememberCoroutineScope()
val context = LocalContext.current
val mpvConf = playerPreferences.mpvConf()
val mpvInput = playerPreferences.mpvInput()
val scope = rememberCoroutineScope()
return listOf(
Preference.PreferenceItem.MultiLineEditTextPreference(

View file

@ -48,23 +48,24 @@ import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.databinding.PlayerActivityBinding
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences
import eu.kanade.tachiyomi.ui.player.settings.PlayerTracksBuilder
import eu.kanade.tachiyomi.ui.player.settings.dialogs.DefaultDecoderDialog
import eu.kanade.tachiyomi.ui.player.settings.PlayerSettingsScreenModel
import eu.kanade.tachiyomi.ui.player.settings.dialogs.EpisodeListDialog
import eu.kanade.tachiyomi.ui.player.settings.dialogs.SkipIntroLengthDialog
import eu.kanade.tachiyomi.ui.player.settings.dialogs.SpeedPickerDialog
import eu.kanade.tachiyomi.ui.player.settings.sheets.PlayerChaptersSheet
import eu.kanade.tachiyomi.ui.player.settings.sheets.PlayerOptionsSheet
import eu.kanade.tachiyomi.ui.player.settings.sheets.PlayerScreenshotSheet
import eu.kanade.tachiyomi.ui.player.settings.sheets.PlayerSettingsSheet
import eu.kanade.tachiyomi.ui.player.settings.sheets.ScreenshotOptionsSheet
import eu.kanade.tachiyomi.ui.player.settings.sheets.TracksCatalogSheet
import eu.kanade.tachiyomi.ui.player.settings.sheets.VideoChaptersSheet
import eu.kanade.tachiyomi.ui.player.settings.sheets.subtitle.SubtitleSettingsSheet
import eu.kanade.tachiyomi.ui.player.settings.sheets.subtitle.toHexString
import eu.kanade.tachiyomi.ui.player.viewer.ACTION_MEDIA_CONTROL
import eu.kanade.tachiyomi.ui.player.viewer.AspectState
import eu.kanade.tachiyomi.ui.player.viewer.CONTROL_TYPE_NEXT
import eu.kanade.tachiyomi.ui.player.viewer.CONTROL_TYPE_PAUSE
import eu.kanade.tachiyomi.ui.player.viewer.CONTROL_TYPE_PLAY
import eu.kanade.tachiyomi.ui.player.viewer.CONTROL_TYPE_PREVIOUS
import eu.kanade.tachiyomi.ui.player.viewer.EXTRA_CONTROL_TYPE
import eu.kanade.tachiyomi.ui.player.viewer.GestureHandler
import eu.kanade.tachiyomi.ui.player.viewer.HwDecState
import eu.kanade.tachiyomi.ui.player.viewer.PictureInPictureHandler
import eu.kanade.tachiyomi.ui.player.viewer.PipState
import eu.kanade.tachiyomi.ui.player.viewer.SeekState
@ -78,7 +79,6 @@ import eu.kanade.tachiyomi.util.system.toShareIntent
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.setComposeContent
import `is`.xyz.mpv.MPVLib
import `is`.xyz.mpv.MPVView.Chapter
import `is`.xyz.mpv.Utils
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@ -88,14 +88,15 @@ import kotlinx.coroutines.runBlocking
import logcat.LogPriority
import tachiyomi.core.util.lang.launchNonCancellable
import tachiyomi.core.util.lang.launchUI
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.io.InputStream
import kotlin.math.abs
import kotlin.math.roundToInt
import `is`.xyz.mpv.MPVView.Chapter as VideoChapter
class PlayerActivity : BaseActivity() {
@ -136,7 +137,7 @@ class PlayerActivity : BaseActivity() {
setInitialEpisodeError(exception)
}
}
lifecycleScope.launch { setVideoList(quality = 0, initResult.first!!) }
lifecycleScope.launch { setVideoList(qualityIndex = 0, initResult.first!!) }
}
super.onNewIntent(intent)
}
@ -207,8 +208,6 @@ class PlayerActivity : BaseActivity() {
null
}
private var playerSettingsSheet: PlayerSettingsSheet? = null
@Suppress("DEPRECATION")
private fun requestAudioFocus() {
if (hasAudioFocus) return
@ -251,19 +250,21 @@ class PlayerActivity : BaseActivity() {
private var playerIsDestroyed = true
private var subTracks: Array<Track> = emptyArray()
private var selectedQualityIndex = 0
private var selectedSub = 0
private var subtitleTracks: Array<Track> = emptyArray()
private var selectedSubtitleIndex = 0
private var hadPreviousSubs = false
private var audioTracks: Array<Track> = emptyArray()
private var selectedAudio = 0
private var selectedAudioIndex = 0
private var hadPreviousAudio = false
private var videoChapters: List<Chapter> = emptyList()
private var videoChapters: List<VideoChapter> = emptyList()
set(value) {
field = value
runOnUiThread {
@ -288,6 +289,7 @@ class PlayerActivity : BaseActivity() {
setupMediaSession()
setupPlayerMPV()
setupPlayerAudio()
setupPlayerSubtitles()
setupPlayerBrightness()
loadDeviceDimensions()
@ -332,7 +334,7 @@ class PlayerActivity : BaseActivity() {
dateFormat = viewModel.dateFormat,
onBookmarkClicked = viewModel::bookmarkEpisode,
onEpisodeClicked = this::changeEpisode,
onDismissRequest = pauseForDialog(),
onDismissRequest = pauseForDialogSheet(),
)
}
}
@ -345,19 +347,7 @@ class PlayerActivity : BaseActivity() {
SpeedPickerDialog(
currentSpeed = MPVLib.getPropertyDouble("speed"),
onSpeedChanged = ::updateSpeed,
onDismissRequest = pauseForDialog(),
)
}
is PlayerViewModel.Dialog.DefaultDecoder -> {
fun updateDecoder(newDec: String) {
playerPreferences.standardHwDec().set(newDec)
mpvUpdateHwDec(HwDecState.get(newDec))
}
DefaultDecoderDialog(
currentDecoder = playerPreferences.standardHwDec().get(),
onSelectDecoder = ::updateDecoder,
onDismissRequest = pauseForDialog(),
onDismissRequest = pauseForDialogSheet(),
)
}
@ -368,7 +358,7 @@ class PlayerActivity : BaseActivity() {
defaultSkipIntroLength = playerPreferences.defaultIntroLength().get(),
fromPlayer = true,
updateSkipIntroLength = viewModel::setAnimeSkipIntroLength,
onDismissRequest = pauseForDialog(),
onDismissRequest = pauseForDialogSheet(),
)
}
}
@ -377,6 +367,115 @@ class PlayerActivity : BaseActivity() {
}
}
binding.sheetRoot.setComposeContent {
val state by viewModel.state.collectAsState()
when (state.sheet) {
is PlayerViewModel.Sheet.ScreenshotOptions -> {
ScreenshotOptionsSheet(
screenModel = PlayerSettingsScreenModel(viewModel.playerPreferences),
cachePath = cacheDir.path,
onSetAsCover = viewModel::setAsCover,
onShare = { viewModel.shareImage(it, player.timePos) },
onSave = { viewModel.saveImage(it, player.timePos) },
onDismissRequest = pauseForDialogSheet(),
)
}
is PlayerViewModel.Sheet.PlayerSettings -> {
PlayerSettingsSheet(
screenModel = PlayerSettingsScreenModel(viewModel.playerPreferences),
onDismissRequest = pauseForDialogSheet(),
)
}
is PlayerViewModel.Sheet.VideoChapters -> {
fun setChapter(videoChapter: VideoChapter, text: String) {
val seekDifference = videoChapter.time.roundToInt() - (player.timePos ?: 0)
doubleTapSeek(time = seekDifference, isDoubleTap = false, videoChapterText = text)
}
VideoChaptersSheet(
timePosition = player.timePos ?: 0,
videoChapters = videoChapters,
onVideoChapterSelected = ::setChapter,
onDismissRequest = pauseForDialogSheet(),
)
}
is PlayerViewModel.Sheet.TracksCatalog -> {
val qualityTracks = currentVideoList?.map { Track("", it.quality) }?.toTypedArray()?.takeUnless { it.isEmpty() }
val subtitleTracks = subtitleTracks.takeUnless { it.isEmpty() }
val audioTracks = audioTracks.takeUnless { it.isEmpty() }
if (qualityTracks != null && subtitleTracks != null && audioTracks != null) {
fun onQualitySelected(qualityIndex: Int) {
if (playerIsDestroyed) return
if (selectedQualityIndex == qualityIndex) return
showLoadingIndicator(true)
logcat(LogPriority.INFO) { "Changing quality" }
setVideoList(qualityIndex, currentVideoList)
}
fun onSubtitleSelected(index: Int) {
if (selectedSubtitleIndex == index || selectedSubtitleIndex > subtitleTracks.lastIndex) return
selectedSubtitleIndex = index
if (index == 0) {
player.sid = -1
return
}
val tracks = player.tracks.getValue("sub")
val selectedLoadedTrack = tracks.firstOrNull {
it.name == subtitleTracks[index].url ||
it.mpvId.toString() == subtitleTracks[index].url
}
selectedLoadedTrack?.let { player.sid = it.mpvId }
?: MPVLib.command(arrayOf("sub-add", subtitleTracks[index].url, "select", subtitleTracks[index].url))
}
fun onAudioSelected(index: Int) {
if (selectedAudioIndex == index || selectedAudioIndex > audioTracks.lastIndex) return
selectedAudioIndex = index
if (index == 0) {
player.aid = -1
return
}
val tracks = player.tracks.getValue("audio")
val selectedLoadedTrack = tracks.firstOrNull {
it.name == audioTracks[index].url ||
it.mpvId.toString() == audioTracks[index].url
}
selectedLoadedTrack?.let { player.aid = it.mpvId }
?: MPVLib.command(arrayOf("audio-add", audioTracks[index].url, "select", audioTracks[index].url))
}
TracksCatalogSheet(
isEpisodeOnline = viewModel.isEpisodeOnline(),
qualityTracks = qualityTracks,
subtitleTracks = subtitleTracks,
audioTracks = audioTracks,
selectedQualityIndex = selectedQualityIndex,
selectedSubtitleIndex = selectedSubtitleIndex,
selectedAudioIndex = selectedAudioIndex,
onQualitySelected = ::onQualitySelected,
onSubtitleSelected = ::onSubtitleSelected,
onAudioSelected = ::onAudioSelected,
onSettingsClicked = viewModel::showSubtitleSettings,
onDismissRequest = pauseForDialogSheet(),
)
}
}
is PlayerViewModel.Sheet.SubtitleSettings -> {
SubtitleSettingsSheet(
screenModel = PlayerSettingsScreenModel(viewModel.playerPreferences, subtitleTracks.size > 1),
onDismissRequest = pauseForDialogSheet(fadeControls = true),
)
}
null -> {}
}
}
playerIsDestroyed = false
registerReceiver(
@ -412,18 +511,28 @@ class PlayerActivity : BaseActivity() {
val logLevel = if (viewModel.networkPreferences.verboseLogging().get()) "info" else "warn"
player.initialize(applicationContext.filesDir.path, logLevel)
val speedProperty = MPVLib.getPropertyDouble("speed")
val currentSpeed = if (speedProperty == 1.0) playerPreferences.playerSpeed().get().toDouble() else speedProperty
MPVLib.setPropertyDouble("speed", currentSpeed)
MPVLib.observeProperty("chapter-list", MPVLib.mpvFormat.MPV_FORMAT_NONE)
MPVLib.setPropertyDouble("speed", playerPreferences.playerSpeed().get().toDouble())
MPVLib.setOptionString("keep-open", "always")
MPVLib.setOptionString("ytdl", "no")
mpvUpdateHwDec(HwDecState.get(playerPreferences.standardHwDec().get()))
MPVLib.setOptionString("hwdec", playerPreferences.hwDec().get())
when (playerPreferences.deband().get()) {
1 -> MPVLib.setOptionString("vf", "gradfun=radius=12")
2 -> MPVLib.setOptionString("deband", "yes")
3 -> MPVLib.setOptionString("vf", "format=yuv420p")
}
val currentPlayerStatisticsPage = playerPreferences.playerStatisticsPage().get()
if (currentPlayerStatisticsPage != 0) {
MPVLib.command(arrayOf("script-binding", "stats/display-stats-toggle"))
MPVLib.command(arrayOf("script-binding", "stats/display-page-$currentPlayerStatisticsPage"))
}
MPVLib.setOptionString("input-default-bindings", "yes")
MPVLib.addLogObserver(playerObserver)
@ -440,6 +549,10 @@ class PlayerActivity : BaseActivity() {
playerPreferences.playerVolumeValue().get()
}
if (playerPreferences.rememberAudioDelay().get()) {
MPVLib.setPropertyDouble("audio-delay", (playerPreferences.audioDelay().get() / 1000).toDouble())
}
verticalScrollRight(0F)
maxVolume = audioManager!!.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
@ -449,6 +562,24 @@ class PlayerActivity : BaseActivity() {
volumeControlStream = AudioManager.STREAM_MUSIC
}
private fun setupPlayerSubtitles() {
with(playerPreferences) {
val overrideType = if (overrideSubsASS().get()) "force" else "no"
MPVLib.setPropertyString("sub-ass-override", overrideType)
if (rememberSubtitlesDelay().get()) {
MPVLib.setPropertyDouble("sub-delay", subtitlesDelay().get().toDouble())
}
MPVLib.setPropertyString("sub-bold", if (boldSubtitles().get()) "yes" else "no")
MPVLib.setPropertyString("sub-italic", if (italicSubtitles().get()) "yes" else "no")
MPVLib.setPropertyInt("sub-font-size", subtitleFontSize().get())
MPVLib.setPropertyString("sub-color", textColorSubtitles().get().toHexString())
MPVLib.setPropertyString("sub-border-color", borderColorSubtitles().get().toHexString())
MPVLib.setPropertyString("sub-back-color", backgroundColorSubtitles().get().toHexString())
}
}
private fun setupPlayerBrightness() {
val useDeviceBrightness = playerPreferences.playerBrightnessValue().get() == -1.0F || !playerPreferences.rememberPlayerBrightness().get()
brightness = if (useDeviceBrightness) {
@ -581,14 +712,28 @@ class PlayerActivity : BaseActivity() {
} else {
switchControlsOrientation(true)
}
val aspectProperty = MPVLib.getPropertyString("video-aspect-override").toDouble()
AspectState.mode = if (aspectProperty != -1.0 && aspectProperty != (deviceWidth / deviceHeight).toDouble()) {
AspectState.CUSTOM
} else {
AspectState.get(playerPreferences.playerViewMode().get())
}
private fun pauseForDialog(): () -> Unit {
playerControls.setViewMode(showText = false)
}
private fun pauseForDialogSheet(fadeControls: Boolean = false): () -> Unit {
val wasPlayerPaused = player.paused ?: true // default to not changing state
player.paused = true
if (!fadeControls) playerControls.fadeOutControlsRunnable.run()
return {
if (!wasPlayerPaused) player.paused = false
viewModel.closeDialog()
if (!wasPlayerPaused) {
player.paused = false
} else {
playerControls.showAndFadeControls()
}
viewModel.closeDialogSheet()
refreshUi()
}
}
@ -700,7 +845,7 @@ class PlayerActivity : BaseActivity() {
rightToLeft = playerControls.binding.toggleAutoplay.id
rightToRight = ConstraintLayout.LayoutParams.UNSET
}
playerControls.binding.playerOverflow.updateLayoutParams<ConstraintLayout.LayoutParams> {
playerControls.binding.settingsBtn.updateLayoutParams<ConstraintLayout.LayoutParams> {
topToTop = ConstraintLayout.LayoutParams.PARENT_ID
topToBottom = ConstraintLayout.LayoutParams.UNSET
}
@ -717,7 +862,7 @@ class PlayerActivity : BaseActivity() {
rightToLeft = ConstraintLayout.LayoutParams.UNSET
rightToRight = ConstraintLayout.LayoutParams.PARENT_ID
}
playerControls.binding.playerOverflow.updateLayoutParams<ConstraintLayout.LayoutParams> {
playerControls.binding.settingsBtn.updateLayoutParams<ConstraintLayout.LayoutParams> {
topToTop = ConstraintLayout.LayoutParams.UNSET
topToBottom = playerControls.binding.episodeListBtn.id
}
@ -727,12 +872,8 @@ class PlayerActivity : BaseActivity() {
}
}
setupGestures()
playerControls.setViewMode(showText = false)
if (pip.supportedAndEnabled) player.paused?.let { pip.update(!it) }
viewModel.closeDialog()
if (playerSettingsSheet?.isShowing == true) {
playerSettingsSheet!!.dismiss()
}
viewModel.closeDialogSheet()
}
}
@ -760,10 +901,7 @@ class PlayerActivity : BaseActivity() {
*/
internal fun changeEpisode(episodeId: Long?, autoPlay: Boolean = false) {
animationHandler.removeCallbacks(nextEpisodeRunnable)
viewModel.closeDialog()
if (playerSettingsSheet?.isShowing == true) {
playerSettingsSheet!!.dismiss()
}
viewModel.closeDialogSheet()
player.paused = true
showLoadingIndicator(true)
@ -787,7 +925,7 @@ class PlayerActivity : BaseActivity() {
if (switchMethod.first != null) {
when {
switchMethod.first!!.isEmpty() -> setInitialEpisodeError(Exception("Video list is empty."))
else -> setVideoList(quality = 0, switchMethod.first!!)
else -> setVideoList(qualityIndex = 0, switchMethod.first!!)
}
} else {
logcat(LogPriority.ERROR) { "Error getting links" }
@ -871,8 +1009,7 @@ class PlayerActivity : BaseActivity() {
time: Int,
event: MotionEvent? = null,
isDoubleTap: Boolean = true,
isChapter: Boolean = false,
text: String? = null,
videoChapterText: String? = null,
) {
if (SeekState.mode != SeekState.DOUBLE_TAP) {
doubleTapBg = if (time < 0) binding.rewBg else binding.ffwdBg
@ -902,8 +1039,8 @@ class PlayerActivity : BaseActivity() {
}
doubleTapBg.visibility = View.VISIBLE
if (isChapter) {
binding.secondsView.binding.doubleTapSeconds.text = text
if (videoChapterText != null) {
binding.secondsView.binding.doubleTapSeconds.text = videoChapterText
} else {
binding.secondsView.seconds -= time
}
@ -921,13 +1058,13 @@ class PlayerActivity : BaseActivity() {
}
doubleTapBg.visibility = View.VISIBLE
if (isChapter) {
binding.secondsView.binding.doubleTapSeconds.text = text
if (videoChapterText != null) {
binding.secondsView.binding.doubleTapSeconds.text = videoChapterText
} else {
binding.secondsView.seconds += time
}
}
if (!isChapter) {
if (videoChapterText == null) {
playerControls.hideUiForSeek()
}
binding.secondsView.start()
@ -1077,179 +1214,6 @@ class PlayerActivity : BaseActivity() {
return super.onKeyUp(keyCode, event)
}
@Suppress("UNUSED_PARAMETER")
fun openTracksSheet(view: View) {
val qualityTracks = currentVideoList?.map { Track("", it.quality) }?.toTypedArray()?.takeUnless { it.isEmpty() }
val subTracks = subTracks.takeUnless { it.isEmpty() }
val audioTracks = audioTracks.takeUnless { it.isEmpty() }
if (qualityTracks == null || subTracks == null || audioTracks == null) return
if (playerSettingsSheet?.isShowing == true) return
playerControls.hideControls(true)
playerSettingsSheet = PlayerSettingsSheet(this@PlayerActivity).apply { show() }
}
private var selectedQuality = 0
internal fun qualityTracksTab(dismissSheet: () -> Unit): PlayerTracksBuilder {
val videoTracks = currentVideoList!!.map {
Track("", it.quality)
}.toTypedArray().takeUnless { it.isEmpty() }!!
return PlayerTracksBuilder(
activity = this,
changeTrackMethod = ::changeQuality,
tracks = videoTracks,
preselectedTrack = selectedQuality,
dismissSheet = dismissSheet,
trackSettings = null,
)
}
private fun changeQuality(quality: Int) {
if (playerIsDestroyed) return
if (selectedQuality == quality) return
showLoadingIndicator(true)
logcat(LogPriority.INFO) { "Changing quality" }
setVideoList(quality, currentVideoList)
}
private fun setChapter(chapter: Chapter) {
val seekDifference = chapter.time.roundToInt() - (player.timePos ?: 0)
doubleTapSeek(
time = seekDifference,
isDoubleTap = false,
isChapter = true,
text = chapter.title.takeIf { !it.isNullOrBlank() }
?: Utils.prettyTime(chapter.time.roundToInt()),
)
}
@Suppress("UNUSED_PARAMETER")
fun pickChapter(view: View) {
playerControls.hideControls(true)
PlayerChaptersSheet(
activity = this@PlayerActivity,
textRes = R.string.chapter_dialog_header,
seekToChapterMethod = ::setChapter,
chapters = videoChapters,
).show()
}
internal fun subtitleTracksTab(dismissTab: () -> Unit): PlayerTracksBuilder {
val subTracks = subTracks.takeUnless { it.isEmpty() }!!
playerControls.hideControls(true)
return PlayerTracksBuilder(
activity = this,
changeTrackMethod = ::setSub,
tracks = subTracks,
preselectedTrack = selectedSub,
dismissSheet = dismissTab,
trackSettings = null,
)
}
private fun setSub(index: Int) {
if (selectedSub == index || selectedSub > subTracks.lastIndex) return
selectedSub = index
if (index == 0) {
player.sid = -1
return
}
val tracks = player.tracks.getValue("sub")
val selectedLoadedTrack = tracks.firstOrNull {
it.name == subTracks[index].url ||
it.mpvId.toString() == subTracks[index].url
}
selectedLoadedTrack?.let { player.sid = it.mpvId }
?: MPVLib.command(arrayOf("sub-add", subTracks[index].url, "select", subTracks[index].url))
}
internal fun audioTracksTab(dismissTab: () -> Unit): PlayerTracksBuilder {
val audioTracks = audioTracks.takeUnless { it.isEmpty() }!!
playerControls.hideControls(true)
return PlayerTracksBuilder(
activity = this,
changeTrackMethod = ::setAudio,
tracks = audioTracks,
preselectedTrack = selectedAudio,
dismissSheet = dismissTab,
trackSettings = null,
)
}
private fun setAudio(index: Int) {
if (selectedAudio == index || selectedAudio > audioTracks.lastIndex) return
selectedAudio = index
if (index == 0) {
player.aid = -1
return
}
val tracks = player.tracks.getValue("audio")
val selectedLoadedTrack = tracks.firstOrNull {
it.name == audioTracks[index].url ||
it.mpvId.toString() == audioTracks[index].url
}
selectedLoadedTrack?.let { player.aid = it.mpvId }
?: MPVLib.command(arrayOf("audio-add", audioTracks[index].url, "select", audioTracks[index].url))
}
fun openScreenshotSheet() {
playerControls.hideControls(true)
PlayerScreenshotSheet(this@PlayerActivity).show()
}
@Suppress("UNUSED_PARAMETER")
fun openOptionsSheet(view: View) {
playerControls.hideControls(true)
PlayerOptionsSheet(this@PlayerActivity).show()
}
var stats: Boolean = false
var statsPage: Int = 0
set(value) {
val newValue = when (value) {
0 -> 1
1 -> 2
2 -> 3
else -> 1
}
if (!stats) toggleStats()
MPVLib.command(arrayOf("script-binding", "stats/display-page-$newValue"))
field = newValue - 1
}
fun toggleStats() {
MPVLib.command(arrayOf("script-binding", "stats/display-stats-toggle"))
stats = !stats
}
private fun takeScreenshot(): InputStream? {
val filename = cacheDir.path + "/${System.currentTimeMillis()}_mpv_screenshot_tmp.png"
val subtitleFlag = if (playerPreferences.screenshotSubtitles().get()) {
"subtitles"
} else {
"video"
}
MPVLib.command(arrayOf("screenshot-to-file", filename, subtitleFlag))
val tempFile = File(filename).takeIf { it.exists() } ?: return null
val newFile = File(cacheDir.path + "/mpv_screenshot.png")
newFile.delete()
tempFile.renameTo(newFile)
return newFile.takeIf { it.exists() }?.inputStream()
}
/**
* Called from the options sheet. It delegates the call to the presenter to do some IO, which
* will call [onShareImageResult] with the path the image was saved on when it's ready.
*/
fun shareImage() {
viewModel.shareImage({ takeScreenshot()!! }, player.timePos)
}
/**
* Called from the presenter when a screenshot is ready to be shared. It shows Android's
* default sharing tool.
@ -1265,14 +1229,6 @@ class PlayerActivity : BaseActivity() {
startActivity(Intent.createChooser(intent, getString(R.string.action_share)))
}
/**
* Called from the options sheet. It delegates saving the screenshot on
* external storage to the presenter.
*/
fun saveImage() {
viewModel.saveImage({ takeScreenshot()!! }, player.timePos)
}
/**
* Called from the presenter when a screenshot is saved or fails. It shows a message
* or logs the event depending on the [result].
@ -1288,14 +1244,6 @@ class PlayerActivity : BaseActivity() {
}
}
/**
* Called from the options sheet. It delegates setting the screenshot
* as the cover to the presenter.
*/
fun setAsCover() {
viewModel.setAsCover(takeScreenshot())
}
/**
* Called from the presenter when a screenshot is set as cover or fails.
* It shows a different message depending on the [result].
@ -1310,17 +1258,6 @@ class PlayerActivity : BaseActivity() {
)
}
@Suppress("UNUSED_PARAMETER")
fun switchDecoder(view: View) {
val newHwDec = when (HwDecState.get(player.hwdecActive)) {
HwDecState.HW -> if (HwDecState.isHwSupported) HwDecState.HW_PLUS else HwDecState.SW
HwDecState.HW_PLUS -> HwDecState.SW
HwDecState.SW -> HwDecState.HW
}
mpvUpdateHwDec(newHwDec)
refreshUi()
}
@Suppress("UNUSED_PARAMETER")
fun cycleSpeed(view: View) {
player.cycleSpeed()
@ -1352,11 +1289,10 @@ class PlayerActivity : BaseActivity() {
player.timePos?.let { playerControls.updatePlaybackPos(it) }
player.duration?.let { playerControls.updatePlaybackDuration(it) }
updatePlaybackStatus(player.paused ?: return@launchUI)
player.loadTracks()
playerControls.updateEpisodeText()
playerControls.updatePlaylistButtons()
playerControls.updateDecoderButton()
playerControls.updateSpeedButton()
withIOContext { player.loadTracks() }
}
}
@ -1444,11 +1380,11 @@ class PlayerActivity : BaseActivity() {
finish()
}
private fun setVideoList(quality: Int, videos: List<Video>?, fromStart: Boolean = false) {
private fun setVideoList(qualityIndex: Int, videos: List<Video>?, fromStart: Boolean = false) {
if (playerIsDestroyed) return
currentVideoList = videos
currentVideoList?.getOrNull(quality)?.let {
selectedQuality = quality
currentVideoList?.getOrNull(qualityIndex)?.let {
selectedQualityIndex = qualityIndex
setHttpOptions(it)
if (viewModel.state.value.isLoadingEpisode) {
viewModel.currentEpisode?.let { episode ->
@ -1464,8 +1400,8 @@ class PlayerActivity : BaseActivity() {
MPVLib.command(arrayOf("set", "start", "${player.timePos}"))
}
}
subTracks = arrayOf(Track("nothing", "Off")) + it.subtitleTracks.toTypedArray()
audioTracks = arrayOf(Track("nothing", "Off")) + it.audioTracks.toTypedArray()
subtitleTracks = arrayOf(Track("nothing", "None")) + it.subtitleTracks.toTypedArray()
audioTracks = arrayOf(Track("nothing", "None")) + it.audioTracks.toTypedArray()
MPVLib.command(arrayOf("loadfile", parseVideoUrl(it.videoUrl)))
}
refreshUi()
@ -1540,17 +1476,17 @@ class PlayerActivity : BaseActivity() {
}
// TODO: exception java.util.ConcurrentModificationException:
// UPDATE: MAY HAVE BEEN FIXED
// at java.lang.Object java.util.ArrayList$Itr.next() (ArrayList.java:860)
// at void eu.kanade.tachiyomi.ui.player.PlayerActivity.fileLoaded() (PlayerActivity.kt:1874)
// at void eu.kanade.tachiyomi.ui.player.PlayerActivity.event(int) (PlayerActivity.kt:1566)
// at void is.xyz.mpv.MPVLib.event(int) (MPVLib.java:86)
@SuppressLint("SourceLockedOrientationActivity")
internal fun fileLoaded() {
internal suspend fun fileLoaded() {
val localLangName = LocaleHelper.getSimpleLocaleDisplayName()
MPVLib.setPropertyDouble("speed", playerPreferences.playerSpeed().get().toDouble())
clearTracks()
player.loadTracks()
subTracks += player.tracks.getOrElse("sub") { emptyList() }
subtitleTracks += player.tracks.getOrElse("sub") { emptyList() }
.drop(1).map { track ->
Track(track.mpvId.toString(), track.name)
}.toTypedArray()
@ -1559,11 +1495,11 @@ class PlayerActivity : BaseActivity() {
Track(track.mpvId.toString(), track.name)
}.toTypedArray()
if (hadPreviousSubs) {
subTracks.getOrNull(selectedSub)?.let { sub ->
subtitleTracks.getOrNull(selectedSubtitleIndex)?.let { sub ->
MPVLib.command(arrayOf("sub-add", sub.url, "select", sub.url))
}
} else {
currentVideoList?.getOrNull(selectedQuality)
currentVideoList?.getOrNull(selectedQualityIndex)
?.subtitleTracks?.let { tracks ->
val langIndex = tracks.indexOfFirst {
it.lang.contains(localLangName)
@ -1571,23 +1507,23 @@ class PlayerActivity : BaseActivity() {
val requestedLanguage = if (langIndex == -1) 0 else langIndex
tracks.getOrNull(requestedLanguage)?.let { sub ->
hadPreviousSubs = true
selectedSub = requestedLanguage + 1
selectedSubtitleIndex = requestedLanguage + 1
MPVLib.command(arrayOf("sub-add", sub.url, "select", sub.url))
}
} ?: run {
val mpvSub = player.tracks.getOrElse("sub") { emptyList() }
.firstOrNull { player.sid == it.mpvId }
selectedSub = mpvSub?.let {
subTracks.indexOfFirst { it.url == mpvSub.mpvId.toString() }
selectedSubtitleIndex = mpvSub?.let {
subtitleTracks.indexOfFirst { it.url == mpvSub.mpvId.toString() }
}?.coerceAtLeast(0) ?: 0
}
}
if (hadPreviousAudio) {
audioTracks.getOrNull(selectedAudio)?.let { audio ->
audioTracks.getOrNull(selectedAudioIndex)?.let { audio ->
MPVLib.command(arrayOf("audio-add", audio.url, "select", audio.url))
}
} else {
currentVideoList?.getOrNull(selectedQuality)
currentVideoList?.getOrNull(selectedQualityIndex)
?.audioTracks?.let { tracks ->
val langIndex = tracks.indexOfFirst {
it.lang.contains(localLangName)
@ -1595,13 +1531,13 @@ class PlayerActivity : BaseActivity() {
val requestedLanguage = if (langIndex == -1) 0 else langIndex
tracks.getOrNull(requestedLanguage)?.let { audio ->
hadPreviousAudio = true
selectedAudio = requestedLanguage + 1
selectedAudioIndex = requestedLanguage + 1
MPVLib.command(arrayOf("audio-add", audio.url, "select", audio.url))
}
} ?: run {
val mpvAudio = player.tracks.getOrElse("audio") { emptyList() }
.firstOrNull { player.aid == it.mpvId }
selectedAudio = mpvAudio?.let {
selectedAudioIndex = mpvAudio?.let {
audioTracks.indexOfFirst { it.url == mpvAudio.mpvId.toString() }
}?.coerceAtLeast(0) ?: 0
}
@ -1646,7 +1582,7 @@ class PlayerActivity : BaseActivity() {
} else {
it.interval.startTime
}
val startChapter = Chapter(
val startChapter = VideoChapter(
index = -2, // Index -2 is used to indicate that this is an AniSkip chapter
title = it.skipType.getString(),
time = startTime,
@ -1655,7 +1591,7 @@ class PlayerActivity : BaseActivity() {
val isNotLastChapter = abs(it.interval.endTime - (duration?.toDouble() ?: -2.0)) > 1.0
val isNotAdjacent = nextStart == null || (abs(it.interval.endTime - nextStart) > 1.0)
if (isNotLastChapter && isNotAdjacent) {
val endChapter = Chapter(
val endChapter = VideoChapter(
index = -1,
title = null,
time = it.interval.endTime,
@ -1671,7 +1607,7 @@ class PlayerActivity : BaseActivity() {
}
}.sortedBy { it.time }.mapIndexed { i, it ->
if (i == 0 && it.time < 1.0) {
Chapter(
VideoChapter(
it.index,
it.title,
0.0,
@ -1690,7 +1626,7 @@ class PlayerActivity : BaseActivity() {
filteredAniskipChapters.none { it.time == 0.0 }
) {
listOf(
Chapter(
VideoChapter(
index = -1,
title = null,
time = 0.0,
@ -1742,11 +1678,6 @@ class PlayerActivity : BaseActivity() {
// mpv events
private fun mpvUpdateHwDec(hwDec: HwDecState) {
MPVLib.setOptionString("hwdec", hwDec.mpvValue)
HwDecState.mode = hwDec
}
internal fun eventPropertyUi(property: String, value: Long) {
when (property) {
"demuxer-cache-time" -> playerControls.updateBufferPosition(value.toInt())

View file

@ -5,7 +5,9 @@ import androidx.lifecycle.viewModelScope
import eu.kanade.tachiyomi.util.system.toast
import `is`.xyz.mpv.MPVLib
import logcat.LogPriority
import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.lang.launchUI
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.system.logcat
class PlayerObserver(val activity: PlayerActivity) :
@ -28,12 +30,13 @@ class PlayerObserver(val activity: PlayerActivity) :
override fun event(eventId: Int) {
when (eventId) {
MPVLib.mpvEventId.MPV_EVENT_FILE_LOADED -> activity.fileLoaded()
MPVLib.mpvEventId.MPV_EVENT_FILE_LOADED -> activity.viewModel.viewModelScope.launchIO { activity.fileLoaded() }
MPVLib.mpvEventId.MPV_EVENT_START_FILE -> activity.viewModel.viewModelScope.launchUI {
activity.player.paused = false
activity.refreshUi()
// Fixes a minor Ui bug but I have no idea why
if (activity.viewModel.isEpisodeOnline() != true) activity.showLoadingIndicator(false)
val isEpisodeOnline = withIOContext { activity.viewModel.isEpisodeOnline() != true }
if (isEpisodeOnline) activity.showLoadingIndicator(false)
}
}
}

View file

@ -513,13 +513,12 @@ class PlayerViewModel(
/**
* Sets the screenshot as cover and notifies the UI of the result.
*/
fun setAsCover(image: InputStream?) {
fun setAsCover(imageStream: () -> InputStream) {
val anime = currentAnime ?: return
val imageStream = image ?: return
viewModelScope.launchNonCancellable {
val result = try {
anime.editCover(Injekt.get(), imageStream)
anime.editCover(Injekt.get(), imageStream())
if (anime.isLocal() || anime.favorite) {
SetAsCover.Success
} else {
@ -674,10 +673,6 @@ class PlayerViewModel(
return null
}
fun closeDialog() {
mutableState.update { it.copy(dialog = null) }
}
fun showEpisodeList() {
mutableState.update { it.copy(dialog = Dialog.EpisodeList) }
}
@ -686,14 +681,34 @@ class PlayerViewModel(
mutableState.update { it.copy(dialog = Dialog.SpeedPicker) }
}
fun showDefaultDecoder() {
mutableState.update { it.copy(dialog = Dialog.DefaultDecoder) }
}
fun showSkipIntroLength() {
mutableState.update { it.copy(dialog = Dialog.SkipIntroLength) }
}
fun showSubtitleSettings() {
mutableState.update { it.copy(sheet = Sheet.SubtitleSettings) }
}
fun showScreenshotOptions() {
mutableState.update { it.copy(sheet = Sheet.ScreenshotOptions) }
}
fun showPlayerSettings() {
mutableState.update { it.copy(sheet = Sheet.PlayerSettings) }
}
fun showVideoChapters() {
mutableState.update { it.copy(sheet = Sheet.VideoChapters) }
}
fun showTracksCatalog() {
mutableState.update { it.copy(sheet = Sheet.TracksCatalog) }
}
fun closeDialogSheet() {
mutableState.update { it.copy(dialog = null, sheet = null) }
}
data class State(
val episodeList: List<Episode> = emptyList(),
val episode: Episode? = null,
@ -701,15 +716,23 @@ class PlayerViewModel(
val source: AnimeSource? = null,
val isLoadingEpisode: Boolean = false,
val dialog: Dialog? = null,
val sheet: Sheet? = null,
)
sealed class Dialog {
object EpisodeList : Dialog()
object SpeedPicker : Dialog()
object DefaultDecoder : Dialog()
object SkipIntroLength : Dialog()
}
sealed class Sheet {
object SubtitleSettings : Sheet()
object ScreenshotOptions : Sheet()
object PlayerSettings : Sheet()
object VideoChapters : Sheet()
object TracksCatalog : Sheet()
}
sealed class Event {
data class SetAnimeSkipIntro(val duration: Int) : Event()
data class SetCoverResult(val result: SetAsCover) : Event()

View file

@ -10,23 +10,18 @@ class PlayerPreferences(
fun preserveWatchingPosition() = preferenceStore.getBoolean("pref_preserve_watching_position", 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 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 autoplayEnabled() = preferenceStore.getBoolean("pref_auto_play_enabled", false)
fun invertedPlaybackTxt() = preferenceStore.getBoolean("pref_invert_playback_txt", false)
fun invertedDurationTxt() = preferenceStore.getBoolean("pref_invert_duration_txt", false)
fun mpvConf() = preferenceStore.getString("pref_mpv_conf", "")
@ -34,11 +29,9 @@ class PlayerPreferences(
fun mpvInput() = preferenceStore.getString("pref_mpv_input", "")
fun defaultPlayerOrientationType() = preferenceStore.getInt("pref_default_player_orientation_type_key", 10)
fun adjustOrientationVideoDimensions() = preferenceStore.getBoolean("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)
@ -56,28 +49,38 @@ class PlayerPreferences(
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)
fun alwaysUseExternalPlayer() = preferenceStore.getBoolean("pref_always_use_external_player", false)
fun externalPlayerPreference() = preferenceStore.getString("external_player_preference", "")
fun progressPreference() = preferenceStore.getFloat("pref_progress_preference", 0.85F)
fun defaultIntroLength() = preferenceStore.getInt("pref_default_intro_length", 85)
fun skipLengthPreference() = preferenceStore.getInt("pref_skip_length_preference", 10)
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 standardHwDec() = preferenceStore.getString("pref_hwdec", HwDecState.defaultHwDec.mpvValue)
fun hwDec() = preferenceStore.getString("pref_hwdec", HwDecState.defaultHwDec.mpvValue)
fun deband() = preferenceStore.getInt("pref_deband", 0)
fun rememberAudioDelay() = preferenceStore.getBoolean("pref_remember_audio_delay", false)
fun audioDelay() = preferenceStore.getInt("pref_audio_delay", 0)
fun rememberSubtitlesDelay() = preferenceStore.getBoolean("pref_remember_subtitles_delay", false)
fun subtitlesDelay() = preferenceStore.getInt("pref_subtitles_delay", 0)
fun overrideSubsASS() = preferenceStore.getBoolean("pref_override_subtitles_ass", false)
fun subtitleFontSize() = preferenceStore.getInt("pref_subtitles_font_size", 55)
fun boldSubtitles() = preferenceStore.getBoolean("pref_bold_subtitles", false)
fun italicSubtitles() = preferenceStore.getBoolean("pref_italic_subtitles", false)
fun textColorSubtitles() = preferenceStore.getInt("pref_text_color_subtitles", -1)
fun borderColorSubtitles() = preferenceStore.getInt("pref_border_color_subtitles", -16777216)
fun backgroundColorSubtitles() = preferenceStore.getInt("pref_background_color_subtitles", 0)
}

View file

@ -0,0 +1,180 @@
package eu.kanade.tachiyomi.ui.player.settings
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.model.ScreenModel
import eu.kanade.presentation.util.collectAsState
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.player.settings.dialogs.PlayerDialog
import eu.kanade.tachiyomi.util.preference.toggle
import `is`.xyz.mpv.MPVLib
import tachiyomi.core.preference.Preference
import tachiyomi.presentation.core.components.material.TextButton
import tachiyomi.presentation.core.components.material.padding
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.io.InputStream
val sheetDialogPadding = PaddingValues(vertical = MaterialTheme.padding.small, horizontal = MaterialTheme.padding.medium)
class PlayerSettingsScreenModel(
val preferences: PlayerPreferences = Injekt.get(),
private val hasSubTracks: Boolean = true,
) : ScreenModel {
fun togglePreference(preference: (PlayerPreferences) -> Preference<Boolean>) =
preference(preferences).toggle()
@Composable
fun ToggleableRow(
@StringRes textRes: Int,
paddingValues: PaddingValues = sheetDialogPadding,
isChecked: Boolean,
onClick: () -> Unit,
coloredText: Boolean = false,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(paddingValues),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = stringResource(id = textRes),
color = if (coloredText) MaterialTheme.colorScheme.primary else Color.Unspecified,
style = MaterialTheme.typography.titleSmall,
)
Switch(
checked = isChecked,
onCheckedChange = null,
)
}
}
@Composable
fun OverrideSubtitlesSwitch(
content: @Composable () -> Unit,
) {
val overrideSubsASS by preferences.overrideSubsASS().collectAsState()
val updateOverrideASS = {
val newOverrideValue = togglePreference(PlayerPreferences::overrideSubsASS)
val overrideType = if (newOverrideValue) "force" else "no"
MPVLib.setPropertyString("sub-ass-override", overrideType)
}
var showDialog by rememberSaveable { mutableStateOf(false) }
if (showDialog) {
ResetSubtitlesDialog(onDismissRequest = { showDialog = false })
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
NoSubtitlesWarning()
content()
ToggleableRow(
textRes = R.string.player_override_ass_subtitles,
isChecked = overrideSubsASS,
onClick = updateOverrideASS,
)
TextButton(onClick = { showDialog = true }) {
Text(stringResource(id = R.string.action_reset))
}
}
}
@Composable
private fun ResetSubtitlesDialog(
onDismissRequest: () -> Unit,
) {
val resetSubtitles = {
with(preferences) {
overrideSubsASS().delete()
subtitleFontSize().delete()
boldSubtitles().delete()
italicSubtitles().delete()
textColorSubtitles().delete()
borderColorSubtitles().delete()
backgroundColorSubtitles().delete()
}
}
PlayerDialog(
titleRes = R.string.player_reset_subtitles,
hideSystemBars = true,
modifier = Modifier
.fillMaxWidth(fraction = 0.6F)
.padding(MaterialTheme.padding.medium),
onConfirmRequest = resetSubtitles,
onDismissRequest = onDismissRequest,
)
}
@Composable
fun NoSubtitlesWarning() {
if (!hasSubTracks) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = MaterialTheme.padding.medium),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Outlined.Info,
contentDescription = null,
modifier = Modifier.size(14.dp),
)
Text(
text = stringResource(id = R.string.player_subtitle_empty_warning),
style = MaterialTheme.typography.labelMedium,
maxLines = 1,
)
}
}
}
fun takeScreenshot(cachePath: String, showSubtitles: Boolean): InputStream? {
val filename = cachePath + "/${System.currentTimeMillis()}_mpv_screenshot_tmp.png"
val subtitleFlag = if (showSubtitles) "subtitles" else "video"
MPVLib.command(arrayOf("screenshot-to-file", filename, subtitleFlag))
val tempFile = File(filename).takeIf { it.exists() } ?: return null
val newFile = File("$cachePath/mpv_screenshot.png")
newFile.delete()
tempFile.renameTo(newFile)
return newFile.takeIf { it.exists() }?.inputStream()
}
}

View file

@ -1,63 +0,0 @@
package eu.kanade.tachiyomi.ui.player.settings
import android.annotation.SuppressLint
import android.widget.TextView
import androidx.core.view.children
import androidx.core.widget.NestedScrollView
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.databinding.PlayerTracksItemBinding
import eu.kanade.tachiyomi.databinding.PlayerTracksSheetBinding
import eu.kanade.tachiyomi.ui.player.PlayerActivity
/**
* Sheet to show when track selection buttons in player are clicked.
*
* @param activity the instance of the PlayerActivity in use.
* @param changeTrackMethod the method to run on changing tracks
* @param tracks the given array of tracks
* @param preselectedTrack the index of the current selected track
* @param dismissSheet the method to run on selecting a track to dismiss the sheet
* @param trackSettings the method to run on clicking the settings button, null if no button
*/
@SuppressLint("ViewConstructor")
class PlayerTracksBuilder(
private val activity: PlayerActivity,
private val changeTrackMethod: (Int) -> Unit,
private val tracks: Array<Track>,
private val preselectedTrack: Int,
private val dismissSheet: () -> Unit,
private val trackSettings: (() -> Unit)?,
) : NestedScrollView(activity, null) {
private val binding = PlayerTracksSheetBinding.inflate(activity.layoutInflater, null, false)
init {
addView(binding.root)
initTracks()
}
private fun initTracks() {
tracks.forEachIndexed { i, track ->
val trackView = PlayerTracksItemBinding.inflate(activity.layoutInflater).root
trackView.text = track.lang
if (preselectedTrack == i) {
trackView.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_check_24dp, 0)
}
trackView.setOnClickListener {
clearSelection()
(it as? TextView)?.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_check_24dp, 0)
changeTrackMethod(i)
dismissSheet()
}
binding.linearLayout.addView(trackView)
}
}
private fun clearSelection() {
binding.linearLayout.children.forEach {
val view = it as? TextView ?: return
view.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_blank_24dp, 0)
}
}
}

View file

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.player.settings.dialogs
import android.os.Build
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@ -34,7 +35,8 @@ fun DefaultDecoderDialog(
}
PlayerDialog(
titleRes = R.string.player_hwdec_dialog_title,
titleRes = R.string.player_hwdec_mode,
modifier = Modifier.fillMaxWidth(fraction = 0.8F),
onDismissRequest = onDismissRequest,
) {
Column {

View file

@ -62,7 +62,7 @@ fun EpisodeListDialog(
PlayerDialog(
titleRes = R.string.episodes,
modifier = Modifier.fillMaxHeight(fraction = 0.8F),
modifier = Modifier.fillMaxHeight(fraction = 0.8F).fillMaxWidth(fraction = 0.8F),
onDismissRequest = onDismissRequest,
) {
VerticalFastScroller(

View file

@ -1,7 +1,9 @@
package eu.kanade.tachiyomi.ui.player.settings.dialogs
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog
@ -15,27 +17,43 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import androidx.core.view.WindowInsetsControllerCompat
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.components.material.TextButton
// TODO: (Merge_Change) stringResource "android.R.string.ok" to be replaced with
// "R.string.action_ok"
@Composable
fun PlayerDialog(
@StringRes titleRes: Int,
modifier: Modifier = Modifier,
hideSystemBars: Boolean = true,
onConfirmRequest: (() -> Unit)? = null,
onDismissRequest: () -> Unit,
content: @Composable () -> Unit,
content: @Composable (() -> Unit)? = null,
) {
val onConfirm = {
onConfirmRequest?.invoke()
onDismissRequest()
}
AlertDialog(
onDismissRequest = onDismissRequest,
modifier = modifier.fillMaxWidth(fraction = 0.8F),
modifier = modifier,
properties = DialogProperties(
dismissOnBackPress = true,
dismissOnClickOutside = true,
usePlatformDefaultWidth = false,
decorFitsSystemWindows = false,
),
) {
Surface(shape = MaterialTheme.shapes.large, modifier = Modifier.fillMaxWidth()) {
val systemUIController = rememberSystemUiController()
systemUIController.isSystemBarsVisible = false
systemUIController.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
Surface(shape = MaterialTheme.shapes.large, modifier = Modifier.fillMaxWidth(), tonalElevation = 1.dp) {
if (hideSystemBars) {
rememberSystemUiController().apply {
isSystemBarsVisible = false
systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
}
Column(modifier = Modifier.padding(16.dp)) {
Text(
@ -43,48 +61,21 @@ fun PlayerDialog(
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
)
content()
content?.invoke()
if (onConfirmRequest != null) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
TextButton(onClick = onDismissRequest) {
Text(stringResource(R.string.action_cancel))
}
TextButton(onClick = onConfirm) {
Text(stringResource(android.R.string.ok))
}
}
}
}
}
}
}
@Composable
fun PlayerDialog(
@StringRes titleRes: Int,
modifier: Modifier = Modifier,
hideSystemBars: Boolean,
confirmButton: @Composable () -> Unit,
dismissButton: @Composable () -> Unit,
onDismissRequest: () -> Unit,
content: @Composable () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismissRequest,
modifier = modifier.fillMaxWidth(fraction = 0.8F),
properties = DialogProperties(
usePlatformDefaultWidth = false,
decorFitsSystemWindows = false,
),
title = { Text(text = stringResource(titleRes)) },
text = {
Surface(shape = MaterialTheme.shapes.large, modifier = Modifier.fillMaxWidth()) {
if (hideSystemBars) {
rememberSystemUiController().apply {
isSystemBarsVisible = false
systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
}
content()
}
},
confirmButton = confirmButton,
dismissButton = dismissButton,
)
}
/**
* style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
*/

View file

@ -2,8 +2,6 @@ package eu.kanade.tachiyomi.ui.player.settings.dialogs
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@ -24,23 +22,13 @@ fun SkipIntroLengthDialog(
PlayerDialog(
titleRes = R.string.action_change_intro_length,
modifier = Modifier.fillMaxWidth(fraction = if (fromPlayer) 0.5F else 0.8F),
hideSystemBars = fromPlayer,
confirmButton = {
TextButton(
onClick = {
onConfirmRequest = if (fromPlayer) null else { {} },
onDismissRequest = {
updateSkipIntroLength(newLength.toLong())
onDismissRequest()
},
) {
Text(text = stringResource(android.R.string.ok))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel))
}
},
onDismissRequest = onDismissRequest,
) {
Box(
modifier = Modifier.fillMaxWidth(),

View file

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.player.settings.dialogs
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
@ -33,6 +34,7 @@ fun SpeedPickerDialog(
PlayerDialog(
titleRes = R.string.title_speed_dialog,
modifier = Modifier.fillMaxWidth(fraction = 0.8F),
onDismissRequest = onDismissRequest,
) {
Column {

View file

@ -1,68 +0,0 @@
package eu.kanade.tachiyomi.ui.player.settings.sheets
import android.view.LayoutInflater
import android.view.View
import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.PlayerChaptersItemBinding
import eu.kanade.tachiyomi.databinding.PlayerChaptersSheetBinding
import eu.kanade.tachiyomi.ui.player.PlayerActivity
import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.widget.sheet.PlayerBottomSheetDialog
import `is`.xyz.mpv.MPVView.Chapter
import `is`.xyz.mpv.Utils
import kotlin.math.roundToInt
/** Sheet to show when Chapter selection buttons in player are clicked. */
class PlayerChaptersSheet(
private val activity: PlayerActivity,
@StringRes
private val textRes: Int,
private val seekToChapterMethod: (Chapter) -> Unit,
private val chapters: List<Chapter>,
) : PlayerBottomSheetDialog(activity) {
private lateinit var binding: PlayerChaptersSheetBinding
private var wasPaused: Boolean? = null
override fun createView(inflater: LayoutInflater): View {
wasPaused = activity.player.paused
activity.player.paused = true
binding = PlayerChaptersSheetBinding.inflate(activity.layoutInflater, null, false)
binding.chapterSelectionHeader.setText(textRes)
chapters.forEachIndexed { i, chapter ->
val chapterView = PlayerChaptersItemBinding.inflate(activity.layoutInflater).root
chapterView.text = if (chapter.title.isNullOrBlank()) {
Utils.prettyTime(chapter.time.roundToInt())
} else {
"${chapter.title} (${Utils.prettyTime(chapter.time.roundToInt())})"
}
// Highlighted the current chapter
if (i == chapters.lastIndex) {
if (activity.player.timePos!!.toInt() >= chapter.time.toInt()) {
chapterView.setBackgroundColor(context.getResourceColor(R.attr.colorPrimary))
chapterView.setTextColor(context.getResourceColor(R.attr.colorOnPrimary))
}
} else if (activity.player.timePos!!.toInt() >= chapter.time.toInt() &&
activity.player.timePos!!.toInt() < chapters[i + 1].time.toInt()
) {
chapterView.setBackgroundColor(context.getResourceColor(R.attr.colorPrimary))
chapterView.setTextColor(context.getResourceColor(R.attr.colorOnPrimary))
}
chapterView.setOnClickListener {
seekToChapterMethod(chapters[i])
this.dismiss()
}
binding.linearLayout.addView(chapterView)
}
return binding.root
}
override fun dismiss() {
activity.playerControls.showAndFadeControls()
wasPaused?.let { activity.player.paused = it }
super.dismiss()
}
}

View file

@ -1,60 +0,0 @@
package eu.kanade.tachiyomi.ui.player.settings.sheets
import android.view.LayoutInflater
import android.view.View
import android.widget.CompoundButton
import androidx.core.view.isVisible
import eu.kanade.tachiyomi.databinding.PlayerOptionsSheetBinding
import eu.kanade.tachiyomi.ui.player.PlayerActivity
import eu.kanade.tachiyomi.widget.sheet.PlayerBottomSheetDialog
/**
* Sheet to show when overflow button in player is clicked.
*
* @param activity the instance of the PlayerActivity in use.
*/
class PlayerOptionsSheet(
private val activity: PlayerActivity,
) : PlayerBottomSheetDialog(activity) {
private lateinit var binding: PlayerOptionsSheetBinding
private var wasPaused: Boolean? = null
override fun createView(inflater: LayoutInflater): View {
wasPaused = activity.player.paused
activity.player.paused = true
binding = PlayerOptionsSheetBinding.inflate(activity.layoutInflater, null, false)
val gestureVolumeBrightness = activity.playerPreferences.gestureVolumeBrightness()
val gestureHorizontalSeek = activity.playerPreferences.gestureHorizontalSeek()
binding.toggleVolumeBrightnessGestures.isChecked = gestureVolumeBrightness.get()
binding.toggleVolumeBrightnessGestures.setOnCheckedChangeListener { _, newValue -> gestureVolumeBrightness.set(newValue) }
binding.toggleHorizontalSeekGesture.isChecked = gestureHorizontalSeek.get()
binding.toggleHorizontalSeekGesture.setOnCheckedChangeListener { _, newValue -> gestureHorizontalSeek.set(newValue) }
binding.toggleStats.isChecked = activity.stats
binding.toggleStats.setOnCheckedChangeListener(toggleStats)
binding.statsPage.isVisible = activity.stats
binding.statsPage.setSelection(activity.statsPage)
binding.statsPage.onItemSelectedListener = setStatsPage
return binding.root
}
private val toggleStats = { _: CompoundButton, newValue: Boolean ->
binding.statsPage.isVisible = newValue
activity.toggleStats()
}
private val setStatsPage = { page: Int ->
activity.statsPage = page
}
override fun dismiss() {
activity.playerControls.showAndFadeControls()
wasPaused?.let { activity.player.paused = it }
super.dismiss()
activity.refreshUi()
}
}

View file

@ -1,63 +0,0 @@
package eu.kanade.tachiyomi.ui.player.settings.sheets
import android.view.LayoutInflater
import android.view.View
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.PlayerScreenshotSheetBinding
import eu.kanade.tachiyomi.ui.player.PlayerActivity
import eu.kanade.tachiyomi.widget.sheet.PlayerBottomSheetDialog
/**
* Sheet to show when the player is long clicked.
*/
class PlayerScreenshotSheet(
private val activity: PlayerActivity,
) : PlayerBottomSheetDialog(activity) {
private lateinit var binding: PlayerScreenshotSheetBinding
override fun createView(inflater: LayoutInflater): View {
binding = PlayerScreenshotSheetBinding.inflate(activity.layoutInflater, null, false)
binding.setAsCover.setOnClickListener { setAsCover() }
binding.share.setOnClickListener { share() }
binding.save.setOnClickListener { save() }
val screenshotSubtitles = activity.playerPreferences.screenshotSubtitles()
binding.toggleSubs.isChecked = screenshotSubtitles.get()
binding.toggleSubs.setOnCheckedChangeListener { _, newValue -> screenshotSubtitles.set(newValue) }
return binding.root
}
/**
* Sets the screenshot as the cover of the anime.
*/
private fun setAsCover() {
MaterialAlertDialogBuilder(activity)
.setMessage(R.string.confirm_set_image_as_cover)
.setPositiveButton(android.R.string.ok) { _, _ ->
activity.setAsCover()
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
/**
* Shares the screenshot with external apps.
*/
private fun share() {
activity.shareImage()
dismiss()
}
/**
* Saves the screenshot on external storage.
*/
private fun save() {
activity.saveImage()
dismiss()
}
}

View file

@ -1,46 +1,132 @@
package eu.kanade.tachiyomi.ui.player.settings.sheets
import android.os.Bundle
import android.view.View
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.FilterChip
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.sp
import eu.kanade.presentation.components.AdaptiveSheet
import eu.kanade.presentation.util.collectAsState
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.player.PlayerActivity
import eu.kanade.tachiyomi.widget.sheet.TabbedPlayerBottomSheetDialog
import eu.kanade.tachiyomi.ui.player.settings.PlayerSettingsScreenModel
import eu.kanade.tachiyomi.ui.player.viewer.HwDecState
import eu.kanade.tachiyomi.ui.player.viewer.PlayerStatsPage
import `is`.xyz.mpv.MPVLib
import tachiyomi.presentation.core.components.material.padding
class PlayerSettingsSheet(
private val activity: PlayerActivity,
) : TabbedPlayerBottomSheetDialog(activity) {
@Composable
fun PlayerSettingsSheet(
screenModel: PlayerSettingsScreenModel,
onDismissRequest: () -> Unit,
) {
val verticalGesture by remember { mutableStateOf(screenModel.preferences.gestureVolumeBrightness()) }
val horizontalGesture by remember { mutableStateOf(screenModel.preferences.gestureHorizontalSeek()) }
var statisticsPage by remember { mutableStateOf(screenModel.preferences.playerStatisticsPage().get()) }
var decoder by remember { mutableStateOf(screenModel.preferences.hwDec().get()) }
private var wasPaused: Boolean? = null
private val videoQualitySettings = activity.qualityTracksTab(this::dismiss) as View
private val subtitleSettings = activity.subtitleTracksTab(this::dismiss) as View
private val audioSettings = activity.audioTracksTab(this::dismiss) as View
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
wasPaused = activity.player.paused
activity.player.paused = true
behavior.isFitToContents = false
behavior.halfExpandedRatio = 0.15f
// TODO: Shift to MPV-Lib
val togglePlayerStatsPage: (Int) -> Unit = { page ->
if ((statisticsPage == 0) xor (page == 0)) {
MPVLib.command(arrayOf("script-binding", "stats/display-stats-toggle"))
}
if (page != 0) {
MPVLib.command(arrayOf("script-binding", "stats/display-page-$page"))
}
statisticsPage = page
screenModel.preferences.playerStatisticsPage().set(page)
}
override fun getTabs(): List<Pair<View, Int>> {
val tabs = mutableListOf(
Pair(subtitleSettings, R.string.subtitle_dialog_header),
Pair(audioSettings, R.string.audio_dialog_header),
val togglePlayerDecoder: (HwDecState) -> Unit = { hwDecState ->
val hwDec = hwDecState.mpvValue
MPVLib.setOptionString("hwdec", hwDec)
decoder = hwDec
screenModel.preferences.hwDec().set(hwDec)
}
AdaptiveSheet(
hideSystemBars = true,
onDismissRequest = onDismissRequest,
) {
Column(
modifier = Modifier.padding(MaterialTheme.padding.medium),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
) {
Text(
text = stringResource(id = R.string.settings_dialog_header),
style = MaterialTheme.typography.titleMedium,
fontSize = 20.sp,
)
screenModel.ToggleableRow(
textRes = R.string.enable_volume_brightness_gestures,
isChecked = verticalGesture.collectAsState().value,
onClick = { screenModel.togglePreference { verticalGesture } },
)
screenModel.ToggleableRow(
textRes = R.string.enable_horizontal_seek_gesture,
isChecked = horizontalGesture.collectAsState().value,
onClick = { screenModel.togglePreference { horizontalGesture } },
)
// TODO: (Merge_Change) below two Columns to be switched to using 'SettingsChipRow'
// from 'SettingsItems.kt'
Column(
modifier = Modifier.fillMaxWidth().padding(horizontal = MaterialTheme.padding.medium),
) {
Text(
text = stringResource(id = R.string.player_hwdec_mode),
style = MaterialTheme.typography.titleSmall,
)
Row(
modifier = Modifier.padding(vertical = MaterialTheme.padding.tiny),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
HwDecState.values().forEach {
if (!HwDecState.isHwSupported && it.title == "HW+") return@forEach
FilterChip(
selected = decoder == it.mpvValue,
onClick = { togglePlayerDecoder(it) },
label = { Text(it.title) },
)
if (activity.viewModel.isEpisodeOnline() == true) {
tabs.add(0, Pair(videoQualitySettings, R.string.quality_dialog_header))
}
return tabs.toList()
}
}
override fun dismiss() {
activity.playerControls.showAndFadeControls()
wasPaused?.let { activity.player.paused = it }
super.dismiss()
activity.refreshUi()
Column(
modifier = Modifier.fillMaxWidth().padding(horizontal = MaterialTheme.padding.medium),
) {
Text(
text = stringResource(id = R.string.toggle_player_statistics_page),
style = MaterialTheme.typography.titleSmall,
)
Row(
modifier = Modifier.padding(vertical = MaterialTheme.padding.tiny),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
PlayerStatsPage.values().forEach {
FilterChip(
selected = statisticsPage == it.page,
onClick = { togglePlayerStatsPage(it.page) },
label = { Text(stringResource(it.textRes)) },
)
}
}
}
}
}
}

View file

@ -0,0 +1,131 @@
package eu.kanade.tachiyomi.ui.player.settings.sheets
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Photo
import androidx.compose.material.icons.outlined.Save
import androidx.compose.material.icons.outlined.Share
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.AdaptiveSheet
import eu.kanade.presentation.util.collectAsState
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.player.settings.PlayerSettingsScreenModel
import eu.kanade.tachiyomi.ui.player.settings.dialogs.PlayerDialog
import tachiyomi.presentation.core.components.material.padding
import java.io.InputStream
@Composable
fun ScreenshotOptionsSheet(
screenModel: PlayerSettingsScreenModel,
cachePath: String,
onSetAsCover: (() -> InputStream) -> Unit,
onShare: (() -> InputStream) -> Unit,
onSave: (() -> InputStream) -> Unit,
onDismissRequest: () -> Unit,
) {
var showSetCoverDialog by remember { mutableStateOf(false) }
val showSubtitles by remember { mutableStateOf(screenModel.preferences.screenshotSubtitles()) }
AdaptiveSheet(
hideSystemBars = true,
onDismissRequest = onDismissRequest,
) {
Column {
Row(
modifier = Modifier.padding(vertical = MaterialTheme.padding.medium),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
ActionButton(
modifier = Modifier.weight(1f),
title = stringResource(R.string.set_as_cover),
icon = Icons.Outlined.Photo,
onClick = { showSetCoverDialog = true },
)
ActionButton(
modifier = Modifier.weight(1f),
title = stringResource(R.string.action_share),
icon = Icons.Outlined.Share,
onClick = {
onShare { screenModel.takeScreenshot(cachePath, showSubtitles.get())!! }
onDismissRequest()
},
)
ActionButton(
modifier = Modifier.weight(1f),
title = stringResource(R.string.action_save),
icon = Icons.Outlined.Save,
onClick = {
onSave { screenModel.takeScreenshot(cachePath, showSubtitles.get())!! }
onDismissRequest()
},
)
}
screenModel.ToggleableRow(
textRes = R.string.screenshot_show_subs,
paddingValues = PaddingValues(MaterialTheme.padding.medium),
isChecked = showSubtitles.collectAsState().value,
onClick = { screenModel.togglePreference { showSubtitles } },
coloredText = true,
)
}
}
if (showSetCoverDialog) {
PlayerDialog(
titleRes = R.string.confirm_set_image_as_cover,
modifier = Modifier.fillMaxWidth(fraction = 0.6F).padding(MaterialTheme.padding.medium),
onConfirmRequest = { onSetAsCover { screenModel.takeScreenshot(cachePath, showSubtitles.get())!! } },
onDismissRequest = { showSetCoverDialog = false },
)
}
}
// TODO: (Merge_Change) function is to be removed once added in merge
// "package tachiyomi.presentation.core.components"
@Composable
private fun ActionButton(
modifier: Modifier = Modifier,
title: String,
icon: ImageVector,
onClick: () -> Unit,
) {
TextButton(
modifier = modifier,
onClick = onClick,
) {
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
imageVector = icon,
contentDescription = null,
)
Text(
text = title,
textAlign = TextAlign.Center,
)
}
}
}

View file

@ -0,0 +1,124 @@
package eu.kanade.tachiyomi.ui.player.settings.sheets
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import eu.kanade.presentation.components.TabbedDialog
import eu.kanade.presentation.components.TabbedDialogPaddings
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.ui.player.settings.sheetDialogPadding
@Composable
fun TracksCatalogSheet(
isEpisodeOnline: Boolean?,
qualityTracks: Array<Track>,
subtitleTracks: Array<Track>,
audioTracks: Array<Track>,
selectedQualityIndex: Int,
selectedSubtitleIndex: Int,
selectedAudioIndex: Int,
onQualitySelected: (Int) -> Unit,
onSubtitleSelected: (Int) -> Unit,
onAudioSelected: (Int) -> Unit,
onSettingsClicked: () -> Unit,
onDismissRequest: () -> Unit,
) {
val tabTitles = mutableListOf(
stringResource(id = R.string.subtitle_dialog_header),
stringResource(id = R.string.audio_dialog_header),
)
if (isEpisodeOnline == true) {
tabTitles.add(0, stringResource(id = R.string.quality_dialog_header))
}
TabbedDialog(
onDismissRequest = onDismissRequest,
tabTitles = tabTitles,
onOverflowMenuClicked = onSettingsClicked,
overflowIcon = Icons.Outlined.Settings,
hideSystemBars = true,
) { contentPadding, page ->
Column(
modifier = Modifier
.padding(contentPadding)
.padding(vertical = TabbedDialogPaddings.Vertical)
.verticalScroll(rememberScrollState()),
) {
@Composable fun QualityTracksPage() = TracksPageBuilder(
tracks = qualityTracks,
selectedTrackIndex = selectedQualityIndex,
onTrackSelected = onQualitySelected,
)
@Composable fun SubtitleTracksPage() = TracksPageBuilder(
tracks = subtitleTracks,
selectedTrackIndex = selectedSubtitleIndex,
onTrackSelected = onSubtitleSelected,
)
@Composable fun AudioTracksPage() = TracksPageBuilder(
tracks = audioTracks,
selectedTrackIndex = selectedAudioIndex,
onTrackSelected = onAudioSelected,
)
when {
isEpisodeOnline == true && page == 0 -> QualityTracksPage()
page == 0 || page == 1 -> SubtitleTracksPage()
page == 2 -> AudioTracksPage()
}
}
}
}
@Composable
private fun TracksPageBuilder(
tracks: Array<Track>,
selectedTrackIndex: Int,
onTrackSelected: (Int) -> Unit,
) {
var selectedIndex by remember { mutableStateOf(selectedTrackIndex) }
val onSelected: (Int) -> Unit = { index ->
onTrackSelected(index)
selectedIndex = index
}
tracks.forEachIndexed { index, track ->
val selected = selectedIndex == index
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = { onSelected(index) })
.padding(sheetDialogPadding),
) {
Text(
text = track.lang,
fontWeight = if (selected) FontWeight.Bold else FontWeight.Normal,
fontStyle = if (selected) FontStyle.Italic else FontStyle.Normal,
style = MaterialTheme.typography.bodyMedium,
color = if (selected) MaterialTheme.colorScheme.primary else Color.Unspecified,
)
}
}
}

View file

@ -0,0 +1,87 @@
package eu.kanade.tachiyomi.ui.player.settings.sheets
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import eu.kanade.presentation.components.AdaptiveSheet
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.player.settings.sheetDialogPadding
import `is`.xyz.mpv.Utils
import tachiyomi.presentation.core.components.material.padding
import kotlin.math.roundToInt
import `is`.xyz.mpv.MPVView.Chapter as VideoChapter
@Composable
fun VideoChaptersSheet(
timePosition: Int,
videoChapters: List<VideoChapter>,
onVideoChapterSelected: (VideoChapter, String) -> Unit,
onDismissRequest: () -> Unit,
) {
var currentTimePosition by remember { mutableStateOf(timePosition) }
AdaptiveSheet(
hideSystemBars = true,
onDismissRequest = onDismissRequest,
) {
Column(
modifier = Modifier.padding(MaterialTheme.padding.medium),
) {
Text(
text = stringResource(id = R.string.chapter_dialog_header),
style = MaterialTheme.typography.titleMedium,
fontSize = 20.sp,
)
videoChapters.forEachIndexed { index, videoChapter ->
val videoChapterTime = videoChapter.time.roundToInt()
val videoChapterName = if (videoChapter.title.isNullOrBlank()) {
Utils.prettyTime(videoChapterTime)
} else {
"${videoChapter.title} (${Utils.prettyTime(videoChapterTime)})"
}
val nextChapterTime = videoChapters.getOrNull(index + 1)?.time?.toInt()
val selected = (index == videoChapters.lastIndex && currentTimePosition >= videoChapterTime) ||
(currentTimePosition >= videoChapterTime && (nextChapterTime == null || currentTimePosition < nextChapterTime))
val onClick = {
currentTimePosition = videoChapter.time.roundToInt()
onVideoChapterSelected(videoChapter, videoChapterName)
onDismissRequest()
}
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(sheetDialogPadding),
) {
Text(
text = videoChapterName,
fontWeight = if (selected) FontWeight.Bold else FontWeight.Normal,
fontStyle = if (selected) FontStyle.Italic else FontStyle.Normal,
style = MaterialTheme.typography.bodyMedium,
color = if (selected) MaterialTheme.colorScheme.primary else Color.Unspecified,
)
}
}
}
}
}

View file

@ -0,0 +1,300 @@
package eu.kanade.tachiyomi.ui.player.settings.sheets.subtitle
import androidx.annotation.StringRes
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowDropDown
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.util.collectAsState
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences
import eu.kanade.tachiyomi.ui.player.settings.PlayerSettingsScreenModel
import `is`.xyz.mpv.MPVLib
import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.getAndSet
import tachiyomi.presentation.core.components.material.padding
import kotlin.math.floor
import kotlin.math.max
@Composable
fun SubtitleColorPage(screenModel: PlayerSettingsScreenModel) {
screenModel.OverrideSubtitlesSwitch {
SubtitleColors(screenModel = screenModel)
}
}
@Composable
private fun SubtitleColors(
screenModel: PlayerSettingsScreenModel,
) {
var subsColor by remember { mutableStateOf(SubsColor.NONE) }
fun updateType(newColor: SubsColor) {
subsColor = if (newColor != subsColor) newColor else SubsColor.NONE
}
val textColorPref = screenModel.preferences.textColorSubtitles()
val borderColorPref = screenModel.preferences.borderColorSubtitles()
val backgroundColorPref = screenModel.preferences.backgroundColorSubtitles()
Row(horizontalArrangement = Arrangement.SpaceEvenly, modifier = Modifier.fillMaxWidth()) {
SubtitleColorSelector(
label = R.string.player_subtitle_text_color,
onClick = { updateType(SubsColor.TEXT) },
selected = subsColor == SubsColor.TEXT,
preference = textColorPref,
)
SubtitleColorSelector(
label = R.string.player_subtitle_border_color,
onClick = { updateType(SubsColor.BORDER) },
selected = subsColor == SubsColor.BORDER,
preference = borderColorPref,
)
SubtitleColorSelector(
label = R.string.player_subtitle_background_color,
onClick = { updateType(SubsColor.BACKGROUND) },
selected = subsColor == SubsColor.BACKGROUND,
preference = backgroundColorPref,
)
}
Column(horizontalAlignment = Alignment.CenterHorizontally) {
SubtitlePreview(
isBold = screenModel.preferences.boldSubtitles().collectAsState().value,
isItalic = screenModel.preferences.italicSubtitles().collectAsState().value,
textColor = Color(textColorPref.collectAsState().value),
borderColor = Color(borderColorPref.collectAsState().value),
backgroundColor = Color(backgroundColorPref.collectAsState().value),
)
}
Column(verticalArrangement = Arrangement.SpaceEvenly) {
if (subsColor != SubsColor.NONE) {
SubtitleColorSlider(
argb = ARGBValue.RED,
subsColor = subsColor,
preference = subsColor.preference(screenModel.preferences),
)
SubtitleColorSlider(
argb = ARGBValue.GREEN,
subsColor = subsColor,
preference = subsColor.preference(screenModel.preferences),
)
SubtitleColorSlider(
argb = ARGBValue.BLUE,
subsColor = subsColor,
preference = subsColor.preference(screenModel.preferences),
)
SubtitleColorSlider(
argb = ARGBValue.ALPHA,
subsColor = subsColor,
preference = subsColor.preference(screenModel.preferences),
)
}
}
}
@Composable
private fun SubtitleColorSelector(
@StringRes label: Int,
selected: Boolean,
onClick: () -> Unit,
preference: Preference<Int>,
) {
val colorCode by preference.collectAsState()
val borderColor = MaterialTheme.colorScheme.onSurface
Column(
modifier = Modifier
.clickable(onClick = onClick)
.padding(MaterialTheme.padding.tiny),
verticalArrangement = Arrangement.SpaceEvenly,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(text = stringResource(label))
Spacer(modifier = Modifier.width(MaterialTheme.padding.tiny))
Canvas(
modifier = Modifier
.wrapContentSize(Alignment.Center)
.requiredSize(20.dp),
) {
drawColorBox(
boxColor = Color(colorCode),
borderColor = borderColor,
radius = floor(2.dp.toPx()),
strokeWidth = floor(2.dp.toPx()),
)
}
Spacer(modifier = Modifier.width(MaterialTheme.padding.tiny))
Text(text = colorCode.toHexString())
if (selected) {
Icon(imageVector = Icons.Outlined.ArrowDropDown, contentDescription = null)
}
}
}
@Composable
private fun SubtitleColorSlider(
argb: ARGBValue,
subsColor: SubsColor,
preference: Preference<Int>,
) {
val colorCode by preference.collectAsState()
fun getColorValue(currentColor: Int, color: Float, mask: Long, bitShift: Int): Int {
return (color.toInt() shl bitShift) or (currentColor and mask.inv().toInt())
}
Row(verticalAlignment = Alignment.CenterVertically) {
Spacer(modifier = Modifier.width(MaterialTheme.padding.small))
Text(
text = stringResource(argb.label),
style = MaterialTheme.typography.bodyMedium,
)
Spacer(modifier = Modifier.width(MaterialTheme.padding.small))
val borderColor = MaterialTheme.colorScheme.onSurface
Canvas(
modifier = Modifier
.wrapContentSize(Alignment.Center)
.requiredSize(20.dp),
) {
drawColorBox(
boxColor = argb.asColor(colorCode),
borderColor = borderColor,
radius = floor(2.dp.toPx()),
strokeWidth = floor(2.dp.toPx()),
)
}
Spacer(modifier = Modifier.width(MaterialTheme.padding.small))
Slider(
modifier = Modifier.weight(1f),
value = argb.toValue(colorCode).toFloat(),
onValueChange = { newColorValue ->
preference.getAndSet { getColorValue(it, newColorValue, argb.mask, argb.bitShift) }
MPVLib.setPropertyString(subsColor.mpvProperty, colorCode.toHexString())
},
valueRange = 0f..255f,
steps = 255,
)
Spacer(modifier = Modifier.width(MaterialTheme.padding.small))
Text(text = String.format("%03d", argb.toValue(colorCode)))
Spacer(modifier = Modifier.width(MaterialTheme.padding.small))
}
}
private enum class SubsColor(val mpvProperty: String, val preference: (PlayerPreferences) -> Preference<Int>) {
NONE("", PlayerPreferences::textColorSubtitles),
TEXT("sub-color", PlayerPreferences::textColorSubtitles),
BORDER("sub-border-color", PlayerPreferences::borderColorSubtitles),
BACKGROUND("sub-back-color", PlayerPreferences::backgroundColorSubtitles),
;
}
private enum class ARGBValue(@StringRes val label: Int, val mask: Long, val bitShift: Int, val toValue: (Int) -> Int, val asColor: (Int) -> Color) {
ALPHA(R.string.color_filter_a_value, 0xFF000000L, 24, ::toAlpha, ::asAlpha),
RED(R.string.color_filter_r_value, 0x00FF0000L, 16, ::toRed, ::asRed),
GREEN(R.string.color_filter_g_value, 0x0000FF00L, 8, ::toGreen, ::asGreen),
BLUE(R.string.color_filter_b_value, 0x000000FFL, 0, ::toBlue, ::asBlue),
;
}
private fun toAlpha(color: Int) = (color ushr 24) and 0xFF
private fun asAlpha(color: Int) = Color((color.toLong() and 0xFF000000L) or 0x00FFFFFFL)
private fun toRed(color: Int) = (color ushr 16) and 0xFF
private fun asRed(color: Int) = Color((color.toLong() and 0x00FF0000L) or 0xFF000000L)
private fun toGreen(color: Int) = (color ushr 8) and 0xFF
private fun asGreen(color: Int) = Color((color.toLong() and 0x0000FF00L) or 0xFF000000L)
private fun toBlue(color: Int) = (color ushr 0) and 0xFF
private fun asBlue(color: Int) = Color((color.toLong() and 0x000000FFL) or 0xFF000000L)
fun Int.toHexString(): String {
val colorCodeAlpha = String.format("%02X", toAlpha(this))
val colorCodeRed = String.format("%02X", toRed(this))
val colorCodeGreen = String.format("%02X", toGreen(this))
val colorCodeBlue = String.format("%02X", toBlue(this))
return "#$colorCodeAlpha$colorCodeRed$colorCodeGreen$colorCodeBlue"
}
private fun DrawScope.drawColorBox(
boxColor: Color,
borderColor: Color,
radius: Float,
strokeWidth: Float,
) {
val halfStrokeWidth = strokeWidth / 2.0f
val stroke = Stroke(strokeWidth)
val checkboxSize = size.width
if (boxColor == borderColor) {
drawRoundRect(
boxColor,
size = Size(checkboxSize, checkboxSize),
cornerRadius = CornerRadius(radius),
style = Fill,
)
} else {
drawRoundRect(
boxColor,
topLeft = Offset(strokeWidth, strokeWidth),
size = Size(checkboxSize - strokeWidth * 2, checkboxSize - strokeWidth * 2),
cornerRadius = CornerRadius(max(0f, radius - strokeWidth)),
style = Fill,
)
drawRoundRect(
borderColor,
topLeft = Offset(halfStrokeWidth, halfStrokeWidth),
size = Size(checkboxSize - strokeWidth, checkboxSize - strokeWidth),
cornerRadius = CornerRadius(radius - halfStrokeWidth),
style = stroke,
)
}
}

View file

@ -0,0 +1,87 @@
package eu.kanade.tachiyomi.ui.player.settings.sheets.subtitle
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.components.OutlinedNumericChooser
import eu.kanade.presentation.util.collectAsState
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.player.settings.PlayerSettingsScreenModel
import `is`.xyz.mpv.MPVLib
import tachiyomi.presentation.core.components.material.padding
@Composable
fun SubtitleDelayPage(
screenModel: PlayerSettingsScreenModel,
) {
Column(verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny)) {
val audioDelay by remember { mutableStateOf(screenModel.preferences.rememberAudioDelay()) }
val subDelay by remember { mutableStateOf(screenModel.preferences.rememberSubtitlesDelay()) }
screenModel.ToggleableRow(
textRes = R.string.player_audio_remember_delay,
isChecked = audioDelay.collectAsState().value,
onClick = { screenModel.togglePreference { audioDelay } },
)
Row(
modifier = Modifier.fillMaxWidth().padding(bottom = MaterialTheme.padding.medium),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly,
) {
OutlinedNumericChooser(
label = stringResource(id = R.string.player_audio_delay),
placeholder = "0",
suffix = "ms",
value = (MPVLib.getPropertyDouble(Tracks.AUDIO.mpvProperty) * 1000).toInt(),
step = 100,
onValueChanged = {
MPVLib.setPropertyDouble(Tracks.AUDIO.mpvProperty, (it / 1000).toDouble())
screenModel.preferences.audioDelay().set(it)
},
)
}
screenModel.NoSubtitlesWarning()
screenModel.ToggleableRow(
textRes = R.string.player_subtitle_remember_delay,
isChecked = subDelay.collectAsState().value,
onClick = { screenModel.togglePreference { subDelay } },
)
Row(
modifier = Modifier.fillMaxWidth().padding(bottom = MaterialTheme.padding.medium),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly,
) {
OutlinedNumericChooser(
label = stringResource(id = R.string.player_subtitle_delay),
placeholder = "0",
suffix = "ms",
value = (MPVLib.getPropertyDouble(Tracks.SUBTITLES.mpvProperty) * 1000).toInt(),
step = 100,
onValueChanged = {
MPVLib.setPropertyDouble(Tracks.SUBTITLES.mpvProperty, (it / 1000).toDouble())
screenModel.preferences.subtitlesDelay().set(it)
},
)
}
}
}
private enum class Tracks(val mpvProperty: String) {
SUBTITLES("sub-delay"),
AUDIO("audio-delay"),
;
}

View file

@ -0,0 +1,122 @@
package eu.kanade.tachiyomi.ui.player.settings.sheets.subtitle
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.material.SnackbarDefaults.backgroundColor
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.FormatBold
import androidx.compose.material.icons.outlined.FormatItalic
import androidx.compose.material.icons.outlined.FormatSize
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.OutlinedNumericChooser
import eu.kanade.presentation.util.collectAsState
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences
import eu.kanade.tachiyomi.ui.player.settings.PlayerSettingsScreenModel
import `is`.xyz.mpv.MPVLib
import tachiyomi.presentation.core.components.material.ReadItemAlpha
import tachiyomi.presentation.core.components.material.padding
@Composable
fun SubtitleFontPage(screenModel: PlayerSettingsScreenModel) {
screenModel.OverrideSubtitlesSwitch {
SubtitleFont(screenModel = screenModel)
}
}
@Composable
private fun SubtitleFont(
screenModel: PlayerSettingsScreenModel,
) {
val boldSubtitles by screenModel.preferences.boldSubtitles().collectAsState()
val italicSubtitles by screenModel.preferences.italicSubtitles().collectAsState()
val subtitleFontSize by screenModel.preferences.subtitleFontSize().collectAsState()
val textColor by screenModel.preferences.textColorSubtitles().collectAsState()
val borderColor by screenModel.preferences.borderColorSubtitles().collectAsState()
val backgroundColor by screenModel.preferences.backgroundColorSubtitles().collectAsState()
val updateBold = {
val toBold = if (boldSubtitles) "no" else "yes"
screenModel.togglePreference(PlayerPreferences::boldSubtitles)
MPVLib.setPropertyString("sub-bold", toBold)
}
val updateItalic = {
val toItalicize = if (italicSubtitles) "no" else "yes"
screenModel.togglePreference(PlayerPreferences::italicSubtitles)
MPVLib.setPropertyString("sub-italic", toItalicize)
}
val onSizeChanged: (Int) -> Unit = {
MPVLib.setPropertyInt("sub-font-size", it)
screenModel.preferences.subtitleFontSize().set(it)
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier.fillMaxWidth(),
) {
Icon(
imageVector = Icons.Outlined.FormatSize,
contentDescription = null,
modifier = Modifier.size(32.dp),
)
OutlinedNumericChooser(
label = stringResource(id = R.string.player_font_size_text_field),
placeholder = "55",
suffix = "",
value = subtitleFontSize,
step = 1,
min = 1,
onValueChanged = onSizeChanged,
)
val boldAlpha = if (boldSubtitles) 1f else ReadItemAlpha
Icon(
imageVector = Icons.Outlined.FormatBold,
contentDescription = null,
modifier = Modifier
.alpha(boldAlpha)
.size(32.dp)
.clickable(onClick = updateBold),
)
val italicAlpha = if (italicSubtitles) 1f else ReadItemAlpha
Icon(
imageVector = Icons.Outlined.FormatItalic,
contentDescription = null,
modifier = Modifier
.alpha(italicAlpha)
.size(32.dp)
.clickable(onClick = updateItalic),
)
}
SubtitlePreview(
isBold = boldSubtitles,
isItalic = italicSubtitles,
textColor = Color(textColor),
borderColor = Color(borderColor),
backgroundColor = Color(backgroundColor),
)
}
}

View file

@ -0,0 +1,83 @@
package eu.kanade.tachiyomi.ui.player.settings.sheets.subtitle
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import eu.kanade.presentation.components.TabbedDialog
import eu.kanade.presentation.components.TabbedDialogPaddings
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.player.settings.PlayerSettingsScreenModel
import tachiyomi.presentation.core.components.material.padding
@Composable
fun SubtitleSettingsSheet(
screenModel: PlayerSettingsScreenModel,
onDismissRequest: () -> Unit,
) {
TabbedDialog(
onDismissRequest = onDismissRequest,
tabTitles = listOf(
stringResource(id = R.string.player_subtitle_settings_delay_tab),
stringResource(id = R.string.player_subtitle_settings_font_tab),
stringResource(id = R.string.player_subtitle_settings_color_tab),
),
hideSystemBars = true,
) { contentPadding, page ->
Column(
modifier = Modifier
.padding(contentPadding)
.padding(top = TabbedDialogPaddings.Vertical)
.verticalScroll(rememberScrollState()),
) {
when (page) {
0 -> SubtitleDelayPage(screenModel)
1 -> SubtitleFontPage(screenModel)
2 -> SubtitleColorPage(screenModel)
}
}
}
}
@Composable
fun SubtitlePreview(
isBold: Boolean,
isItalic: Boolean,
textColor: Color,
borderColor: Color,
backgroundColor: Color,
) {
Box(modifier = Modifier.padding(vertical = MaterialTheme.padding.medium)) {
Column(modifier = Modifier.fillMaxWidth(0.8f).background(color = backgroundColor)) {
Text(
text = stringResource(R.string.player_subtitle_settings_example),
modifier = Modifier.align(Alignment.CenterHorizontally),
style = TextStyle(
fontFamily = FontFamily.SansSerif,
fontSize = MaterialTheme.typography.titleMedium.fontSize,
fontWeight = if (isBold) FontWeight.Bold else FontWeight.Normal,
fontStyle = if (isItalic) FontStyle.Italic else FontStyle.Normal,
shadow = Shadow(color = borderColor, blurRadius = 7.5f),
color = textColor,
textAlign = TextAlign.Center,
),
)
}
}
}

View file

@ -114,7 +114,7 @@ class GestureHandler(
override fun onLongPress(e: MotionEvent) {
if (SeekState.mode == SeekState.LOCKED) { playerControls.toggleControls(); return }
activity.openScreenshotSheet()
activity.viewModel.showScreenshotOptions()
}
}

View file

@ -88,7 +88,6 @@ class PlayerControlsView @JvmOverloads constructor(context: Context, attrs: Attr
// Long click controls
binding.cycleSpeedBtn.setOnLongClickListener { activity.viewModel.showSpeedPicker(); true }
binding.cycleDecoderBtn.setOnLongClickListener { activity.viewModel.showDefaultDecoder(); true }
binding.prevBtn.setOnClickListener { switchEpisode(previous = true) }
binding.playBtn.setOnClickListener { playPause() }
@ -122,6 +121,12 @@ class PlayerControlsView @JvmOverloads constructor(context: Context, attrs: Attr
binding.cycleViewModeBtn.setOnClickListener { cycleViewMode() }
binding.settingsBtn.setOnClickListener { activity.viewModel.showPlayerSettings() }
binding.tracksBtn.setOnClickListener { activity.viewModel.showTracksCatalog() }
binding.chaptersBtn.setOnClickListener { activity.viewModel.showVideoChapters() }
binding.titleMainTxt.setOnClickListener { activity.viewModel.showEpisodeList() }
binding.titleSecondaryTxt.setOnClickListener { activity.viewModel.showEpisodeList() }
@ -163,14 +168,6 @@ class PlayerControlsView @JvmOverloads constructor(context: Context, attrs: Attr
}
}
internal suspend fun updateDecoderButton() {
withUIContext {
if (binding.cycleDecoderBtn.visibility == View.VISIBLE) {
binding.cycleDecoderBtn.text = HwDecState.mode.title
}
}
}
internal suspend fun updateSpeedButton() {
withUIContext {
binding.cycleSpeedBtn.text = context.getString(R.string.ui_speed, player.playbackSpeed)
@ -242,7 +239,7 @@ class PlayerControlsView @JvmOverloads constructor(context: Context, attrs: Attr
private val animationHandler = Handler(Looper.getMainLooper())
// Fade out Player controls
private val fadeOutControlsRunnable = Runnable { fadeOutControls() }
internal val fadeOutControlsRunnable = Runnable { fadeOutControls() }
internal fun lockControls(locked: Boolean) {
SeekState.mode = if (locked) SeekState.LOCKED else SeekState.NONE
@ -371,43 +368,48 @@ class PlayerControlsView @JvmOverloads constructor(context: Context, attrs: Attr
}
}
private var playerViewMode = AspectState.get(playerPreferences.playerViewMode().get())
private fun cycleViewMode() {
playerViewMode = when (playerViewMode) {
AspectState.STRETCH -> AspectState.FIT
AspectState.mode = when (AspectState.mode) {
AspectState.FIT -> AspectState.CROP
AspectState.CROP -> AspectState.STRETCH
else -> AspectState.FIT
}
setViewMode(showText = true)
}
internal fun setViewMode(showText: Boolean) {
binding.playerInformation.text = activity.getString(playerViewMode.stringRes)
when (playerViewMode) {
binding.playerInformation.text = activity.getString(AspectState.mode.stringRes)
var aspect = "-1"
var pan = "1.0"
when (AspectState.mode) {
AspectState.CROP -> {
mpvUpdateAspect(aspect = "-1", pan = "1.0")
pan = "1.0"
}
AspectState.FIT -> {
mpvUpdateAspect(aspect = "-1", pan = "0.0")
pan = "0.0"
}
AspectState.STRETCH -> {
val newAspect = "${activity.deviceWidth}/${activity.deviceHeight}"
mpvUpdateAspect(aspect = newAspect, pan = "1.0")
aspect = "${activity.deviceWidth}/${activity.deviceHeight}"
pan = "0.0"
}
AspectState.CUSTOM -> {
aspect = MPVLib.getPropertyString("video-aspect-override")
}
}
mpvUpdateAspect(aspect = aspect, pan = pan)
playerPreferences.playerViewMode().set(AspectState.mode.index)
if (showText) {
animationHandler.removeCallbacks(playerInformationRunnable)
binding.playerInformation.visibility = View.VISIBLE
animationHandler.postDelayed(playerInformationRunnable, 1000L)
}
playerPreferences.playerViewMode().set(playerViewMode.index)
}
private fun mpvUpdateAspect(aspect: String, pan: String) {
MPVLib.setOptionString("video-aspect-override", aspect)
MPVLib.setOptionString("panscan", pan)
MPVLib.setPropertyString("video-aspect-override", aspect)
MPVLib.setPropertyString("panscan", pan)
}
internal fun toggleAutoplay(isAutoplay: Boolean) {

View file

@ -40,6 +40,7 @@ enum class AspectState(val index: Int, @StringRes val stringRes: Int) {
CROP(index = 0, stringRes = R.string.video_crop_screen),
FIT(index = 1, stringRes = R.string.video_fit_screen),
STRETCH(index = 2, stringRes = R.string.video_stretch_screen),
CUSTOM(index = 3, stringRes = R.string.video_custom_screen),
;
companion object {
@ -62,14 +63,16 @@ enum class HwDecState(val title: String, val mpvValue: String) {
internal val isHwSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
internal val defaultHwDec = if (isHwSupported) HW_PLUS else HW
internal var mode: HwDecState = defaultHwDec
internal fun get(title: String) = when (title) {
"mediacodec" -> HW_PLUS
"mediacodec-copy" -> HW
"no" -> SW
else -> defaultHwDec
}
}
}
/**
* Player's Statistics Page handler
*/
enum class PlayerStatsPage(val page: Int, @StringRes val textRes: Int) {
OFF(0, R.string.off),
PAGE1(1, R.string.player_statistics_page_1),
PAGE2(2, R.string.player_statistics_page_2),
PAGE3(3, R.string.player_statistics_page_3),
;
}

View file

@ -1,49 +0,0 @@
package eu.kanade.tachiyomi.widget.sheet
import android.content.Context
import android.os.Bundle
import android.util.DisplayMetrics
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import com.google.android.material.bottomsheet.BottomSheetDialog
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.displayCompat
abstract class PlayerBottomSheetDialog(context: Context) : BottomSheetDialog(context) {
abstract fun createView(inflater: LayoutInflater): View
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val rootView = createView(layoutInflater)
setContentView(rootView)
// Enforce max width for tablets
val width = context.resources.getDimensionPixelSize(R.dimen.bottom_sheet_width)
if (width > 0) {
behavior.maxWidth = width
}
// Set peek height to 50% display height
context.displayCompat?.let {
val metrics = DisplayMetrics()
it.getRealMetrics(metrics)
behavior.peekHeight = metrics.heightPixels / 2
}
val bottomSheet = rootView.parent as ViewGroup
bottomSheet.systemUiVisibility = View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LOW_PROFILE
val window = window ?: return
WindowInsetsControllerCompat(window, bottomSheet).hide(WindowInsetsCompat.Type.systemBars())
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
}
}

View file

@ -1,40 +0,0 @@
package eu.kanade.tachiyomi.widget.sheet
import android.app.Activity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import eu.kanade.tachiyomi.databinding.CommonTabbedSheetBinding
import eu.kanade.tachiyomi.widget.ViewPagerAdapter
abstract class TabbedPlayerBottomSheetDialog(private val activity: Activity) : PlayerBottomSheetDialog(activity) {
lateinit var binding: CommonTabbedSheetBinding
override fun createView(inflater: LayoutInflater): View {
binding = CommonTabbedSheetBinding.inflate(activity.layoutInflater)
val adapter = LibrarySettingsSheetAdapter()
binding.pager.adapter = adapter
binding.tabs.setupWithViewPager(binding.pager)
return binding.root
}
abstract fun getTabs(): List<Pair<View, Int>>
private inner class LibrarySettingsSheetAdapter : ViewPagerAdapter() {
override fun createView(container: ViewGroup, position: Int): View {
return getTabs()[position].first
}
override fun getCount(): Int {
return getTabs().size
}
override fun getPageTitle(position: Int): CharSequence {
return activity.resources!!.getString(getTabs()[position].second)
}
}
}

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M19,1l-5,5v11l5,-4.5L19,1zM1,6v14.65c0,0.25 0.25,0.5 0.5,0.5 0.1,0 0.15,-0.05 0.25,-0.05C3.1,20.45 5.05,20 6.5,20c1.95,0 4.05,0.4 5.5,1.5L12,6c-1.45,-1.1 -3.55,-1.5 -5.5,-1.5S2.45,4.9 1,6zM23,19.5L23,6c-0.6,-0.45 -1.25,-0.75 -2,-1v13.5c-1.1,-0.35 -2.3,-0.5 -3.5,-0.5 -1.7,0 -4.15,0.65 -5.5,1.5v2c1.35,-0.85 3.8,-1.5 5.5,-1.5 1.65,0 3.35,0.3 4.75,1.05 0.1,0.05 0.15,0.05 0.25,0.05 0.25,0 0.5,-0.25 0.5,-0.5v-1.1z" />
</vector>

View file

@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19,1l-5,5v11l5,-4.5L19,1zM1,6v14.65c0,0.25 0.25,0.5 0.5,0.5 0.1,0 0.15,-0.05 0.25,-0.05C3.1,20.45 5.05,20 6.5,20c1.95,0 4.05,0.4 5.5,1.5L12,6c-1.45,-1.1 -3.55,-1.5 -5.5,-1.5S2.45,4.9 1,6zM23,19.5L23,6c-0.6,-0.45 -1.25,-0.75 -2,-1v13.5c-1.1,-0.35 -2.3,-0.5 -3.5,-0.5 -1.7,0 -4.15,0.65 -5.5,1.5v2c1.35,-0.85 3.8,-1.5 5.5,-1.5 1.65,0 3.35,0.3 4.75,1.05 0.1,0.05 0.15,0.05 0.25,0.05 0.25,0 0.5,-0.25 0.5,-0.5v-1.1z"/>
</vector>

View file

@ -21,6 +21,11 @@
android:layout_width="match_parent"
android:layout_height="match_parent" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/sheet_root"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"

View file

@ -1,29 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/optionsScrollView"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<LinearLayout
android:id="@+id/linear_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="32dp" >
<TextView
android:id="@+id/chapter_selection_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
tools:text="@string/chapter_dialog_header"
android:textAppearance="@style/TextAppearance.Tachiyomi.SectionHeader" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -109,31 +109,29 @@
<!-- Top Controls (Left)-->
<ImageButton
android:id="@+id/player_overflow"
android:id="@+id/settingsBtn"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginTop="10dp"
android:layout_marginRight="10dp"
android:background="?attr/selectableItemBackground"
android:contentDescription="Settings"
android:onClick="openOptionsSheet"
android:src="@drawable/ic_overflow_20dp"
app:layout_constraintLeft_toRightOf="@id/settingsBtn"
app:layout_constraintLeft_toRightOf="@id/tracksBtn"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/episodeListBtn"
app:tint="?attr/colorOnPrimarySurface" />
<ImageButton
android:id="@+id/settingsBtn"
android:id="@+id/tracksBtn"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="?attr/selectableItemBackground"
android:contentDescription="Settings"
android:onClick="openTracksSheet"
android:contentDescription="Tracks"
android:src="@drawable/ic_video_settings_20dp"
app:layout_constraintLeft_toRightOf="@id/chaptersBtn"
app:layout_constraintRight_toLeftOf="@id/player_overflow"
app:layout_constraintTop_toTopOf="@id/player_overflow"
app:layout_constraintRight_toLeftOf="@id/settingsBtn"
app:layout_constraintTop_toTopOf="@id/settingsBtn"
app:tint="?attr/colorOnPrimarySurface" />
<ImageButton
@ -142,27 +140,12 @@
android:layout_height="50dp"
android:background="?attr/selectableItemBackground"
android:contentDescription="Settings"
android:onClick="pickChapter"
android:src="@drawable/ic_video_chapter_24dp"
android:src="@drawable/ic_video_chapter_20dp"
android:visibility="gone"
app:layout_constraintLeft_toRightOf="@id/cycleDecoderBtn"
app:layout_constraintRight_toLeftOf="@id/settingsBtn"
app:layout_constraintTop_toTopOf="@id/settingsBtn"
app:tint="?attr/colorOnPrimarySurface" />
<TextView
android:id="@+id/cycleDecoderBtn"
android:layout_width="80dp"
android:layout_height="50dp"
android:background="?attr/selectableItemBackground"
android:onClick="switchDecoder"
android:gravity="center"
android:text="HW+"
android:textSize="12sp"
android:textColor="?attr/colorOnPrimarySurface"
app:layout_constraintLeft_toRightOf="@id/toggleAutoplay"
app:layout_constraintRight_toLeftOf="@id/chaptersBtn"
app:layout_constraintTop_toTopOf="@id/chaptersBtn" />
app:layout_constraintRight_toLeftOf="@id/tracksBtn"
app:layout_constraintTop_toTopOf="@id/tracksBtn"
app:tint="?attr/colorOnPrimarySurface" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/toggleAutoplay"
@ -170,8 +153,8 @@
android:layout_height="50dp"
tools:checked="true"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@id/cycleDecoderBtn"
app:layout_constraintTop_toTopOf="@id/cycleDecoderBtn" />
app:layout_constraintRight_toLeftOf="@id/chaptersBtn"
app:layout_constraintTop_toTopOf="@id/chaptersBtn" />
<!-- Audio -->

View file

@ -1,52 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/optionsScrollView"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/toggleVolumeBrightnessGestures"
android:layout_width="match_parent"
android:layout_height="56dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:text="@string/enable_volume_brightness_gestures"
android:textColor="?android:attr/textColorSecondary" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/toggleHorizontalSeekGesture"
android:layout_width="match_parent"
android:layout_height="56dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:text="@string/enable_horizontal_seek_gesture"
android:textColor="?android:attr/textColorSecondary" />
<!-- Stats preferences -->
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/toggleStats"
android:layout_width="match_parent"
android:layout_height="56dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:text="@string/toggle_stats"
android:textColor="?android:attr/textColorSecondary" />
<eu.kanade.tachiyomi.widget.MaterialSpinnerView
android:id="@+id/statsPage"
android:layout_width="match_parent"
android:layout_height="56dp"
android:entries="@array/stats_pages"
app:title="@string/stats_page" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -1,15 +0,0 @@
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/optionsScrollView"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/linear_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="32dp" >
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -104,8 +104,8 @@
</string-array>
<string-array name="stats_pages">
<item>@string/stats_page_1</item>
<item>@string/stats_page_2</item>
<item>@string/stats_page_3</item>
<item>@string/player_statistics_page_1</item>
<item>@string/player_statistics_page_2</item>
<item>@string/player_statistics_page_3</item>
</string-array>
</resources>

View file

@ -155,7 +155,7 @@
<string name="episode_download_progress">%1$d%%</string>
<string name="action_view_episodes">عرض الحلقات</string>
<string name="information_no_recent_anime">لم تتم مشاهدة أي شيء مؤخرا</string>
<string name="stats_page_1">الصفحة 1</string>
<string name="player_statistics_page_1">الصفحة 1</string>
<string name="anime_categories">فئات الأنمي</string>
<string name="enable_auto_play">التشغيل التلقائي مفعل</string>
<string name="pref_category_player">المشغل</string>
@ -166,7 +166,7 @@
<string name="action_mark_as_unseen">وضع علامة كـ\"غير مشاهد\"</string>
<string name="repeating_anime">إعادة المشاهدة</string>
<string name="pref_clear_anime_database_summary">حذف سجل الأنمي التي لم يتم حفظها في مكتبتك</string>
<string name="stats_page">صفحة الإحصائيات</string>
<string name="toggle_player_statistics_page">صفحة الإحصائيات</string>
<string name="action_display_download_badge_anime">الحلقات المحملة</string>
<string name="screenshot_show_subs">إظهار الترجمة في لقطة الشاشة</string>
<string name="pref_pip_on_exit">التبديل تلقائيا إلى وضع \"الصورة في الصورة\" عند الخروج من المشغل</string>
@ -196,14 +196,13 @@
<string name="action_sort_last_checked">اخر فحص</string>
<string name="action_sort_last_anime_update">أحدث تحديث للأنمي</string>
<string name="action_mark_as_seen">وضع علامة كـ\"مشاهد\"</string>
<string name="stats_page_2">الصفحة 2</string>
<string name="player_statistics_page_2">الصفحة 2</string>
<string name="notification_new_episodes">تم العثور على حلقات جديدة</string>
<string name="watching">مشاهدة</string>
<string name="pref_download_new_episodes">تحميل الحلقات الجديدة</string>
<string name="anime_from_library">الأنمي من المكتبة</string>
<string name="recent_anime_time">الحلقة. %1$s - %2$s</string>
<string name="stats_page_3">الصفحة 3</string>
<string name="toggle_stats">تبديل الإحصائيات</string>
<string name="player_statistics_page_3">الصفحة 3</string>
<string name="enable_horizontal_seek_gesture">تبديل إيماءة البحث الأفقية</string>
<string name="enable_volume_brightness_gestures">تفعيل إيماءات الصوت والسطوع</string>
<string name="screenshot_header">خد لقطة للشاشة</string>
@ -246,7 +245,7 @@
<string name="pref_waiting_time_aniskip_8">8 ثواني</string>
<string name="pref_waiting_time_aniskip">زر إنتهاء الوقت</string>
<string name="pref_waiting_time_aniskip_9">9 ثواني</string>
<string name="player_hwdec_dialog_title">تعيين وضع فك تشفير الأجهزة الافتراضي</string>
<string name="player_hwdec_mode">تعيين وضع فك تشفير الأجهزة الافتراضي</string>
<string name="pref_pip_episode_toasts">إظهار الإخطار المنبثق عند تبديل الحلقات في وضع \"(PiP) صورة في صورة\"</string>
<string name="notification_episodes_single">الحلقة %1$s</string>
<string name="pref_waiting_time_aniskip_6">6 ثواني</string>
@ -300,7 +299,6 @@
<item quantity="other">الحلقات التالية</item>
</plurals>
<string name="pref_show_next_episode_airing_time">إظهار وقت بث الحلقة القادمة</string>
<string name="stats_header">عرض الإحصائيات</string>
<string name="action_save_screenshot">حفظ لقطة شاشة</string>
<string name="pref_episode_swipe">تقييم الحلقة</string>
<string name="pref_episode_swipe_end">انتقد لتحسين العمل</string>

View file

@ -147,10 +147,9 @@
<string name="episode_progress_no_total">Progrés: %1$s</string>
<string name="screenshot_show_subs">Mostra els subtítols a la captura de pantalla</string>
<string name="screenshot_header">Fes captura de pantalla</string>
<string name="toggle_stats">Alternar estadístiques</string>
<string name="stats_page_1">Pàgina 1</string>
<string name="stats_page_2">Pàgina 2</string>
<string name="stats_page_3">Pàgina 3</string>
<string name="player_statistics_page_1">Pàgina 1</string>
<string name="player_statistics_page_2">Pàgina 2</string>
<string name="player_statistics_page_3">Pàgina 3</string>
<string name="recent_anime_time">Ep. %1$s - %2$s</string>
<string name="episode_settings_updated">Actualitzada configuració per defecte dels episodis</string>
<string name="information_no_recent_anime">Cap visualització recent</string>
@ -199,7 +198,7 @@
<string name="notification_episodes_single">Episodi %1$s</string>
<string name="player_aniskip_dontskip_toast">Salta en %d segons</string>
<string name="player_aniskip_skip">%s saltats</string>
<string name="player_hwdec_dialog_title">Establir per defecte el mode decodificació per hardware</string>
<string name="player_hwdec_mode">Establir per defecte el mode decodificació per hardware</string>
<string name="episode_settings">Ajustos del episodi</string>
<string name="quality_dialog_header">Canviar la qualitat del vídeo:</string>
<string name="want_to_watch">Pendent de visualitzar</string>
@ -222,7 +221,7 @@
<string name="default_anime_category">Categoria anime per defecte</string>
<string name="pref_progress_mark_as_seen">Punt on marcar l\'episodi com a vist</string>
<string name="pref_preserve_watching_position">Mantindre la posició de visionat als capítols finalitzats</string>
<string name="stats_page">Pàgina d\'estats</string>
<string name="toggle_player_statistics_page">Pàgina d\'estats</string>
<string name="label_anime_extensions">Extensions d\'anime</string>
<string name="label_migration_manga">Migrar el manga</string>
<string name="label_migration_anime">Migrar l\'anime</string>

View file

@ -175,7 +175,7 @@
<string name="enable_auto_play">Automatické přerávání je zapnuté</string>
<string name="action_change_intro_length">Změnit délku intra</string>
<string name="share_screenshot_info">%1$s: %2$s, %3$s</string>
<string name="stats_page">Stránka statistik</string>
<string name="toggle_player_statistics_page">Stránka statistik</string>
<string name="repeating_anime">Sleduji znovu</string>
<string name="player_controls_skip_intro_text">+%1$d s</string>
<string name="plan_to_watch">Plánuji sledovat</string>
@ -187,10 +187,9 @@
<string name="screenshot_show_subs">Zobrazit titulky ve snímku obrazovky</string>
<string name="enable_volume_brightness_gestures">Přepnutí gest pro hlasitost a jas</string>
<string name="enable_horizontal_seek_gesture">Přepnutí gesta pro horizontální posun</string>
<string name="toggle_stats">Přepnout statistiky</string>
<string name="stats_page_1">Strana 1</string>
<string name="stats_page_2">Strana 2</string>
<string name="stats_page_3">Strana 3</string>
<string name="player_statistics_page_1">Strana 1</string>
<string name="player_statistics_page_2">Strana 2</string>
<string name="player_statistics_page_3">Strana 3</string>
<string name="video_list_empty_error">Nenalezeno žádné video</string>
<string name="notification_new_episodes">Nalezeny nové epizody</string>
<string name="information_no_recent_anime">Nedávno nic nebylo sledováno</string>
@ -255,7 +254,7 @@
<string name="pref_enable_netflix_style_aniskip">Zapnout styl Netflixu</string>
<string name="player_aniskip_dontskip">Nepřeskakovat</string>
<string name="player_aniskip_skip">Přeskočeno %s</string>
<string name="player_hwdec_dialog_title">Nastavit výchozí hardwarový dekódovací režim</string>
<string name="player_hwdec_mode">Nastavit výchozí hardwarový dekódovací režim</string>
<string name="notification_episodes_single_and_more">Epizoda %1$s a dalších %2$d</string>
<string name="notification_episodes_multiple">Epizody %1$s</string>
<string name="episode_settings">Nastavení epizody</string>

View file

@ -167,11 +167,10 @@
<string name="screenshot_show_subs">Untertitel in Bildschirmfoto zeigen</string>
<string name="enable_volume_brightness_gestures">Lautstärke- und Helligkeitsgesten aktivieren</string>
<string name="enable_horizontal_seek_gesture">Horizontale Spul-Geste aktivieren</string>
<string name="toggle_stats">Statistiken aktivieren</string>
<string name="stats_page">Statistik-Seite</string>
<string name="stats_page_1">Seite 1</string>
<string name="stats_page_2">Seite 2</string>
<string name="stats_page_3">Seite 3</string>
<string name="toggle_player_statistics_page">Statistik-Seite</string>
<string name="player_statistics_page_1">Seite 1</string>
<string name="player_statistics_page_2">Seite 2</string>
<string name="player_statistics_page_3">Seite 3</string>
<string name="recent_anime_time">Flg. %1$s - %2$s</string>
<string name="download_insufficient_space">Herunterladen von Kapiteln aufgrund von zu wenig Speicherplatz nicht möglich</string>
<string name="download_queue_size_warning">Achtung: Große Downloads könnten dazu führen, dass Quellen langsamer werden und/oder Tachiyomi blockieren. Tippe, um mehr zu erfahren.</string>
@ -234,7 +233,7 @@
<string name="player_aniskip_dontskip">Nicht überspringen</string>
<string name="player_aniskip_dontskip_toast">Wird in %d Sekunden übersprungen</string>
<string name="player_aniskip_skip">%s übersprungen</string>
<string name="player_hwdec_dialog_title">Standard Hardware-Dekodierung wählen</string>
<string name="player_hwdec_mode">Standard Hardware-Dekodierung wählen</string>
<string name="notification_episodes_single">Folge %1$s</string>
<string name="notification_episodes_single_and_more">Folge %1$s und %2$d mehr</string>
<string name="notification_episodes_multiple">Folgen %1$s</string>
@ -276,7 +275,6 @@
<string name="pref_hide_in_anime_library_items">Animeeinträge verstecken, die schon in der Bibliothek sind</string>
<string name="choose_video_quality">Videoqualität auswählen:</string>
<string name="extension_settings">Erweiterungs-Einstellungen</string>
<string name="stats_header">Statistiken anzeigen</string>
<string name="action_save_screenshot">Bildschirmfoto speichern</string>
<string name="action_hide">Ausblenden</string>
<string name="label_recent_anime_updates">Anime-Aktualisierungen</string>

View file

@ -169,11 +169,10 @@
<string name="screenshot_show_subs">Mostrar subtítulos en captura de pantalla</string>
<string name="enable_volume_brightness_gestures">Gestos para cambiar el volumen y el brillo</string>
<string name="enable_horizontal_seek_gesture">Alternar con el gesto de la búsqueda horizontal</string>
<string name="toggle_stats">Alternar estadísticas</string>
<string name="stats_page">Página de las estadísticas</string>
<string name="stats_page_1">Página 1</string>
<string name="stats_page_2">Página 2</string>
<string name="stats_page_3">Página 3</string>
<string name="toggle_player_statistics_page">Página de las estadísticas</string>
<string name="player_statistics_page_1">Página 1</string>
<string name="player_statistics_page_2">Página 2</string>
<string name="player_statistics_page_3">Página 3</string>
<string name="recent_anime_time">Episodio %1$s - %2$s</string>
<string name="download_insufficient_space">No se pudo descargar ningún capítulo, queda muy poco espacio</string>
<string name="download_queue_size_warning">Advertencia: Las descargas grandes pueden llevar a que las fuentes se vuelvan cada vez más lentas y en casos extremos que los servidores limiten o impidan el acceso a Tachiyomi. Toca aquí para más información.</string>
@ -239,7 +238,7 @@
<string name="player_aniskip_dontskip">No te lo saltes</string>
<string name="player_aniskip_dontskip_toast">Omitir en %d segundos</string>
<string name="player_aniskip_skip">%s omitidos</string>
<string name="player_hwdec_dialog_title">Establecer el modo de descodificación por hardware por defecto</string>
<string name="player_hwdec_mode">Establecer el modo de descodificación por hardware por defecto</string>
<string name="notification_episodes_single">Episodio %1$s</string>
<string name="notification_episodes_single_and_more">Episodio %1$s y %2$d más</string>
<string name="notification_episodes_multiple">Episodios %1$s</string>
@ -284,7 +283,6 @@
<string name="choose_video_quality">Elige la calidad del vídeo:</string>
<string name="extension_settings">Configuración de las extensiones</string>
<string name="action_save_screenshot">Guardar la captura de pantalla</string>
<string name="stats_header">Mostrar las estadísticas</string>
<string name="chapter_dialog_header">Ir al capítulo</string>
<string name="pref_episode_swipe">Pasar episodio</string>
<string name="pref_episode_swipe_end">Deslizar a la derecha</string>

View file

@ -82,7 +82,6 @@
<string name="player_aniskip_dontskip">گذر نکن</string>
<string name="label_migration_manga">انتقال مانگا</string>
<string name="settings">تنظیمات</string>
<string name="stats_header">نمایش آمار و ارقام</string>
<string name="player_aniskip_op">گذر از اوپنینگ</string>
<string name="pref_show_next_episode_airing_time">نمایش زمان انتشار قسمت بعدی</string>
<string name="used_cache_both">اشغال توسط انیمه: %1$s، اشغال توسط مانگا: %2$s</string>
@ -93,7 +92,7 @@
<string name="plan_to_watch">برنامه برای مشاهده</string>
<string name="not_interesting">غیر جالب</string>
<string name="want_to_read">خواهان خواندن</string>
<string name="stats_page_3">صفحه 3</string>
<string name="player_statistics_page_3">صفحه 3</string>
<string name="download_notifier_download_paused_episodes">دانلود قسمت متوقف شد</string>
<string name="pref_invalidate_download_cache_summary">اجبار برنامه به بررسی مجدد چپتر و قسمت‌های دانلود شده</string>
<string name="no_next_episode">قسمت‌ بعدی پیدا نشد!</string>
@ -105,7 +104,7 @@
<string name="pref_waiting_time_aniskip">اتمام مهلت</string>
<string name="pref_waiting_time_aniskip_7">7 ثانیه</string>
<string name="pref_waiting_time_aniskip_9">9 ثانیه</string>
<string name="player_hwdec_dialog_title">تنظیم حالت پیشفرض رمزگشایی سخت افزاری</string>
<string name="player_hwdec_mode">تنظیم حالت پیشفرض رمزگشایی سخت افزاری</string>
<string name="pref_backup_flags">تنظیمات بکاپ</string>
<string name="label_migration_anime">انتقال انیمه</string>
<string name="data_saver_exclude">استثناء از صرفه‌جویی داده</string>
@ -135,7 +134,7 @@
<string name="pref_episode_swipe_end">کارکرد کشیدن به راست</string>
<string name="pref_episode_swipe_start">کارکرد کشیدن به چپ</string>
<string name="action_display_local_badge_anime">انیمه داخلی</string>
<string name="stats_page_1">صفحه 1</string>
<string name="player_statistics_page_1">صفحه 1</string>
<string name="action_filter_unseen">دیده نشده</string>
<string name="action_sort_last_anime_update">آخرین بروزرسانی انیمه</string>
<string name="action_sort_unseen_count">مقدار دیده نشده</string>
@ -159,7 +158,7 @@
<string name="delete_downloads_for_anime">حذف قسمت‌های دانلود شده؟</string>
<string name="display_mode_episode">قسمت %1$s</string>
<string name="dialog_with_checkbox_reset_anime">بازنشانی همه قسمت‌های مشاهده شده برای این انیمه</string>
<string name="stats_page_2">صفحه 2</string>
<string name="player_statistics_page_2">صفحه 2</string>
<string name="episode_progress">پیشرفت: %1$s از %2$s</string>
<string name="download_queue_size_warning">اخطار: دانلود انبوه ممکن است منجر به کند شدن منابع یا مسدود شدن آنی‌یومی شود. برای اطلاعات بیشتر لمس کنید.</string>
<string name="player_overlay_back">عقب</string>
@ -226,10 +225,9 @@
<string name="action_play_internally">پخش داخلی</string>
<string name="extension_settings">تنظیمات افزونه‌ها</string>
<string name="watching">در حال مشاهده</string>
<string name="toggle_stats">تغییر آمار</string>
<string name="player_controls_skip_intro_text">%1$d ثانیه</string>
<string name="pref_bottom_nav_no_manga">انتقال مانگا به برگه \"بیشتر\"</string>
<string name="stats_page">صفحه آمار</string>
<string name="toggle_player_statistics_page">صفحه آمار</string>
<string name="episode_progress_no_total">پیشرفت: %1$s</string>
<string name="pref_mpv_conf">ویرایش فایل پیکربندی MPV برای تنظیمات بیشتر پخش کننده</string>
<string name="download_unseen">دیده نشده</string>

View file

@ -80,8 +80,8 @@
<string name="pref_skip_disable">Poista käytöstä</string>
<string name="pref_player_smooth_seek">Ota tarkka haku käyttöön</string>
<string name="want_to_read">Haluan lukea</string>
<string name="stats_page_2">Sivu 2</string>
<string name="stats_page_3">Sivu 3</string>
<string name="player_statistics_page_2">Sivu 2</string>
<string name="player_statistics_page_3">Sivu 3</string>
<string name="action_bookmark_episode">Lisää jakso kirjanmerkkeihin</string>
<string name="anime_categories">Animeluokat</string>
<string name="action_download_unseen">Lataa katsomattomat jaksot</string>
@ -91,10 +91,10 @@
<string name="action_display_download_badge_anime">Lataa jaksot</string>
<string name="pref_category_player_orientation">Suunta</string>
<string name="pref_default_intro_length">Esittelyn ohituksen oletuspituus</string>
<string name="stats_page_1">Sivu 1</string>
<string name="player_statistics_page_1">Sivu 1</string>
<string name="pref_always_use_external_player">Käytä aina ulkoista toisto-ohjelmaa</string>
<string name="pref_download_new_episodes">Lataa uudet jaksot</string>
<string name="stats_page">Tilastosivu</string>
<string name="toggle_player_statistics_page">Tilastosivu</string>
<string name="snack_add_to_anime_library">Lisätäänkö anime kirjastoon\?</string>
<string name="screenshot_header">Ota näyttökuva</string>
<string name="display_mode_episode">Jakso %1$s</string>

View file

@ -77,7 +77,6 @@
<string name="sort_by_episode_number">Ayon sa bilang ng episode</string>
<string name="enable_volume_brightness_gestures">Paganahin ang mga gesture ng Volume at Linawag</string>
<string name="enable_horizontal_seek_gesture">Paganahin ang Pahalang na Hanapin ang Gesture</string>
<string name="toggle_stats">I-toggle ang mga istatistika</string>
<string name="pref_category_player_aniskip_info">Kinakailangan ng AniSkip na masubaybayan ang anime sa MAL o Anilist upang gumana</string>
<string name="pref_default_home_tab_library">Itakda ang panimulang screen sa Manga Tab</string>
<string name="pref_anime_library_update_categories_details">Ang mga anime sa mga ibinukod na kategorya ay hindi maa-update kahit na sila ay kasama rin sa mga kategoryang kasama.</string>
@ -113,7 +112,7 @@
<string name="player_aniskip_skip">Nilaktawan ng %s</string>
<string name="pref_enable_netflix_style_aniskip">Paganahin ang istilo ng Netflix</string>
<string name="player_aniskip_dontskip">Huwag laktawan</string>
<string name="player_hwdec_dialog_title">Itakda ang default na hardware decoding mode</string>
<string name="player_hwdec_mode">Itakda ang default na hardware decoding mode</string>
<string name="notification_episodes_multiple">Mga Episode %1$s</string>
<string name="notification_episodes_single_and_more">Episode %1$s at %2$d pa</string>
<string name="pref_backup_flags">Mga opsyon sa pag-backup</string>
@ -139,7 +138,7 @@
<string name="display_mode_episode">Episode %1$s</string>
<string name="share_screenshot_info">%1$s: %2$s, %3$s</string>
<string name="dialog_with_checkbox_reset_anime">I-reset ang lahat ng episode para sa anime na ito</string>
<string name="stats_page_1">Pahina 1</string>
<string name="player_statistics_page_1">Pahina 1</string>
<string name="recent_anime_time">Ep. %1$s - %2$s</string>
<string name="video_list_empty_error">Walang nakitang video</string>
<string name="episode_settings">Mga setting ng episode</string>
@ -200,7 +199,7 @@
<item quantity="one">1 bagong episode</item>
<item quantity="other">%1$d (na) mga bagong episode</item>
</plurals>
<string name="stats_page_3">Pahina 3</string>
<string name="player_statistics_page_3">Pahina 3</string>
<string name="action_edit_manga_categories">I-edit ang mga kategorya ng manga</string>
<string name="action_display_show_continue_reading_button">Ipakita ang pindutan sa tuluyang panonood/pagbasa</string>
<string name="action_start_download_externally">Gumamit ng external downloader</string>
@ -249,8 +248,8 @@
<string name="screenshot_show_subs">Ipakita ang mga subtitle sa screenshot</string>
<string name="dialog_with_checkbox_remove_description_anime">Aalisin nito ang petsa ng panonood ng episode na ito. Sigurado ka ba\?</string>
<string name="episode_progress">Progress: %1$s/%2$s</string>
<string name="stats_page">Pahina ng istatistika</string>
<string name="stats_page_2">Pahina 2</string>
<string name="toggle_player_statistics_page">Pahina ng istatistika</string>
<string name="player_statistics_page_2">Pahina 2</string>
<string name="notification_new_episodes">May nakitang mga bagong episode</string>
<string name="episode_settings_updated">Na-update ang mga default na setting ng episode</string>
<string name="download_notifier_download_paused_episodes">Na-pause ang pag-download ng episode</string>
@ -298,7 +297,6 @@
<string name="data_saver">Data Saver</string>
<string name="resmush">resmush.it</string>
<string name="bandwidth_data_saver_server">Bandwidth Hero Proxy Server</string>
<string name="stats_header">Ipakita ang statistika</string>
<string name="pref_hide_in_manga_library_items">Itago ang mga entry na nasa library na</string>
<string name="pref_hide_in_anime_library_items">Itago ang mga entry na nasa library</string>
<string name="chapter_dialog_header">Mag-seek sa chapter</string>

View file

@ -155,9 +155,9 @@
<string name="screenshot_show_subs">Afficher les sous-titres dans la capture d\'écran</string>
<string name="enable_volume_brightness_gestures">Gestes de basculement du volume et de la luminosité</string>
<string name="enable_horizontal_seek_gesture">Activer le geste de recherche horizontale</string>
<string name="stats_page_1">Page 1</string>
<string name="stats_page_2">Page 2</string>
<string name="stats_page_3">Page 3</string>
<string name="player_statistics_page_1">Page 1</string>
<string name="player_statistics_page_2">Page 2</string>
<string name="player_statistics_page_3">Page 3</string>
<string name="recent_anime_time">Ép. %1$s - %2$s</string>
<string name="download_insufficient_space">Impossible de télécharger les chapitres, l\'espace de stockage est insuffisant</string>
<string name="download_queue_size_warning">Attention : les téléchargements massifs peuvent entraîner un ralentissement des sources ou le blocage de Tachiyomi. Appuyez pour en savoir plus.</string>
@ -231,8 +231,7 @@
<string name="player_aniskip_recap">Passer le récap</string>
<string name="notification_episodes_multiple">Épisodes %1$s</string>
<string name="episode_settings">Paramètres des épisodes</string>
<string name="toggle_stats">Activer les statistiques</string>
<string name="stats_page">Pages des statistiques</string>
<string name="toggle_player_statistics_page">Pages des statistiques</string>
<string name="download_notifier_download_paused_episodes">Téléchargement d\'épisodes en pause</string>
<string name="pref_category_player_aniskip_info">Paramètres AniSkip</string>
<string name="label_anime_extensions">Extensions d\'animés</string>
@ -267,7 +266,7 @@
<string name="pref_track_on_add_library">Ouvrir le menu de pistes lors dun ajout à la bibliothèque</string>
<string name="player_aniskip_mixedOp">Ignorer MixedOp</string>
<string name="pref_search_pinned_manga_sources_only">N\'inclure que les sources de manga épinglées</string>
<string name="player_hwdec_dialog_title">Définir le mode de décodage matériel par défaut</string>
<string name="player_hwdec_mode">Définir le mode de décodage matériel par défaut</string>
<string name="unofficial_anime_extension_message">Cette extension ne fait pas partie de la liste officielle des extensions Aniyomi.</string>
<string name="pref_enable_pip">Permettre l\'utilisation du mode PiP</string>
<string name="pref_player_smooth_seek_summary">Si activé, la recherche ne va pas se concentrer sur les images-clés, permettant de faire une recherche plus précise mais plus lente</string>

View file

@ -190,7 +190,6 @@
<string name="pref_clear_anime_database_summary">מחק היסטוריה עבור אנימה שאינה נשמרת בספרייה שלך</string>
<string name="anime_from_library">אנימה מתוך הספרייה</string>
<string name="download_unseen">לא נצפה</string>
<string name="stats_header">הצג סטטיסטיקות</string>
<string name="pref_invalidate_download_cache_summary">הכריח את האפליקציה לבדוק מחדש את הפרקים והפרקים שהורדת</string>
<string name="video_crop_screen">חתוך לגודל המסך</string>
<string name="player_aniskip_op">דלג על הפתיחה</string>
@ -202,7 +201,7 @@
<string name="pref_enable_netflix_style_aniskip">אפשר סגנון נטפליקס</string>
<string name="player_aniskip_dontskip">אל תדלג</string>
<string name="player_aniskip_skip">%s דולג</string>
<string name="player_hwdec_dialog_title">הגדר מצב פענוח חומרה ברירת מחדל</string>
<string name="player_hwdec_mode">הגדר מצב פענוח חומרה ברירת מחדל</string>
<string name="notification_episodes_single">פרק %1$s</string>
<string name="episode_settings">הגדרות פרק</string>
<string name="label_manga_extensions">הרחבות מנגה</string>
@ -247,7 +246,7 @@
<string name="notification_episodes_multiple">פרקים %1$s</string>
<string name="snack_add_to_anime_library">להוסיף אנימה לספרייה\?</string>
<string name="downloaded_episodes">פרקים שהורדו</string>
<string name="stats_page_1">עמוד 1</string>
<string name="player_statistics_page_1">עמוד 1</string>
<string name="want_to_read">רוצה לקרוא</string>
<string name="want_to_watch">רוצה לצפות</string>
<string name="local_manga_source">מקור מנגה מקומי</string>
@ -265,9 +264,8 @@
<string name="dialog_with_checkbox_reset_anime">אפס את כל הפרקים עבור האנימה הזו</string>
<string name="episode_progress">התקדמות: %1$s/%2$s</string>
<string name="screenshot_header">צלם צילום מסך</string>
<string name="toggle_stats">הצג סטטיסטיקות</string>
<string name="stats_page">דף סטטיסטיקות</string>
<string name="stats_page_2">עמוד 2</string>
<string name="toggle_player_statistics_page">דף סטטיסטיקות</string>
<string name="player_statistics_page_2">עמוד 2</string>
<string name="video_list_empty_error">לא נמצא סרטון</string>
<string name="information_no_recent_anime">שום דבר לא נצפה לאחרונה</string>
<string name="episode_settings_updated">הגדרות ברירת המחדל המעודכנות של פרק</string>
@ -288,7 +286,7 @@
<string name="currently_watching">כרגע צופה</string>
<string name="share_screenshot_info">%1$s: %2$s, %3$s</string>
<string name="screenshot_show_subs">הצג כתוביות בצילום מסך</string>
<string name="stats_page_3">עמוד 3</string>
<string name="player_statistics_page_3">עמוד 3</string>
<string name="recent_anime_time">פרק %1$s - %2$s</string>
<string name="label_anime_history">אנימה</string>
<string name="notification_new_episodes">נמצאו פרקים חדשים</string>

View file

@ -93,8 +93,7 @@
<string name="action_display_download_badge_anime">Preuzete epizode</string>
<string name="action_sort_unseen_count">Broj nepogledanih</string>
<string name="episode_progress_no_total">Napredak: %1$s</string>
<string name="stats_page_3">Stranica 3</string>
<string name="toggle_stats">Uklj./Isklj. statistiku</string>
<string name="player_statistics_page_3">Stranica 3</string>
<string name="video_list_empty_error">Nije pronađen nijedan video</string>
<string name="subtitle_dialog_header">Titlovi</string>
<string name="audio_dialog_header">Audio</string>
@ -120,7 +119,7 @@
<string name="pref_waiting_time_aniskip_10">10 sekunda</string>
<string name="player_aniskip_dontskip_toast">Preskoči za %d sekunde</string>
<string name="player_aniskip_skip">%s preskočeno</string>
<string name="player_hwdec_dialog_title">Postavi standardni modus hardverskog dekodiranja</string>
<string name="player_hwdec_mode">Postavi standardni modus hardverskog dekodiranja</string>
<string name="notification_episodes_multiple">Epizode %1$s</string>
<string name="episode_settings">Postavke epizode</string>
<string name="pref_backup_flags">Opcije za sigurnosne kopije</string>
@ -244,10 +243,9 @@
<string name="want_to_read">Želim čitati</string>
<string name="screenshot_header">Snimi snimku ekrana</string>
<string name="screenshot_show_subs">Prikaži titlove u snimci ekrana</string>
<string name="stats_header">Prikaži statistiku</string>
<string name="stats_page">Stranica statistike</string>
<string name="stats_page_1">Stranica 1</string>
<string name="stats_page_2">Stranica 2</string>
<string name="toggle_player_statistics_page">Stranica statistike</string>
<string name="player_statistics_page_1">Stranica 1</string>
<string name="player_statistics_page_2">Stranica 2</string>
<string name="recent_anime_time">Epizoda %1$s %2$s</string>
<string name="notification_new_episodes">Pronađene su nove epizode</string>
<string name="download_notifier_download_paused_episodes">Preuzimanje epizode zaustavljeno</string>

View file

@ -165,11 +165,10 @@
<string name="screenshot_show_subs">Tampilkan subtitel di tangkap layar</string>
<string name="enable_volume_brightness_gestures">Ubah volume dan gestur kecerahan</string>
<string name="enable_horizontal_seek_gesture">ubah gestur gerakan horisontal</string>
<string name="toggle_stats">Ubah status</string>
<string name="stats_page">Halaman status</string>
<string name="stats_page_1">halaman 1</string>
<string name="stats_page_2">Halaman 2</string>
<string name="stats_page_3">Halaman 3</string>
<string name="toggle_player_statistics_page">Halaman status</string>
<string name="player_statistics_page_1">halaman 1</string>
<string name="player_statistics_page_2">Halaman 2</string>
<string name="player_statistics_page_3">Halaman 3</string>
<string name="recent_anime_time">Ep. %1$s - %2$s</string>
<string name="download_insufficient_space">Tidak dapat mengunduh karena ruang penyimpanan rendah</string>
<string name="download_queue_size_warning">Peringatan: mengunduh dalam jumlah besar bisa menyebabkan sumber menjadi lambat dan/atau memblokir Tachiyomi. Ketuk untuk mempelajari lebih lanjut.</string>
@ -229,7 +228,7 @@
<string name="player_aniskip_dontskip">Jangan di skip</string>
<string name="player_aniskip_dontskip_toast">Lewati dalam %d detik</string>
<string name="player_aniskip_skip">%s lewati</string>
<string name="player_hwdec_dialog_title">Pengaturan standar mode decoding hardware</string>
<string name="player_hwdec_mode">Pengaturan standar mode decoding hardware</string>
<string name="notification_episodes_single">Episode %1$s</string>
<string name="notification_episodes_single_and_more">Episode %1$s dan %2$d lainnya</string>
<string name="notification_episodes_multiple">Episode %1$s</string>
@ -269,7 +268,6 @@
<string name="copied_video_link_to_clipboard">Tautan kualitas video yang disalin ke papan klip</string>
<string name="choose_video_quality">Pilih kualitas video:</string>
<string name="extension_settings">Pengaturan ekstensi</string>
<string name="stats_header">Tampilkan statistik</string>
<string name="action_save_screenshot">Tangkap layar disimpan</string>
<string name="chapter_dialog_header">Geser untuk ke chapter</string>
<string name="data_saver_stop_exclude">Menghentikan pengecualian dari penghemat data</string>

View file

@ -146,8 +146,7 @@
<string name="player_aniskip_dontskip">Non saltare</string>
<string name="video_crop_screen">Centrato</string>
<string name="playback_speed_dialog_reset">Ripristina</string>
<string name="toggle_stats">Attiva statistiche</string>
<string name="player_hwdec_dialog_title">Imposta la modalità di decodifica hardware predefinita</string>
<string name="player_hwdec_mode">Imposta la modalità di decodifica hardware predefinita</string>
<string name="rotation_sensor_portrait">Sensore portrait</string>
<string name="action_sort_unseen_count">Conteggio di non visti</string>
<string name="action_sort_last_anime_update">Ultimo aggiornamento anime</string>
@ -219,10 +218,10 @@
<string name="share_screenshot_info">%1$s: %2$s, %3$s</string>
<string name="episode_progress_no_total">Progresso: %1$s</string>
<string name="screenshot_header">Fai uno screenshot</string>
<string name="stats_page">Statistiche</string>
<string name="stats_page_1">Pagina 1</string>
<string name="stats_page_2">Pagina 2</string>
<string name="stats_page_3">Pagina 3</string>
<string name="toggle_player_statistics_page">Statistiche</string>
<string name="player_statistics_page_1">Pagina 1</string>
<string name="player_statistics_page_2">Pagina 2</string>
<string name="player_statistics_page_3">Pagina 3</string>
<string name="recent_anime_time">Ep. %1$s - %2$s</string>
<string name="notification_new_episodes">Nuovi episodi trovati</string>
<string name="episode_settings_updated">Aggiornate le impostazioni di default per gli episodi</string>

View file

@ -165,11 +165,10 @@
<string name="screenshot_show_subs">스크린 샷에 자막 표시</string>
<string name="enable_volume_brightness_gestures">볼륨 및 밝기 제스처 활성화</string>
<string name="enable_horizontal_seek_gesture">가로 탐색 제스처 활성화</string>
<string name="toggle_stats">통계 활성화</string>
<string name="stats_page">통계 페이지</string>
<string name="stats_page_1">페이지 1</string>
<string name="stats_page_2">페이지 2</string>
<string name="stats_page_3">3페이지</string>
<string name="toggle_player_statistics_page">통계 페이지</string>
<string name="player_statistics_page_1">페이지 1</string>
<string name="player_statistics_page_2">페이지 2</string>
<string name="player_statistics_page_3">3페이지</string>
<string name="recent_anime_time">%1$s %2$s화</string>
<string name="download_insufficient_space">저장 공간이 부족하여 회차를 다운로드 할 수 없습니다</string>
<string name="download_queue_size_warning">경고: 대량 다운로드는 소스가 느려지거나 Tachiyomi를 차단할 수 있습니다. 탭하여 자세히 알아보기.</string>
@ -229,7 +228,7 @@
<string name="player_aniskip_dontskip">건너뛰지 않기</string>
<string name="player_aniskip_dontskip_toast">%d초 후에 건너뛰기</string>
<string name="player_aniskip_skip">%s 건너뜀</string>
<string name="player_hwdec_dialog_title">기본 하드웨어 디코딩 모드 설정</string>
<string name="player_hwdec_mode">기본 하드웨어 디코딩 모드 설정</string>
<string name="notification_episodes_single">에피소드 %1$s</string>
<string name="notification_episodes_single_and_more">에피소드 %1$s와 그 외 %2$d</string>
<string name="notification_episodes_multiple">에피소드 %1$s</string>
@ -269,7 +268,6 @@
<string name="choose_video_quality">동영상 화질 선택:</string>
<string name="extension_settings">확장 앱 설정</string>
<string name="action_save_screenshot">스크린샷 저장</string>
<string name="stats_header">통계 표시</string>
<string name="copied_video_link_to_clipboard">영상 화질 링크 주소가 클립보드에 복사되었습니다</string>
<string name="pref_show_next_episode_airing_time">다음 에피소드 방송일 표시</string>
<string name="pref_episode_swipe_start">왼쪽으로 스와이프</string>

View file

@ -317,7 +317,7 @@
<string name="choose_video_quality">Wybierz jakość wideo:</string>
<string name="want_to_read">Chcę przeczytać</string>
<string name="pref_mpv_conf">Edytuj plik konfiguracyjny MPV w celu dalszej konfiguracji odtwarzacza</string>
<string name="player_hwdec_dialog_title">Ustaw domyślny tryb dekodowania sprzętowego</string>
<string name="player_hwdec_mode">Ustaw domyślny tryb dekodowania sprzętowego</string>
<string name="pref_default_landscape_orientation">Domyślny krajobrazowe</string>
<string name="local_manga_source">Lokalne źródło mangi</string>
<string name="playback_options_quality">Jakość</string>

View file

@ -131,9 +131,9 @@
<string name="episode_progress_no_total">Progresso: %1$s</string>
<string name="screenshot_header">Fazer captura de tela</string>
<string name="screenshot_show_subs">Mostrar legendas na captura de tela</string>
<string name="stats_page_1">Página 1</string>
<string name="stats_page_2">Página 2</string>
<string name="stats_page_3">Página 3</string>
<string name="player_statistics_page_1">Página 1</string>
<string name="player_statistics_page_2">Página 2</string>
<string name="player_statistics_page_3">Página 3</string>
<string name="recent_anime_time">Ep. %1$s - %2$s</string>
<string name="download_insufficient_space">Não foi possível fazer o download devido ao pouco espaço de armazenamento</string>
<string name="download_queue_size_warning">Aviso: grandes downloads em massa podem tornar as fontes mais lentas e/ou começarem a bloquear o Aniyomi. Toque para saber mais.</string>
@ -199,7 +199,7 @@
<string name="settings">Configurações</string>
<string name="pref_category_internal_player">Player interno</string>
<string name="entries">Entradas da biblioteca</string>
<string name="stats_page">Página de status</string>
<string name="toggle_player_statistics_page">Página de status</string>
<string name="download_notifier_download_paused_episodes">Download de episódio pausado</string>
<string name="action_play_externally">Reproduzir externamente</string>
<string name="action_play_internally">Reproduzir internamente</string>
@ -247,7 +247,7 @@
<string name="pref_player_fullscreen">Mostrar conteúdo no recorte de exibição</string>
<string name="enable_volume_brightness_gestures">Ativar Gestos de Volume e Brilho</string>
<string name="pref_default_intro_length">Duração padrão do pulo de abertura</string>
<string name="player_hwdec_dialog_title">Definir o modo de decodificação de hardware padrão</string>
<string name="player_hwdec_mode">Definir o modo de decodificação de hardware padrão</string>
<string name="rotation_sensor_landscape">Paisagem (Sensor)</string>
<string name="download_ahead_info_anime">Funciona apenas em entradas na biblioteca e se o episódio atual e o próximo já estiverem baixados</string>
<string name="rotation_reverse_landscape">Paisagem reversa</string>
@ -255,7 +255,6 @@
<string name="video_stretch_screen">Esticado para a tela</string>
<string name="pref_track_on_add_library">Abra o menu de rastreadores ao adicionar à biblioteca</string>
<string name="download_unseen">Não visto</string>
<string name="toggle_stats">Alternar estatísticas</string>
<string name="player_aniskip_recap">Pular recapitulação</string>
<string name="episode_settings_updated">Configurações de episódio padrão atualizadas</string>
<string name="video_crop_screen">Cortado para a tela</string>
@ -284,7 +283,6 @@
<string name="pref_hide_in_anime_library_items">Ocultar entradas de anime já na biblioteca</string>
<string name="pref_hide_in_manga_library_items">Ocultar entradas de mangá já na biblioteca</string>
<string name="action_save_screenshot">Salvar captura de tela</string>
<string name="stats_header">Mostrar estatisticas</string>
<string name="chapter_dialog_header">Procurar capítulo</string>
<string name="data_saver">Economia de dados</string>
<string name="data_saver_summary">Compactar imagens antes de baixar ou carregar no leitor</string>

View file

@ -119,9 +119,9 @@
<string name="episode_progress_no_total">Progresso: %1$s</string>
<string name="screenshot_header">Fazer captura de ecrã</string>
<string name="screenshot_show_subs">Mostrar legendas na captura de ecrã</string>
<string name="stats_page_1">Página 1</string>
<string name="stats_page_2">Página 2</string>
<string name="stats_page_3">Página 3</string>
<string name="player_statistics_page_1">Página 1</string>
<string name="player_statistics_page_2">Página 2</string>
<string name="player_statistics_page_3">Página 3</string>
<string name="recent_anime_time">Ep. %1$s - %2$s</string>
<string name="download_insufficient_space">Não foi possível transferir devido a falta de espaço de armazenamento</string>
<string name="download_queue_size_warning">Aviso: descargas grandes em massa podem levar as fontes a ficarem lentas e/ou começarem a bloquear o Tachiyomi. Toque para saber mais.</string>
@ -198,7 +198,7 @@
<string name="pref_category_external_downloader">Downloader externo</string>
<string name="pref_use_external_downloader">Sempre usar downloader externo para anime</string>
<string name="pref_external_downloader_selection">App de download preferido</string>
<string name="stats_page">Página de estatísticas</string>
<string name="toggle_player_statistics_page">Página de estatísticas</string>
<string name="playback_speed_dialog_reset">Repor</string>
<string name="player_aniskip_dontskip">Não saltar</string>
<string name="pref_library_manga_columns">Mangás por linha</string>
@ -230,11 +230,10 @@
<string name="pref_remember_brightness">Lembrar e mudar para o último brilho usado</string>
<string name="pref_mpv_conf">Editar o ficheiro de configuração do MPV para obter mais definições do reprodutor</string>
<string name="enable_volume_brightness_gestures">Ativar Gestos de Volume e Brilho</string>
<string name="toggle_stats">Alternar estatísticas</string>
<string name="video_stretch_screen">Esticado ao ecrã</string>
<string name="action_change_intro_length">Alterar a duração da abertura</string>
<string name="player_aniskip_skip">%s saltado</string>
<string name="player_hwdec_dialog_title">Definir o modo de descodificação de hardware padrão</string>
<string name="player_hwdec_mode">Definir o modo de descodificação de hardware padrão</string>
<plurals name="seconds">
<item quantity="one">%d segundo</item>
<item quantity="many">%d segundos</item>
@ -282,7 +281,6 @@
<string name="chapter_dialog_header">Procurar capítulo</string>
<string name="pref_hide_in_manga_library_items">Ocultar entradas de manga já na biblioteca</string>
<string name="pref_hide_in_anime_library_items">Ocultar entradas de anime já na biblioteca</string>
<string name="stats_header">Mostrar estatísticas</string>
<string name="copied_video_link_to_clipboard">Ligação de qualidade de vídeo copiada para a área de transferência</string>
<string name="choose_video_quality">Escolha a qualidade do vídeo:</string>
<string name="extension_settings">Configurações de extensão</string>

View file

@ -141,9 +141,9 @@
<string name="episode_progress_no_total">İlerleme: %1$s</string>
<string name="episode_progress">İlerleme: %1$s/%2$s</string>
<string name="screenshot_show_subs">Ekran görüntüsünde alt yazıları göster</string>
<string name="stats_page_1">Sayfa 1</string>
<string name="stats_page_2">Sayfa 2</string>
<string name="stats_page_3">Sayfa 3</string>
<string name="player_statistics_page_1">Sayfa 1</string>
<string name="player_statistics_page_2">Sayfa 2</string>
<string name="player_statistics_page_3">Sayfa 3</string>
<string name="player_controls_skip_intro_text">+%1$d s</string>
<string name="no_next_episode">Sonraki bölüm bulunamadı!</string>
<string name="label_history">Manga</string>
@ -225,7 +225,6 @@
<string name="anime_from_library">Kütüphaneden Anime</string>
<string name="plan_to_watch">İzlemeyi planla</string>
<string name="enable_horizontal_seek_gesture">Yatay hareketle göz atmayı aktifleştir</string>
<string name="toggle_stats">İstatistikleri değiştir</string>
<string name="recent_anime_time">Bölüm %1$s - %2$s</string>
<string name="subtitle_dialog_header">Altyazı</string>
<string name="notification_episodes_multiple">%1$s bölüm</string>
@ -237,7 +236,7 @@
<string name="pref_pip_episode_toasts">Pencere içinde pencere modunda bölüm değiştirirken bölüm bildirimlerini gösterme</string>
<string name="currently_reading">Şu anda okunuyor</string>
<string name="want_to_watch">İzlemek istiyorum</string>
<string name="stats_page">İstatistikler</string>
<string name="toggle_player_statistics_page">İstatistikler</string>
<string name="episode_settings_updated">Varsayılan bölüm ayarları güncellendi</string>
<string name="pref_invalidate_download_cache_summary">Uygulamayı indirilen bölümleri ve kısımları yeniden kontrol etmeye zorla</string>
<string name="audio_dialog_header">Ses</string>
@ -273,11 +272,10 @@
<string name="player_aniskip_recap">Özeti Atla</string>
<string name="pref_waiting_time_aniskip">Düğme zaman aşımı</string>
<string name="player_aniskip_mixedOp">MixedOp\'u atla</string>
<string name="player_hwdec_dialog_title">Varsayılan donanım kod çözme modunu ayarla</string>
<string name="player_hwdec_mode">Varsayılan donanım kod çözme modunu ayarla</string>
<string name="label_migration_manga">Manga\'yı Taşı</string>
<string name="label_migration_anime">Anime\'yi Taşı</string>
<string name="chapter_dialog_header">Bölüm ara</string>
<string name="stats_header">İstatistikleri göster</string>
<string name="action_save_screenshot">Ekran görüntüsünü kaydet</string>
<string name="pref_media_control_chapter_seeking_summary">Eģer etkinleştirilirse, sıradaki bölüm düğmesine basıldığında ve sırada bölüm yok ise 85 saniye ileri atlar</string>
<string name="go_to_next_chapter">Sıradaki bölüm</string>

View file

@ -152,7 +152,7 @@
<string name="episode_progress_no_total">Прогрес: %1$s</string>
<string name="enable_volume_brightness_gestures">Ввімкнути жести гучності та яскравості</string>
<string name="enable_horizontal_seek_gesture">Ввімкнути жест горизонтального пошуку</string>
<string name="stats_page">Сторінка статистики</string>
<string name="toggle_player_statistics_page">Сторінка статистики</string>
<string name="player_controls_skip_intro_text">+%1$d с</string>
<string name="label_anime_history">Аніме</string>
<string name="disable_auto_play">Автовідтворення вимкнено</string>
@ -163,7 +163,7 @@
<string name="pref_category_player_aniskip_info">Для роботи AniSkip потрібно, щоб аніме відстежувалося за допомогою MAL або Anilist</string>
<string name="pref_waiting_time_aniskip_7">7 секунд</string>
<string name="pref_waiting_time_aniskip_8">8 секунд</string>
<string name="player_hwdec_dialog_title">Встановити режим апаратного декодування за замовчуванням</string>
<string name="player_hwdec_mode">Встановити режим апаратного декодування за замовчуванням</string>
<string name="notification_episodes_single">Епізод %1$s</string>
<string name="episode_settings">Налаштування епізоду</string>
<string name="rotation_sensor_landscape">Горизонтальне зображення</string>
@ -173,7 +173,6 @@
<string name="unknown_studio">Невідома студія</string>
<string name="download_unseen">Не переглянуто</string>
<string name="watching">Переглядаю</string>
<string name="toggle_stats">Перемикання статистики</string>
<string name="notification_new_episodes">Знайдено нові епізоди</string>
<string name="information_no_recent_anime">Останнім часом нічого не дивилися</string>
<string name="episode_settings_updated">Оновлено налаштування епізодів за умовчанням</string>
@ -265,9 +264,9 @@
<string name="currently_watching">Зараз переглядається</string>
<string name="plan_to_watch">Заплановано</string>
<string name="screenshot_show_subs">Показати субтитри на знімку екрана</string>
<string name="stats_page_1">Сторінка 1</string>
<string name="stats_page_2">Сторінка 2</string>
<string name="stats_page_3">Сторінка 3</string>
<string name="player_statistics_page_1">Сторінка 1</string>
<string name="player_statistics_page_2">Сторінка 2</string>
<string name="player_statistics_page_3">Сторінка 3</string>
<string name="recent_anime_time">Еп. %1$s - %2$s</string>
<string name="video_list_empty_error">Відео не знайдено</string>
<string name="label_history">Манґа</string>
@ -291,7 +290,6 @@
<string name="choose_video_quality">Виберіть якість відео:</string>
<string name="extension_settings">Налаштування розширення</string>
<string name="action_save_screenshot">Зберегти знімок екрана</string>
<string name="stats_header">Показати статистику</string>
<string name="chapter_dialog_header">Перейти до розділу</string>
<string name="data_saver_downloader">Використовувати зберігач даних у завантажувачі</string>
<string name="data_saver_exclude">Виключити зі зберігача даних</string>

View file

@ -159,11 +159,10 @@
<string name="screenshot_show_subs">在截屏中显示字幕</string>
<string name="enable_volume_brightness_gestures">启用音量和亮度手势</string>
<string name="enable_horizontal_seek_gesture">启用水平定位手势</string>
<string name="toggle_stats">启用数据统计</string>
<string name="stats_page">数据统计页</string>
<string name="stats_page_1">第一页</string>
<string name="stats_page_2">第二页</string>
<string name="stats_page_3">第三页</string>
<string name="toggle_player_statistics_page">数据统计页</string>
<string name="player_statistics_page_1">第一页</string>
<string name="player_statistics_page_2">第二页</string>
<string name="player_statistics_page_3">第三页</string>
<string name="download_insufficient_space">存储空间不足,无法下载章节</string>
<string name="download_queue_size_warning">警告:批量下载可能导致图源变慢,甚至会使得它们屏蔽 Tachiyomi。点击了解详情。</string>
<string name="video_list_empty_error">未找到视频</string>

View file

@ -128,6 +128,7 @@
<string name="pref_remember_volume">Remember and switch to the last used volume</string>
<!-- Needs better English -->
<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_category_external_player">External player</string>
<string name="pref_always_use_external_player">Always use external player</string>
@ -209,15 +210,13 @@
<string name="episode_progress">Progress: %1$s/%2$s</string>
<string name="episode_progress_no_total">Progress: %1$s</string>
<string name="screenshot_header">Take screenshot</string>
<string name="screenshot_show_subs">Show subtitles in 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="stats_header">Show statistics</string>
<string name="toggle_stats">Toggle stats</string>
<string name="stats_page">Stats page</string>
<string name="stats_page_1">Page 1</string>
<string name="stats_page_2">Page 2</string>
<string name="stats_page_3">Page 3</string>
<string name="toggle_player_statistics_page">Toggle statistics page</string>
<string name="player_statistics_page_1">Page 1</string>
<string name="player_statistics_page_2">Page 2</string>
<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_debanding_title">Debanding</string>
@ -261,9 +260,11 @@
<string name="video_fit_screen">"Fit to screen"</string>
<string name="video_crop_screen">"Cropped to screen"</string>
<string name="video_stretch_screen">"Stretched to screen"</string>
<string name="video_custom_screen">"Custom aspect ratio"</string>
<!-- Aniyomi stuff -->
<string name="playback_speed_dialog_title">Change playback speed:</string>
<string name="playback_speed_dialog_reset">Reset</string>
<string name="settings_dialog_header">Player settings</string>
<string name="quality_dialog_header">Video quality</string>
<string name="chapter_dialog_header">Seek to chapter</string>
<string name="subtitle_dialog_header">Subtitle</string>
@ -290,7 +291,7 @@
<string name="player_aniskip_dontskip">Don\'t skip</string>
<string name="player_aniskip_dontskip_toast">Skip in %d seconds</string>
<string name="player_aniskip_skip">%s skipped</string>
<string name="player_hwdec_dialog_title">Set default hardware decoding mode</string>
<string name="player_hwdec_mode">Hardware decoding mode</string>
<string name="notification_episodes_single">Episode %1$s</string>
<string name="notification_episodes_single_and_more">Episode %1$s and %2$d more</string>
<string name="notification_episodes_multiple">Episodes %1$s</string>
@ -321,6 +322,26 @@
<string name="pref_category_hide_hidden">Hide hidden categories from categories screen</string>
<!-- Subtitle settings -->
<string name="player_subtitle_settings_example">Lorem ipsum dolor sit amet.</string>
<string name="player_subtitle_settings_delay_tab">Delay</string>
<string name="player_subtitle_settings_font_tab">Font</string>
<string name="player_subtitle_settings_color_tab">Color</string>
<string name="player_subtitle_settings">Subtitle settings</string>
<string name="player_subtitle_empty_warning">Has no effect because there aren\'t any subtitle tracks in this video</string>
<string name="player_override_ass_subtitles">Override ASS subtitles</string>
<string name="player_reset_subtitles">Reset subtitles to default</string>
<string name="player_subtitle_delay">Subtitle delay</string>
<string name="player_subtitle_remember_delay">Remember subtitle delay</string>
<string name="player_audio_delay">Audio delay</string>
<string name="player_audio_remember_delay">Remember audio delay</string>
<string name="player_track_delay_text_field">Delay(s)</string>
<string name="player_font_size_text_field">Font size</string>
<string name="player_subtitle_text_color">Text</string>
<string name="player_subtitle_border_color">Border</string>
<string name="player_subtitle_background_color">Background</string>
<!-- TachiyomiSY -->
<string name="data_saver_exclude">Exclude from data saver</string>
<string name="data_saver_stop_exclude">Stop excluding from data saver</string>

View file

@ -1,5 +1,6 @@
package tachiyomi.presentation.core.components
import android.content.res.Configuration.ORIENTATION_LANDSCAPE
import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
@ -38,6 +39,7 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.Velocity
@ -60,6 +62,12 @@ fun AdaptiveSheet(
content: @Composable () -> Unit,
) {
val scope = rememberCoroutineScope()
val maxWidth = if (LocalConfiguration.current.orientation == ORIENTATION_LANDSCAPE) {
600.dp
} else {
460.dp
}
if (isTabletUi) {
var targetAlpha by remember { mutableStateOf(0f) }
val alpha by animateFloatAsState(
@ -86,7 +94,7 @@ fun AdaptiveSheet(
) {
Surface(
modifier = Modifier
.requiredWidthIn(max = 460.dp)
.requiredWidthIn(max = maxWidth)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
@ -126,7 +134,7 @@ fun AdaptiveSheet(
val anchors = mapOf(0f to 0, fullHeight to 1)
Surface(
modifier = Modifier
.widthIn(max = 460.dp)
.widthIn(max = maxWidth)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,