diff --git a/app/src/main/java/eu/kanade/presentation/components/AdaptiveSheet.kt b/app/src/main/java/eu/kanade/presentation/components/AdaptiveSheet.kt index cacb0fe20..56f4756a2 100644 --- a/app/src/main/java/eu/kanade/presentation/components/AdaptiveSheet.kt +++ b/app/src/main/java/eu/kanade/presentation/components/AdaptiveSheet.kt @@ -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, diff --git a/app/src/main/java/eu/kanade/presentation/components/SettingsItems.kt b/app/src/main/java/eu/kanade/presentation/components/SettingsItems.kt index c411ba58b..e43bbebae 100644 --- a/app/src/main/java/eu/kanade/presentation/components/SettingsItems.kt +++ b/app/src/main/java/eu/kanade/presentation/components/SettingsItems.kt @@ -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) } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt b/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt index e13638ac2..cdb7567f1 100644 --- a/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt @@ -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, 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), ) } - DropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false }, - ) { - content { expanded = false } + if (onClickIcon == null) { + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + ) { + content!! { expanded = false } + } } } } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/AdvancedPlayerSettingsScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/AdvancedPlayerSettingsScreen.kt index 2ddce0753..8bc5d2250 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/AdvancedPlayerSettingsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/AdvancedPlayerSettingsScreen.kt @@ -18,10 +18,10 @@ object AdvancedPlayerSettingsScreen : SearchableSettings { @Composable override fun getPreferences(): List { val playerPreferences = remember { Injekt.get() } + val scope = rememberCoroutineScope() val context = LocalContext.current val mpvConf = playerPreferences.mpvConf() val mpvInput = playerPreferences.mpvInput() - val scope = rememberCoroutineScope() return listOf( Preference.PreferenceItem.MultiLineEditTextPreference( diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt index cb7412395..8ce52c925 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerActivity.kt @@ -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 = emptyArray() + private var selectedQualityIndex = 0 - private var selectedSub = 0 + private var subtitleTracks: Array = emptyArray() + + private var selectedSubtitleIndex = 0 private var hadPreviousSubs = false private var audioTracks: Array = emptyArray() - private var selectedAudio = 0 + private var selectedAudioIndex = 0 private var hadPreviousAudio = false - private var videoChapters: List = emptyList() + private var videoChapters: List = 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()) + } + + playerControls.setViewMode(showText = false) } - private fun pauseForDialog(): () -> Unit { + 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 { + playerControls.binding.settingsBtn.updateLayoutParams { 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 { + playerControls.binding.settingsBtn.updateLayoutParams { 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