mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-24 13:48:55 +03:00
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:
parent
2431d8a858
commit
f0783f534d
61 changed files with 1894 additions and 1070 deletions
|
@ -10,10 +10,12 @@ 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.annotation.InternalVoyagerApi
|
||||
import cafe.adriel.voyager.core.lifecycle.DisposableEffectIgnoringConfiguration
|
||||
import cafe.adriel.voyager.core.screen.Screen
|
||||
import cafe.adriel.voyager.navigator.Navigator
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import eu.kanade.presentation.util.ScreenTransition
|
||||
import eu.kanade.presentation.util.isTabletUi
|
||||
import tachiyomi.presentation.core.components.AdaptiveSheet as AdaptiveSheetImpl
|
||||
|
@ -70,6 +72,7 @@ fun NavigatorAdaptiveSheet(
|
|||
*/
|
||||
@Composable
|
||||
fun AdaptiveSheet(
|
||||
hideSystemBars: Boolean = false,
|
||||
tonalElevation: Dp = 1.dp,
|
||||
enableSwipeDismiss: Boolean = true,
|
||||
onDismissRequest: () -> Unit,
|
||||
|
@ -81,6 +84,12 @@ fun AdaptiveSheet(
|
|||
onDismissRequest = onDismissRequest,
|
||||
properties = dialogProperties,
|
||||
) {
|
||||
if (hideSystemBars) {
|
||||
rememberSystemUiController().apply {
|
||||
isSystemBarsVisible = false
|
||||
systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
}
|
||||
}
|
||||
AdaptiveSheetImpl(
|
||||
isTabletUi = isTabletUi,
|
||||
tonalElevation = tonalElevation,
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
@ -42,9 +43,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 (Int) -> Unit,
|
||||
) {
|
||||
AdaptiveSheet(
|
||||
hideSystemBars = hideSystemBars,
|
||||
onDismissRequest = onDismissRequest,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
@ -77,7 +82,7 @@ fun TabbedDialog(
|
|||
}
|
||||
}
|
||||
|
||||
tabOverflowMenuContent?.let { MoreMenu(it) }
|
||||
MoreMenu(onOverflowMenuClicked, tabOverflowMenuContent, overflowIcon)
|
||||
}
|
||||
Divider()
|
||||
|
||||
|
@ -94,21 +99,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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
*/
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
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,
|
||||
) { page ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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"),
|
||||
;
|
||||
}
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
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,
|
||||
) { page ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -114,7 +114,7 @@ class GestureHandler(
|
|||
|
||||
override fun onLongPress(e: MotionEvent) {
|
||||
if (SeekState.mode == SeekState.LOCKED) { playerControls.toggleControls(); return }
|
||||
activity.openScreenshotSheet()
|
||||
activity.viewModel.showScreenshotOptions()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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),
|
||||
;
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
10
app/src/main/res/drawable/ic_video_chapter_20dp.xml
Normal file
10
app/src/main/res/drawable/ic_video_chapter_20dp.xml
Normal 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>
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -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 -->
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 d’un 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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
@ -323,6 +324,25 @@
|
|||
|
||||
<string name="pref_update_anime_release_grace_period">Expected anime release grace period</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>
|
||||
|
|
|
@ -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 { mutableFloatStateOf(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,
|
||||
|
|
Loading…
Reference in a new issue