From 3efe7bba74a6e556c4d9b34d3338800fa091ef3d Mon Sep 17 00:00:00 2001 From: SkyD666 Date: Fri, 24 May 2024 23:36:04 +0800 Subject: [PATCH] [feature|optimize|chore] Support select audio tracks, optimize player UI code; optimize Feed screen tablet layout; reduce useless permissions --- app/build.gradle.kts | 2 +- app/src/main/AndroidManifest.xml | 10 + .../adapter/proxy/Feed1Proxy.kt | 11 +- .../anivu/ui/component/shape/SemiCircle.kt | 2 +- .../ui/fragment/article/ArticleFragment.kt | 2 +- .../skyd/anivu/ui/fragment/feed/FeedScreen.kt | 40 +- .../java/com/skyd/anivu/ui/mpv/MPVView.kt | 3 + .../com/skyd/anivu/ui/mpv/PlayerCommand.kt | 28 + .../java/com/skyd/anivu/ui/mpv/PlayerView.kt | 823 ++---------------- .../ui/mpv/{ => controller}/ForwardRipple.kt | 2 +- .../ui/mpv/controller/PlayerController.kt | 348 ++++++++ .../{ => controller}/PointerInputDetector.kt | 10 +- .../ui/mpv/controller/bar/BarIconButton.kt | 30 + .../anivu/ui/mpv/controller/bar/BottomBar.kt | 201 +++++ .../anivu/ui/mpv/controller/bar/TopBar.kt | 78 ++ .../ui/mpv/controller/button/Forward85s.kt | 37 + .../mpv/controller/button/ResetTransform.kt | 38 + .../ui/mpv/controller/button/Screenshot.kt | 35 + .../mpv/controller/dialog/AudioTrackDialog.kt | 73 ++ .../dialog/SubtitleTrackDialog.kt} | 55 +- .../controller/dialog/TrackDialogListItem.kt | 57 ++ .../controller/preview/BrightnessPreview.kt | 68 ++ .../preview/LongPressSpeedPreview.kt | 53 ++ .../mpv/controller/preview/SeekTimePreview.kt | 55 ++ .../mpv/controller/preview/VolumePreview.kt | 70 ++ .../mpv/{ => controller}/state/PlayState.kt | 2 +- .../{ => controller}/state/TransformState.kt | 2 +- .../state/track/AudioTrackDialogState.kt | 23 + .../state/track}/SubtitleTrackDialogState.kt | 2 +- .../state/track/TrackDialogState.kt | 16 + app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 32 files changed, 1339 insertions(+), 839 deletions(-) create mode 100644 app/src/main/java/com/skyd/anivu/ui/mpv/PlayerCommand.kt rename app/src/main/java/com/skyd/anivu/ui/mpv/{ => controller}/ForwardRipple.kt (99%) create mode 100644 app/src/main/java/com/skyd/anivu/ui/mpv/controller/PlayerController.kt rename app/src/main/java/com/skyd/anivu/ui/mpv/{ => controller}/PointerInputDetector.kt (97%) create mode 100644 app/src/main/java/com/skyd/anivu/ui/mpv/controller/bar/BarIconButton.kt create mode 100644 app/src/main/java/com/skyd/anivu/ui/mpv/controller/bar/BottomBar.kt create mode 100644 app/src/main/java/com/skyd/anivu/ui/mpv/controller/bar/TopBar.kt create mode 100644 app/src/main/java/com/skyd/anivu/ui/mpv/controller/button/Forward85s.kt create mode 100644 app/src/main/java/com/skyd/anivu/ui/mpv/controller/button/ResetTransform.kt create mode 100644 app/src/main/java/com/skyd/anivu/ui/mpv/controller/button/Screenshot.kt create mode 100644 app/src/main/java/com/skyd/anivu/ui/mpv/controller/dialog/AudioTrackDialog.kt rename app/src/main/java/com/skyd/anivu/ui/mpv/{SubtitleTrack.kt => controller/dialog/SubtitleTrackDialog.kt} (69%) create mode 100644 app/src/main/java/com/skyd/anivu/ui/mpv/controller/dialog/TrackDialogListItem.kt create mode 100644 app/src/main/java/com/skyd/anivu/ui/mpv/controller/preview/BrightnessPreview.kt create mode 100644 app/src/main/java/com/skyd/anivu/ui/mpv/controller/preview/LongPressSpeedPreview.kt create mode 100644 app/src/main/java/com/skyd/anivu/ui/mpv/controller/preview/SeekTimePreview.kt create mode 100644 app/src/main/java/com/skyd/anivu/ui/mpv/controller/preview/VolumePreview.kt rename app/src/main/java/com/skyd/anivu/ui/mpv/{ => controller}/state/PlayState.kt (93%) rename app/src/main/java/com/skyd/anivu/ui/mpv/{ => controller}/state/TransformState.kt (92%) create mode 100644 app/src/main/java/com/skyd/anivu/ui/mpv/controller/state/track/AudioTrackDialogState.kt rename app/src/main/java/com/skyd/anivu/ui/mpv/{state => controller/state/track}/SubtitleTrackDialogState.kt (91%) create mode 100644 app/src/main/java/com/skyd/anivu/ui/mpv/controller/state/track/TrackDialogState.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 07dd9639..dc190583 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -21,7 +21,7 @@ android { minSdk = 24 targetSdk = 34 versionCode = 16 - versionName = "1.1-beta32" + versionName = "1.1-beta33" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 244eb152..8ecc27b1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -10,6 +10,16 @@ + + + Boolean = { true }, + private val selected: (FeedBean) -> Boolean = { false }, private val isEnded: (index: Int) -> Boolean = { false }, private val useCardLayout: () -> Boolean = { false }, private val onClick: ((FeedBean) -> Unit)? = null, @@ -58,6 +59,7 @@ class Feed1Proxy( index = index, data = data, visible = visible, + selected = selected, isEnded = isEnded, useCardLayout = useCardLayout, onClick = onClick, @@ -72,6 +74,7 @@ fun Feed1Item( index: Int, data: FeedViewBean, visible: (groupId: String) -> Boolean, + selected: (FeedBean) -> Boolean, useCardLayout: () -> Boolean, onClick: ((FeedBean) -> Unit)? = null, isEnded: (index: Int) -> Boolean, @@ -97,8 +100,12 @@ fun Feed1Item( } else RectangleShape ) .run { - if (useCardLayout()) background(color = MaterialTheme.colorScheme.surfaceContainer) - else this + if (useCardLayout()) { + background( + if (selected(feed)) MaterialTheme.colorScheme.surfaceContainerHighest + else MaterialTheme.colorScheme.surfaceContainer + ) + } else this } .combinedClickable( onLongClick = if (onRemove != null && onEdit != null) { diff --git a/app/src/main/java/com/skyd/anivu/ui/component/shape/SemiCircle.kt b/app/src/main/java/com/skyd/anivu/ui/component/shape/SemiCircle.kt index 160c9f12..24a49d03 100644 --- a/app/src/main/java/com/skyd/anivu/ui/component/shape/SemiCircle.kt +++ b/app/src/main/java/com/skyd/anivu/ui/component/shape/SemiCircle.kt @@ -7,7 +7,7 @@ import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.Shape import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.LayoutDirection -import com.skyd.anivu.ui.mpv.ForwardRippleDirect +import com.skyd.anivu.ui.mpv.controller.ForwardRippleDirect class ForwardRippleShape(private val direct: ForwardRippleDirect) : Shape { override fun createOutline( diff --git a/app/src/main/java/com/skyd/anivu/ui/fragment/article/ArticleFragment.kt b/app/src/main/java/com/skyd/anivu/ui/fragment/article/ArticleFragment.kt index a1e43fef..ec25797e 100644 --- a/app/src/main/java/com/skyd/anivu/ui/fragment/article/ArticleFragment.kt +++ b/app/src/main/java/com/skyd/anivu/ui/fragment/article/ArticleFragment.kt @@ -184,7 +184,7 @@ private fun ArticleList( } AniVuLazyVerticalGrid( modifier = modifier.fillMaxSize(), - columns = GridCells.Adaptive(300.dp), + columns = GridCells.Adaptive(360.dp), dataList = articles, adapter = adapter, contentPadding = contentPadding, diff --git a/app/src/main/java/com/skyd/anivu/ui/fragment/feed/FeedScreen.kt b/app/src/main/java/com/skyd/anivu/ui/fragment/feed/FeedScreen.kt index 6ebb922d..98a67ff3 100644 --- a/app/src/main/java/com/skyd/anivu/ui/fragment/feed/FeedScreen.kt +++ b/app/src/main/java/com/skyd/anivu/ui/fragment/feed/FeedScreen.kt @@ -39,6 +39,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo +import androidx.compose.material3.adaptive.currentWindowSize import androidx.compose.material3.adaptive.layout.AnimatedPane import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole @@ -61,6 +62,7 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource @@ -116,11 +118,22 @@ fun FeedScreen() { ) val navController = LocalNavController.current val windowSizeClass = LocalWindowSizeClass.current + val density = LocalDensity.current - BackHandler(navigator.canNavigateBack()) { + var listPaneSelectedFeedUrls by remember { mutableStateOf?>(null) } + + val onNavigatorBack: () -> Unit = { navigator.navigateBack() + listPaneSelectedFeedUrls = navigator.currentDestination?.content + } + + BackHandler(navigator.canNavigateBack()) { + onNavigatorBack() } + val windowWidth = with(density) { currentWindowSize().width.toDp() } + val feedListWidth by remember(windowWidth) { mutableStateOf(windowWidth * 0.36f) } + ListDetailPaneScaffold( modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing.only( WindowInsetsSides.Right.run { @@ -130,17 +143,21 @@ fun FeedScreen() { directive = navigator.scaffoldDirective, value = navigator.scaffoldValue, listPane = { - AnimatedPane { + AnimatedPane(modifier = Modifier.preferredWidth(feedListWidth)) { FeedList( + listPaneSelectedFeedUrls = listPaneSelectedFeedUrls, onShowArticleList = { feedUrls -> if (navigator.scaffoldDirective.maxHorizontalPartitions > 1) { navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, feedUrls) } else { - navController.navigate(R.id.action_to_article_fragment, Bundle().apply { - putStringArrayList( - ArticleFragment.FEED_URLS_KEY, ArrayList(feedUrls), - ) - }) + navController.navigate( + R.id.action_to_article_fragment, + Bundle().apply { + putStringArrayList( + ArticleFragment.FEED_URLS_KEY, ArrayList(feedUrls), + ) + } + ) } }, ) @@ -149,7 +166,8 @@ fun FeedScreen() { detailPane = { AnimatedPane { navigator.currentDestination?.content?.let { - ArticleScreen(feedUrls = it, onBackClick = { navigator.navigateBack() }) + listPaneSelectedFeedUrls = it + ArticleScreen(feedUrls = it, onBackClick = onNavigatorBack) } } }, @@ -158,6 +176,7 @@ fun FeedScreen() { @Composable private fun FeedList( + listPaneSelectedFeedUrls: List? = null, onShowArticleList: (List) -> Unit, viewModel: FeedViewModel = hiltViewModel(), ) { @@ -250,6 +269,7 @@ private fun FeedList( FeedList( result = groupListState.dataList, contentPadding = innerPadding + PaddingValues(bottom = fabHeight + 16.dp), + selectedFeedUrls = listPaneSelectedFeedUrls, onRemoveFeed = { feed -> dispatch(FeedIntent.RemoveFeed(feed.url)) }, onShowArticleList = { feedUrls -> onShowArticleList(feedUrls) }, onEditFeed = { feed -> @@ -556,6 +576,7 @@ private fun CreateGroupDialog( private fun FeedList( result: List, contentPadding: PaddingValues = PaddingValues(), + selectedFeedUrls: List? = null, onShowArticleList: (List) -> Unit, onRemoveFeed: (FeedBean) -> Unit, onEditFeed: (FeedBean) -> Unit, @@ -593,7 +614,7 @@ private fun FeedList( val shouldHideEmptyDefault: (index: Int) -> Boolean = remember(hideEmptyDefault, result) { { hideEmptyDefault && result.getOrNull(it + 1) !is FeedViewBean } } - val adapter = remember(shouldHideEmptyDefault) { + val adapter = remember(shouldHideEmptyDefault, selectedFeedUrls) { val group1Proxy = Group1Proxy( isExpand = { feedVisible[it.groupId] ?: false }, onExpandChange = { data, expand -> feedVisible[data.groupId] = expand }, @@ -620,6 +641,7 @@ private fun FeedList( group1Proxy, Feed1Proxy( visible = { feedVisible[it] ?: false }, + selected = { selectedFeedUrls != null && it.url in selectedFeedUrls }, useCardLayout = { true }, onClick = { onShowArticleList(listOf(it.url)) }, isEnded = { it == result.lastIndex || result[it + 1] is GroupBean }, diff --git a/app/src/main/java/com/skyd/anivu/ui/mpv/MPVView.kt b/app/src/main/java/com/skyd/anivu/ui/mpv/MPVView.kt index fc4501ac..92d26ad3 100644 --- a/app/src/main/java/com/skyd/anivu/ui/mpv/MPVView.kt +++ b/app/src/main/java/com/skyd/anivu/ui/mpv/MPVView.kt @@ -11,6 +11,7 @@ import com.skyd.anivu.config.Const import com.skyd.anivu.ext.dataStore import com.skyd.anivu.ext.getOrDefault import com.skyd.anivu.model.preference.player.HardwareDecodePreference +import com.skyd.anivu.ui.mpv.controller.bar.toDurationString import `is`.xyz.mpv.MPVLib import `is`.xyz.mpv.MPVLib.mpvFormat.MPV_FORMAT_DOUBLE import `is`.xyz.mpv.MPVLib.mpvFormat.MPV_FORMAT_FLAG @@ -165,6 +166,8 @@ class MPVView(context: Context, attrs: AttributeSet?) : SurfaceView(context, att val subtitleTrack: List get() = tracks["sub"].orEmpty().toList() + val audioTrack: List + get() = tracks["audio"].orEmpty().toList() private fun getTrackDisplayName(mpvId: Int, lang: String?, title: String?): String { return if (!lang.isNullOrEmpty() && !title.isNullOrEmpty()) { diff --git a/app/src/main/java/com/skyd/anivu/ui/mpv/PlayerCommand.kt b/app/src/main/java/com/skyd/anivu/ui/mpv/PlayerCommand.kt new file mode 100644 index 00000000..cfb38d99 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/mpv/PlayerCommand.kt @@ -0,0 +1,28 @@ +package com.skyd.anivu.ui.mpv + +import android.net.Uri +import androidx.compose.ui.geometry.Offset + +sealed interface PlayerCommand { + data class SetUri(val uri: Uri) : PlayerCommand + data object Destroy : PlayerCommand + data class Paused(val paused: Boolean) : PlayerCommand + data object GetPaused : PlayerCommand + data object PlayOrPause : PlayerCommand + data class SeekTo(val position: Int) : PlayerCommand + data class Rotate(val rotate: Int) : PlayerCommand + data class Zoom(val zoom: Float) : PlayerCommand + data object GetZoom : PlayerCommand + data class VideoOffset(val offset: Offset) : PlayerCommand + data object GetVideoOffsetX : PlayerCommand + data object GetVideoOffsetY : PlayerCommand + data class SetSpeed(val speed: Float) : PlayerCommand + data object GetSpeed : PlayerCommand + data object LoadAllTracks : PlayerCommand + data object GetSubtitleTrack : PlayerCommand + data class SetSubtitleTrack(val trackId: Int) : PlayerCommand + data object GetAudioTrack : PlayerCommand + data class SetAudioTrack(val trackId: Int) : PlayerCommand + data object Screenshot : PlayerCommand + data class AddSubtitle(val filePath: String) : PlayerCommand +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/mpv/PlayerView.kt b/app/src/main/java/com/skyd/anivu/ui/mpv/PlayerView.kt index 455f8ef5..c0810c5f 100644 --- a/app/src/main/java/com/skyd/anivu/ui/mpv/PlayerView.kt +++ b/app/src/main/java/com/skyd/anivu/ui/mpv/PlayerView.kt @@ -1,138 +1,47 @@ package com.skyd.anivu.ui.mpv import android.net.Uri -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.animateContentSize -import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.spring -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.background -import androidx.compose.foundation.basicMarquee -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.displayCutout -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.rounded.VolumeDown -import androidx.compose.material.icons.automirrored.rounded.VolumeMute -import androidx.compose.material.icons.automirrored.rounded.VolumeUp -import androidx.compose.material.icons.outlined.ArrowBackIosNew -import androidx.compose.material.icons.rounded.BrightnessHigh -import androidx.compose.material.icons.rounded.BrightnessLow -import androidx.compose.material.icons.rounded.BrightnessMedium -import androidx.compose.material.icons.rounded.CameraAlt -import androidx.compose.material.icons.rounded.ClosedCaption -import androidx.compose.material.icons.rounded.FastForward -import androidx.compose.material.icons.rounded.FastRewind -import androidx.compose.material.icons.rounded.Pause -import androidx.compose.material.icons.rounded.PlayArrow -import androidx.compose.material3.Icon -import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Slider -import androidx.compose.material3.SliderDefaults -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.BiasAlignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.LayoutCoordinates -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.TextUnit -import androidx.compose.ui.unit.TextUnitType -import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView -import androidx.constraintlayout.compose.ConstraintLayout import androidx.core.view.WindowInsetsControllerCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner -import com.materialkolor.ktx.toColor -import com.materialkolor.ktx.toHct -import com.skyd.anivu.R import com.skyd.anivu.config.Const -import com.skyd.anivu.ext.alwaysLight import com.skyd.anivu.ext.startWith -import com.skyd.anivu.ext.toPercentage import com.skyd.anivu.ui.component.rememberSystemUiController -import com.skyd.anivu.ui.local.LocalPlayerShow85sButton -import com.skyd.anivu.ui.local.LocalPlayerShowScreenshotButton -import com.skyd.anivu.ui.mpv.state.PlayState -import com.skyd.anivu.ui.mpv.state.PlayStateCallback -import com.skyd.anivu.ui.mpv.state.SubtitleTrackDialogCallback -import com.skyd.anivu.ui.mpv.state.SubtitleTrackDialogState -import com.skyd.anivu.ui.mpv.state.TransformState -import com.skyd.anivu.ui.mpv.state.TransformStateCallback +import com.skyd.anivu.ui.mpv.controller.PlayerController +import com.skyd.anivu.ui.mpv.controller.bar.BottomBarCallback +import com.skyd.anivu.ui.mpv.controller.state.PlayState +import com.skyd.anivu.ui.mpv.controller.state.PlayStateCallback +import com.skyd.anivu.ui.mpv.controller.state.TransformState +import com.skyd.anivu.ui.mpv.controller.state.TransformStateCallback +import com.skyd.anivu.ui.mpv.controller.state.track.AudioTrackDialogCallback +import com.skyd.anivu.ui.mpv.controller.state.track.AudioTrackDialogState +import com.skyd.anivu.ui.mpv.controller.state.track.SubtitleTrackDialogCallback +import com.skyd.anivu.ui.mpv.controller.state.track.SubtitleTrackDialogState +import com.skyd.anivu.ui.mpv.controller.state.track.TrackDialogState import `is`.xyz.mpv.MPVLib import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import java.io.File -import kotlin.math.abs import kotlin.math.pow -sealed interface PlayerCommand { - data class SetUri(val uri: Uri) : PlayerCommand - data object Destroy : PlayerCommand - data class Paused(val paused: Boolean) : PlayerCommand - data object GetPaused : PlayerCommand - data object PlayOrPause : PlayerCommand - data class SeekTo(val position: Int) : PlayerCommand - data class Rotate(val rotate: Int) : PlayerCommand - data class Zoom(val zoom: Float) : PlayerCommand - data object GetZoom : PlayerCommand - data class VideoOffset(val offset: Offset) : PlayerCommand - data object GetVideoOffsetX : PlayerCommand - data object GetVideoOffsetY : PlayerCommand - data class SetSpeed(val speed: Float) : PlayerCommand - data class SetSubtitleTrack(val trackId: Int) : PlayerCommand - data object GetSpeed : PlayerCommand - data object LoadAllTracks : PlayerCommand - data object GetSubtitleTrack : PlayerCommand - data object Screenshot : PlayerCommand - data class AddSubtitle(val filePath: String) : PlayerCommand -} private fun MPVView.solveCommand( command: PlayerCommand, @@ -140,6 +49,8 @@ private fun MPVView.solveCommand( isPlayingChanged: (Boolean) -> Unit, onSubtitleTrack: (subtitleTrack: List) -> Unit, onSubtitleTrackChanged: (Int) -> Unit, + onAudioTrack: (subtitleTrack: List) -> Unit, + onAudioTrackChanged: (Int) -> Unit, transformState: () -> TransformState, onVideoZoom: (Float) -> Unit, onVideoOffset: (Offset) -> Unit, @@ -179,11 +90,17 @@ private fun MPVView.solveCommand( PlayerCommand.GetSpeed -> onSpeedChanged(playbackSpeed.toFloat()) PlayerCommand.LoadAllTracks -> loadTracks() PlayerCommand.GetSubtitleTrack -> onSubtitleTrack(subtitleTrack) + PlayerCommand.GetAudioTrack -> onAudioTrack(audioTrack) is PlayerCommand.SetSubtitleTrack -> { sid = command.trackId onSubtitleTrackChanged(command.trackId) } + is PlayerCommand.SetAudioTrack -> { + aid = command.trackId + onAudioTrackChanged(command.trackId) + } + PlayerCommand.Screenshot -> screenshot(onSaveScreenshot = onSaveScreenshot) is PlayerCommand.AddSubtitle -> { addSubtitle(command.filePath) @@ -211,6 +128,15 @@ fun PlayerView( var mediaLoaded by rememberSaveable { mutableStateOf(false) } var subtitleTrackDialogState by remember { mutableStateOf(SubtitleTrackDialogState.initial) } + var audioTrackDialogState by remember { mutableStateOf(AudioTrackDialogState.initial) } + val trackDialogState by remember { + mutableStateOf( + TrackDialogState( + audioTrackDialogState = { audioTrackDialogState }, + subtitleTrackDialogState = { subtitleTrackDialogState }, + ) + ) + } var playState by remember { mutableStateOf(PlayState.initial) } var transformState by remember { mutableStateOf(TransformState.initial) } @@ -238,6 +164,17 @@ fun PlayerView( onSubtitleTrackChanged = { commandQueue.trySend(PlayerCommand.SetSubtitleTrack(it.trackId)) }, ) } + val audioTrackDialogCallback = remember { + AudioTrackDialogCallback( + onAudioTrackChanged = { commandQueue.trySend(PlayerCommand.SetAudioTrack(it.trackId)) }, + ) + } + val bottomBarCallback = remember { + BottomBarCallback( + onAudioTrackClick = { commandQueue.trySend(PlayerCommand.GetAudioTrack) }, + onSubtitleTrackClick = { commandQueue.trySend(PlayerCommand.GetSubtitleTrack) }, + ) + } val mpvObserver = remember { object : MPVLib.EventObserver { @@ -328,6 +265,18 @@ fun PlayerView( currentSubtitleTrack = subtitleTrack.find { it.trackId == newTrackId }!!, ) }, + onAudioTrack = { + audioTrackDialogState = audioTrackDialogState.copy( + show = true, + currentAudioTrack = audioTrack.find { it.trackId == aid }!!, + audioTrack = audioTrack, + ) + }, + onAudioTrackChanged = { newTrackId -> + audioTrackDialogState = audioTrackDialogState.copy( + currentAudioTrack = audioTrack.find { it.trackId == newTrackId }!!, + ) + }, transformState = { transformState }, onVideoZoom = { transformState = transformState.copy(videoZoom = it) @@ -341,7 +290,6 @@ fun PlayerView( } .collect() } - } }, ) @@ -351,12 +299,16 @@ fun PlayerView( onBack = onBack, playState = { playState }, playStateCallback = playStateCallback, - subtitleTrackDialogState = { subtitleTrackDialogState }, + bottomBarCallback = bottomBarCallback, + trackDialogState = trackDialogState, onDismissSubtitleTrackDialog = { subtitleTrackDialogState = subtitleTrackDialogState.copy(show = false) }, - onRequestSubtitleTrack = { commandQueue.trySend(PlayerCommand.GetSubtitleTrack) }, + onDismissAudioTrackDialog = { + audioTrackDialogState = audioTrackDialogState.copy(show = false) + }, subtitleTrackDialogCallback = subtitleTrackDialogCallback, + audioTrackDialogCallback = audioTrackDialogCallback, transformState = { transformState }, transformStateCallback = transformStateCallback, onScreenshot = { commandQueue.trySend(PlayerCommand.Screenshot) }, @@ -391,665 +343,4 @@ fun PlayerView( lifecycleOwner.lifecycle.addObserver(observer) onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } } -} - -private val ControllerBarGray = Color(0xAD000000) -internal val ControllerLabelGray = Color(0x70000000) - -@Composable -private fun PlayerController( - enabled: () -> Boolean, - onBack: () -> Unit, - playState: () -> PlayState, - playStateCallback: PlayStateCallback, - subtitleTrackDialogState: () -> SubtitleTrackDialogState, - onDismissSubtitleTrackDialog: () -> Unit, - onRequestSubtitleTrack: () -> Unit, - subtitleTrackDialogCallback: SubtitleTrackDialogCallback, - transformState: () -> TransformState, - transformStateCallback: TransformStateCallback, - onScreenshot: () -> Unit, -) { - var showController by rememberSaveable { mutableStateOf(true) } - var controllerWidth by remember { mutableIntStateOf(0) } - var controllerHeight by remember { mutableIntStateOf(0) } - var controllerLayoutCoordinates by remember { mutableStateOf(null) } - - val view = LocalView.current - val autoHideControllerRunnable = remember { Runnable { showController = false } } - val cancelAutoHideControllerRunnable = { view.removeCallbacks(autoHideControllerRunnable) } - val restartAutoHideControllerRunnable = { - cancelAutoHideControllerRunnable() - if (showController) { - view.postDelayed(autoHideControllerRunnable, 5000) - } - } - LaunchedEffect(showController) { restartAutoHideControllerRunnable() } - - var showSeekTimePreview by remember { mutableStateOf(false) } - var seekTimePreview by remember { mutableIntStateOf(0) } - - var showBrightnessPreview by remember { mutableStateOf(false) } - var brightnessValue by remember { mutableFloatStateOf(0f) } - var brightnessRange by remember { mutableStateOf(0f..0f) } - - var showVolumePreview by remember { mutableStateOf(false) } - var volumeValue by remember { mutableIntStateOf(0) } - var volumeRange by remember { mutableStateOf(0..0) } - - var showForwardRipple by remember { mutableStateOf(false) } - var forwardRippleStartControllerOffset by remember { mutableStateOf(Offset.Zero) } - var showBackwardRipple by remember { mutableStateOf(false) } - var backwardRippleStartControllerOffset by remember { mutableStateOf(Offset.Zero) } - - var isLongPressing by remember { mutableStateOf(false) } - - LaunchedEffect(subtitleTrackDialogState()) { - if (subtitleTrackDialogState().show) cancelAutoHideControllerRunnable() - else restartAutoHideControllerRunnable() - } - - CompositionLocalProvider(LocalContentColor provides Color.White) { - Box( - modifier = Modifier - .fillMaxSize() - .onGloballyPositioned { - controllerWidth = it.size.width - controllerHeight = it.size.height - controllerLayoutCoordinates = it - } - .detectPressGestures( - controllerWidth = { controllerWidth }, - playState = playState, - playStateCallback = playStateCallback, - showController = { showController }, - onShowControllerChanged = { showController = it }, - isLongPressing = { isLongPressing }, - isLongPressingChanged = { isLongPressing = it }, - onShowForwardRipple = { - forwardRippleStartControllerOffset = it - showForwardRipple = true - }, - onShowBackwardRipple = { - backwardRippleStartControllerOffset = it - showBackwardRipple = true - }, - cancelAutoHideControllerRunnable = cancelAutoHideControllerRunnable, - restartAutoHideControllerRunnable = restartAutoHideControllerRunnable, - ) - .detectControllerGestures( - enabled = enabled, - controllerWidth = { controllerWidth }, - controllerHeight = { controllerHeight }, - onShowBrightness = { showBrightnessPreview = it }, - onBrightnessRangeChanged = { brightnessRange = it }, - onBrightnessChanged = { brightnessValue = it }, - onShowVolume = { showVolumePreview = it }, - onVolumeRangeChanged = { volumeRange = it }, - onVolumeChanged = { volumeValue = it }, - playState = playState, - playStateCallback = playStateCallback, - onShowSeekTimePreview = { showSeekTimePreview = it }, - onTimePreviewChanged = { seekTimePreview = it }, - transformState = transformState, - transformStateCallback = transformStateCallback, - cancelAutoHideControllerRunnable = cancelAutoHideControllerRunnable, - restartAutoHideControllerRunnable = restartAutoHideControllerRunnable, - ) - ) { - // Forward ripple - AnimatedVisibility( - visible = showForwardRipple, - modifier = Modifier.align(Alignment.CenterEnd), - enter = fadeIn(animationSpec = spring(stiffness = Spring.StiffnessHigh)), - exit = fadeOut(), - ) { - ForwardRipple( - direct = ForwardRippleDirect.Forward, - text = "+10s", - icon = Icons.Rounded.FastForward, - controllerWidth = { controllerWidth }, - parentLayoutCoordinates = controllerLayoutCoordinates, - rippleStartControllerOffset = forwardRippleStartControllerOffset, - onHideRipple = { showForwardRipple = false }, - ) - } - // Backward ripple - AnimatedVisibility( - visible = showBackwardRipple, - modifier = Modifier.align(Alignment.CenterStart), - enter = fadeIn(animationSpec = spring(stiffness = Spring.StiffnessHigh)), - exit = fadeOut(), - ) { - ForwardRipple( - direct = ForwardRippleDirect.Backward, - text = "-10s", - icon = Icons.Rounded.FastRewind, - controllerWidth = { controllerWidth }, - parentLayoutCoordinates = controllerLayoutCoordinates, - rippleStartControllerOffset = backwardRippleStartControllerOffset, - onHideRipple = { showBackwardRipple = false }, - ) - } - // Auto hide box - AutoHiddenBox( - enabled = enabled, - show = { showController }, - onBack = onBack, - playState = playState, - playStateCallback = playStateCallback, - onSubtitleTrackClick = onRequestSubtitleTrack, - transformState = transformState, - transformStateCallback = transformStateCallback, - onScreenshot = onScreenshot, - onRestartAutoHideControllerRunnable = restartAutoHideControllerRunnable, - ) - - // Seek time preview - if (showSeekTimePreview) { - SeekTimePreview( - value = { seekTimePreview }, - duration = { playState().duration }, - ) - } - // Brightness preview - if (showBrightnessPreview) { - BrightnessPreview(value = { brightnessValue }, range = { brightnessRange }) - } - // Volume preview - if (showVolumePreview) { - VolumePreview(value = { volumeValue }, range = { volumeRange }) - } - // Long press speed preview - if (isLongPressing) { - LongPressSpeedPreview(speed = { playState().speed }) - } - - SubtitleTrackDialog( - onDismissRequest = onDismissSubtitleTrackDialog, - subtitleTrackDialogState = subtitleTrackDialogState, - subtitleTrackDialogCallback = subtitleTrackDialogCallback, - ) - - val systemUiController = rememberSystemUiController() - LaunchedEffect(subtitleTrackDialogState().show) { - delay(200) - systemUiController.isSystemBarsVisible = false - systemUiController.systemBarsBehavior = - WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE - } - } - } -} - -@Composable -private fun AutoHiddenBox( - enabled: () -> Boolean, - show: () -> Boolean, - onBack: () -> Unit, - playState: () -> PlayState, - playStateCallback: PlayStateCallback, - onSubtitleTrackClick: () -> Unit, - transformState: () -> TransformState, - transformStateCallback: TransformStateCallback, - onScreenshot: () -> Unit, - onRestartAutoHideControllerRunnable: () -> Unit, -) { - Box { - AnimatedVisibility( - visible = show(), - enter = fadeIn(), - exit = fadeOut(), - ) { - ConstraintLayout(modifier = Modifier.fillMaxSize()) { - val (topBar, bottomBar, screenshot, forward85s, resetTransform) = createRefs() - - TopBar( - modifier = Modifier.constrainAs(topBar) { top.linkTo(parent.top) }, - title = playState().title, - onBack = onBack, - ) - BottomBar( - modifier = Modifier.constrainAs(bottomBar) { bottom.linkTo(parent.bottom) }, - playStateCallback = playStateCallback, - playState = playState, - onSubtitleTrackClick = onSubtitleTrackClick, - onRestartAutoHideControllerRunnable = onRestartAutoHideControllerRunnable, - ) - - if (LocalPlayerShowScreenshotButton.current) { - Screenshot( - modifier = Modifier - .constrainAs(screenshot) { - bottom.linkTo(parent.bottom) - top.linkTo(parent.top) - end.linkTo(parent.end) - } - .padding(end = 20.dp), - onClick = onScreenshot, - ) - } - - // +85s button - if (LocalPlayerShow85sButton.current) { - Forward85s( - modifier = Modifier - .constrainAs(forward85s) { - bottom.linkTo(bottomBar.top) - end.linkTo(parent.end) - } - .padding(end = 20.dp), - onClick = { - with(playState()) { playStateCallback.onSeekTo(currentPosition + 85) } - }, - ) - } - - // Reset transform - if (transformState().run { - videoZoom != 1f || videoRotate != 0f || videoOffset != Offset.Zero - } - ) { - ResetTransform( - modifier = Modifier.constrainAs(resetTransform) { - bottom.linkTo(bottomBar.top) - top.linkTo(parent.top) - start.linkTo(parent.start) - end.linkTo(parent.end) - verticalBias = 1f - }, - enabled = enabled, - onClick = { - with(transformStateCallback) { - onVideoOffset(Offset.Zero) - onVideoZoom(1f) - onVideoRotate(0f) - } - } - ) - } - } - } - } -} - -@Composable -private fun BoxScope.SeekTimePreview( - value: () -> Int, - duration: () -> Int, -) { - Row( - modifier = Modifier - .align(Alignment.Center) - .clip(RoundedCornerShape(6.dp)) - .background(color = ControllerLabelGray) - .padding(horizontal = 16.dp, vertical = 10.dp), - ) { - Text( - text = value() - .coerceIn(0..duration()) - .toDurationString(), - style = MaterialTheme.typography.labelLarge, - fontSize = TextUnit(18f, TextUnitType.Sp), - color = Color.White, - ) - Text( - modifier = Modifier.padding(horizontal = 6.dp), - text = "/", - style = MaterialTheme.typography.labelLarge, - fontSize = TextUnit(18f, TextUnitType.Sp), - color = Color.White, - ) - Text( - text = duration().toDurationString(), - style = MaterialTheme.typography.labelLarge, - fontSize = TextUnit(18f, TextUnitType.Sp), - color = Color.White, - ) - } -} - -@Composable -private fun BoxScope.BrightnessPreview( - value: () -> Float, - range: () -> ClosedFloatingPointRange, -) { - Row( - modifier = Modifier - .align(Alignment.Center) - .clip(RoundedCornerShape(6.dp)) - .background(color = ControllerLabelGray) - .padding(horizontal = 16.dp, vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - val start = range().start - val endInclusive = range().endInclusive - val length = endInclusive - start - val icon = when (value()) { - in start..start + length / 3 -> Icons.Rounded.BrightnessLow - in start + length * 2 / 3..endInclusive -> Icons.Rounded.BrightnessHigh - else -> Icons.Rounded.BrightnessMedium - } - val percentValue = (value() - start) / length - Icon(modifier = Modifier.size(30.dp), imageVector = icon, contentDescription = null) - LinearProgressIndicator( - progress = { percentValue }, - modifier = Modifier - .padding(horizontal = 16.dp) - .width(100.dp), - drawStopIndicator = null, - ) - Text( - modifier = Modifier.animateContentSize(), - text = percentValue.toPercentage(format = "%.0f%%"), - style = MaterialTheme.typography.labelLarge, - fontSize = TextUnit(18f, TextUnitType.Sp), - color = Color.White, - ) - } -} - -@Composable -private fun BoxScope.VolumePreview( - value: () -> Int, - range: () -> IntRange, -) { - Row( - modifier = Modifier - .align(Alignment.Center) - .clip(RoundedCornerShape(6.dp)) - .background(color = ControllerLabelGray) - .padding(horizontal = 16.dp, vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - val start = range().first - val endInclusive = range().last - val length = endInclusive - start - val v = value() - val icon = when { - v <= start -> Icons.AutoMirrored.Rounded.VolumeMute - v in start..start + length / 2 -> Icons.AutoMirrored.Rounded.VolumeDown - else -> Icons.AutoMirrored.Rounded.VolumeUp - } - val percentValue = (value() - start).toFloat() / length - Icon(modifier = Modifier.size(30.dp), imageVector = icon, contentDescription = null) - LinearProgressIndicator( - progress = { percentValue }, - modifier = Modifier - .padding(horizontal = 16.dp) - .width(100.dp), - drawStopIndicator = null, - ) - Text( - modifier = Modifier.animateContentSize(), - text = percentValue.toPercentage(format = "%.0f%%"), - style = MaterialTheme.typography.labelLarge, - fontSize = TextUnit(18f, TextUnitType.Sp), - color = Color.White, - ) - } -} - -@Composable -private fun BoxScope.LongPressSpeedPreview(speed: () -> Float) { - Row( - modifier = Modifier - .align(BiasAlignment(0f, -0.6f)) - .clip(RoundedCornerShape(6.dp)) - .background(color = ControllerLabelGray) - .padding(horizontal = 16.dp, vertical = 10.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - modifier = Modifier.size(30.dp), - imageVector = Icons.Rounded.FastForward, - contentDescription = stringResource(id = R.string.player_long_press_playback_speed), - ) - Spacer(modifier = Modifier.width(12.dp)) - Text( - text = "${speed()}x", - style = MaterialTheme.typography.labelLarge, - fontSize = TextUnit(18f, TextUnitType.Sp), - color = Color.White, - ) - } -} - -@Composable -private fun ResetTransform( - modifier: Modifier = Modifier, - enabled: () -> Boolean, - onClick: () -> Unit, -) { - Text( - modifier = modifier - .clip(RoundedCornerShape(6.dp)) - .background(color = ControllerLabelGray) - .clickable(enabled = enabled(), onClick = onClick) - .padding(horizontal = 16.dp, vertical = 10.dp), - text = stringResource(id = R.string.player_reset_zoom), - style = MaterialTheme.typography.labelLarge, - fontSize = TextUnit(16f, TextUnitType.Sp), - color = Color.White, - ) -} - -@Composable -private fun Screenshot( - modifier: Modifier = Modifier, - onClick: () -> Unit, -) { - Icon( - modifier = modifier - .clip(RoundedCornerShape(6.dp)) - .background(color = ControllerLabelGray) - .clickable(onClick = onClick) - .padding(10.dp), - imageVector = Icons.Rounded.CameraAlt, - contentDescription = stringResource(id = R.string.player_screenshot), - tint = Color.White, - ) -} - -@Composable -private fun Forward85s( - modifier: Modifier = Modifier, - onClick: () -> Unit, -) { - Text( - modifier = modifier - .clip(RoundedCornerShape(6.dp)) - .background(color = ControllerLabelGray) - .clickable(onClick = onClick) - .padding(horizontal = 16.dp, vertical = 10.dp), - text = stringResource(id = R.string.player_forward_85s), - style = MaterialTheme.typography.labelLarge, - fontSize = TextUnit(18f, TextUnitType.Sp), - color = Color.White, - ) -} - -@Composable -private fun TopBar( - modifier: Modifier = Modifier, - title: String, - onBack: () -> Unit -) { - Row( - modifier = modifier - .fillMaxWidth() - .background( - brush = Brush.verticalGradient( - colors = listOf(ControllerBarGray, Color.Transparent) - ) - ) - .windowInsetsPadding( - WindowInsets.displayCutout.only( - WindowInsetsSides.Horizontal + WindowInsetsSides.Top - ) - ) - .padding(bottom = 30.dp) - .padding(horizontal = 6.dp, vertical = 3.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - modifier = Modifier - .clip(CircleShape) - .size(56.dp) - .clickable(onClick = onBack) - .padding(15.dp), - imageVector = Icons.Outlined.ArrowBackIosNew, - contentDescription = stringResource(id = R.string.back), - ) - Spacer(modifier = Modifier.width(3.dp)) - Text( - modifier = Modifier - .weight(1f) - .basicMarquee(), - text = title, - style = MaterialTheme.typography.titleMedium, - color = Color.White, - maxLines = 1, - ) - Spacer(modifier = Modifier.width(3.dp)) - } -} - -@Composable -private fun BottomBar( - modifier: Modifier = Modifier, - playState: () -> PlayState, - playStateCallback: PlayStateCallback, - onSubtitleTrackClick: () -> Unit, - onRestartAutoHideControllerRunnable: () -> Unit, -) { - val playPositionStateValue = playState() - - Column( - modifier = modifier - .fillMaxWidth() - .background( - brush = Brush.verticalGradient( - colors = listOf(Color.Transparent, ControllerBarGray) - ) - ) - .windowInsetsPadding( - WindowInsets.displayCutout.only( - WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom - ) - ) - .padding(top = 30.dp) - .padding(horizontal = 6.dp) - ) { - Row( - modifier = Modifier.padding(horizontal = 12.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - val sliderInteractionSource = remember { MutableInteractionSource() } - var sliderValue by rememberSaveable { - mutableFloatStateOf(playPositionStateValue.currentPosition.toFloat()) - } - var valueIsChanging by rememberSaveable { mutableStateOf(false) } - if (!valueIsChanging && !playPositionStateValue.isSeeking && - sliderValue != playPositionStateValue.currentPosition.toFloat() - ) { - sliderValue = playPositionStateValue.currentPosition.toFloat() - } - Text( - text = playPositionStateValue.currentPosition.toDurationString(), - style = MaterialTheme.typography.labelLarge, - color = Color.White, - ) - Slider( - modifier = Modifier - .padding(6.dp) - .height(10.dp) - .weight(1f), - value = sliderValue, - onValueChange = { - valueIsChanging = true - onRestartAutoHideControllerRunnable() - sliderValue = it - }, - onValueChangeFinished = { - playStateCallback.onSeekTo(sliderValue.toInt()) - valueIsChanging = false - }, - valueRange = 0f..playPositionStateValue.duration.toFloat(), - interactionSource = sliderInteractionSource, - thumb = { - Box( - modifier = Modifier.fillMaxHeight(), - contentAlignment = Alignment.Center, - ) { - Spacer( - modifier = Modifier - .padding(horizontal = 3.dp) - .clip(CircleShape) - .size(14.dp) - .background( - MaterialTheme.colorScheme.primary - .alwaysLight(true) - .toHct() - .withTone(90.0) - .toColor() - ) - ) - } - }, - track = { - Spacer( - modifier = Modifier - .clip(RoundedCornerShape(6.dp)) - .fillMaxWidth() - .height(3.dp) - .background(SliderDefaults.colors().activeTrackColor) - ) - }, - ) - Text( - text = playPositionStateValue.duration.toDurationString(), - style = MaterialTheme.typography.labelLarge, - color = Color.White, - ) - } - Row( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 3.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - modifier = Modifier - .clip(CircleShape) - .size(50.dp) - .clickable(onClick = playStateCallback.onPlayStateChanged) - .padding(7.dp), - imageVector = if (playPositionStateValue.isPlaying) Icons.Rounded.Pause else Icons.Rounded.PlayArrow, - contentDescription = stringResource(if (playPositionStateValue.isPlaying) R.string.pause else R.string.play), - ) - - Spacer(modifier = Modifier.weight(1f)) - - Icon( - modifier = Modifier - .clip(CircleShape) - .size(45.dp) - .clickable(onClick = onSubtitleTrackClick) - .padding(9.dp), - imageVector = Icons.Rounded.ClosedCaption, - contentDescription = stringResource(R.string.player_subtitle_track), - ) - } - } -} - -fun Int.toDurationString(sign: Boolean = false, splitter: String = ":"): String { - if (sign) return (if (this >= 0) "+" else "-") + abs(this).toDurationString() - - val hours = this / 3600 - val minutes = this % 3600 / 60 - val seconds = this % 60 - return if (hours == 0) "%02d$splitter%02d".format(minutes, seconds) - else "%d$splitter%02d$splitter%02d".format(hours, minutes, seconds) } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/mpv/ForwardRipple.kt b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/ForwardRipple.kt similarity index 99% rename from app/src/main/java/com/skyd/anivu/ui/mpv/ForwardRipple.kt rename to app/src/main/java/com/skyd/anivu/ui/mpv/controller/ForwardRipple.kt index 085820d8..d5fe4352 100644 --- a/app/src/main/java/com/skyd/anivu/ui/mpv/ForwardRipple.kt +++ b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/ForwardRipple.kt @@ -1,4 +1,4 @@ -package com.skyd.anivu.ui.mpv +package com.skyd.anivu.ui.mpv.controller import androidx.compose.animation.core.Spring import androidx.compose.animation.core.animateFloatAsState diff --git a/app/src/main/java/com/skyd/anivu/ui/mpv/controller/PlayerController.kt b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/PlayerController.kt new file mode 100644 index 00000000..170f54bc --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/PlayerController.kt @@ -0,0 +1,348 @@ +package com.skyd.anivu.ui.mpv.controller + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.FastForward +import androidx.compose.material.icons.rounded.FastRewind +import androidx.compose.material3.LocalContentColor +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +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.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.core.view.WindowInsetsControllerCompat +import com.skyd.anivu.ui.component.rememberSystemUiController +import com.skyd.anivu.ui.local.LocalPlayerShow85sButton +import com.skyd.anivu.ui.local.LocalPlayerShowScreenshotButton +import com.skyd.anivu.ui.mpv.controller.bar.BottomBar +import com.skyd.anivu.ui.mpv.controller.bar.BottomBarCallback +import com.skyd.anivu.ui.mpv.controller.bar.TopBar +import com.skyd.anivu.ui.mpv.controller.button.Forward85s +import com.skyd.anivu.ui.mpv.controller.button.ResetTransform +import com.skyd.anivu.ui.mpv.controller.button.Screenshot +import com.skyd.anivu.ui.mpv.controller.preview.BrightnessPreview +import com.skyd.anivu.ui.mpv.controller.preview.LongPressSpeedPreview +import com.skyd.anivu.ui.mpv.controller.preview.SeekTimePreview +import com.skyd.anivu.ui.mpv.controller.preview.VolumePreview +import com.skyd.anivu.ui.mpv.controller.state.PlayState +import com.skyd.anivu.ui.mpv.controller.state.PlayStateCallback +import com.skyd.anivu.ui.mpv.controller.state.TransformState +import com.skyd.anivu.ui.mpv.controller.state.TransformStateCallback +import com.skyd.anivu.ui.mpv.controller.state.track.AudioTrackDialogCallback +import com.skyd.anivu.ui.mpv.controller.state.track.SubtitleTrackDialogCallback +import com.skyd.anivu.ui.mpv.controller.state.track.TrackDialogState +import com.skyd.anivu.ui.mpv.controller.dialog.AudioTrackDialog +import com.skyd.anivu.ui.mpv.controller.dialog.SubtitleTrackDialog +import kotlinx.coroutines.delay + + +internal val ControllerBarGray = Color(0xAD000000) +internal val ControllerLabelGray = Color(0x70000000) + +@Composable +internal fun PlayerController( + enabled: () -> Boolean, + onBack: () -> Unit, + playState: () -> PlayState, + playStateCallback: PlayStateCallback, + bottomBarCallback: BottomBarCallback, + trackDialogState: TrackDialogState, + onDismissSubtitleTrackDialog: () -> Unit, + onDismissAudioTrackDialog: () -> Unit, + subtitleTrackDialogCallback: SubtitleTrackDialogCallback, + audioTrackDialogCallback: AudioTrackDialogCallback, + transformState: () -> TransformState, + transformStateCallback: TransformStateCallback, + onScreenshot: () -> Unit, +) { + var showController by rememberSaveable { mutableStateOf(true) } + var controllerWidth by remember { mutableIntStateOf(0) } + var controllerHeight by remember { mutableIntStateOf(0) } + var controllerLayoutCoordinates by remember { mutableStateOf(null) } + + val view = LocalView.current + val autoHideControllerRunnable = remember { Runnable { showController = false } } + val cancelAutoHideControllerRunnable = { view.removeCallbacks(autoHideControllerRunnable) } + val restartAutoHideControllerRunnable = { + cancelAutoHideControllerRunnable() + if (showController) { + view.postDelayed(autoHideControllerRunnable, 5000) + } + } + LaunchedEffect(showController) { restartAutoHideControllerRunnable() } + + var showSeekTimePreview by remember { mutableStateOf(false) } + var seekTimePreview by remember { mutableIntStateOf(0) } + + var showBrightnessPreview by remember { mutableStateOf(false) } + var brightnessValue by remember { mutableFloatStateOf(0f) } + var brightnessRange by remember { mutableStateOf(0f..0f) } + + var showVolumePreview by remember { mutableStateOf(false) } + var volumeValue by remember { mutableIntStateOf(0) } + var volumeRange by remember { mutableStateOf(0..0) } + + var showForwardRipple by remember { mutableStateOf(false) } + var forwardRippleStartControllerOffset by remember { mutableStateOf(Offset.Zero) } + var showBackwardRipple by remember { mutableStateOf(false) } + var backwardRippleStartControllerOffset by remember { mutableStateOf(Offset.Zero) } + + var isLongPressing by remember { mutableStateOf(false) } + + LaunchedEffect(trackDialogState.subtitleTrackDialogState()) { + if (trackDialogState.subtitleTrackDialogState().show) cancelAutoHideControllerRunnable() + else restartAutoHideControllerRunnable() + } + + CompositionLocalProvider(LocalContentColor provides Color.White) { + Box( + modifier = Modifier + .fillMaxSize() + .onGloballyPositioned { + controllerWidth = it.size.width + controllerHeight = it.size.height + controllerLayoutCoordinates = it + } + .detectPressGestures( + controllerWidth = { controllerWidth }, + playState = playState, + playStateCallback = playStateCallback, + showController = { showController }, + onShowControllerChanged = { showController = it }, + isLongPressing = { isLongPressing }, + isLongPressingChanged = { isLongPressing = it }, + onShowForwardRipple = { + forwardRippleStartControllerOffset = it + showForwardRipple = true + }, + onShowBackwardRipple = { + backwardRippleStartControllerOffset = it + showBackwardRipple = true + }, + cancelAutoHideControllerRunnable = cancelAutoHideControllerRunnable, + restartAutoHideControllerRunnable = restartAutoHideControllerRunnable, + ) + .detectControllerGestures( + enabled = enabled, + controllerWidth = { controllerWidth }, + controllerHeight = { controllerHeight }, + onShowBrightness = { showBrightnessPreview = it }, + onBrightnessRangeChanged = { brightnessRange = it }, + onBrightnessChanged = { brightnessValue = it }, + onShowVolume = { showVolumePreview = it }, + onVolumeRangeChanged = { volumeRange = it }, + onVolumeChanged = { volumeValue = it }, + playState = playState, + playStateCallback = playStateCallback, + onShowSeekTimePreview = { showSeekTimePreview = it }, + onTimePreviewChanged = { seekTimePreview = it }, + transformState = transformState, + transformStateCallback = transformStateCallback, + cancelAutoHideControllerRunnable = cancelAutoHideControllerRunnable, + restartAutoHideControllerRunnable = restartAutoHideControllerRunnable, + ) + ) { + // Forward ripple + AnimatedVisibility( + visible = showForwardRipple, + modifier = Modifier.align(Alignment.CenterEnd), + enter = fadeIn(animationSpec = spring(stiffness = Spring.StiffnessHigh)), + exit = fadeOut(), + ) { + ForwardRipple( + direct = ForwardRippleDirect.Forward, + text = "+10s", + icon = Icons.Rounded.FastForward, + controllerWidth = { controllerWidth }, + parentLayoutCoordinates = controllerLayoutCoordinates, + rippleStartControllerOffset = forwardRippleStartControllerOffset, + onHideRipple = { showForwardRipple = false }, + ) + } + // Backward ripple + AnimatedVisibility( + visible = showBackwardRipple, + modifier = Modifier.align(Alignment.CenterStart), + enter = fadeIn(animationSpec = spring(stiffness = Spring.StiffnessHigh)), + exit = fadeOut(), + ) { + ForwardRipple( + direct = ForwardRippleDirect.Backward, + text = "-10s", + icon = Icons.Rounded.FastRewind, + controllerWidth = { controllerWidth }, + parentLayoutCoordinates = controllerLayoutCoordinates, + rippleStartControllerOffset = backwardRippleStartControllerOffset, + onHideRipple = { showBackwardRipple = false }, + ) + } + // Auto hide box + AutoHiddenBox( + enabled = enabled, + show = { showController }, + onBack = onBack, + playState = playState, + playStateCallback = playStateCallback, + bottomBarCallback = bottomBarCallback, + transformState = transformState, + transformStateCallback = transformStateCallback, + onScreenshot = onScreenshot, + onRestartAutoHideControllerRunnable = restartAutoHideControllerRunnable, + ) + + // Seek time preview + if (showSeekTimePreview) { + SeekTimePreview( + value = { seekTimePreview }, + duration = { playState().duration }, + ) + } + // Brightness preview + if (showBrightnessPreview) { + BrightnessPreview(value = { brightnessValue }, range = { brightnessRange }) + } + // Volume preview + if (showVolumePreview) { + VolumePreview(value = { volumeValue }, range = { volumeRange }) + } + // Long press speed preview + if (isLongPressing) { + LongPressSpeedPreview(speed = { playState().speed }) + } + + AudioTrackDialog( + onDismissRequest = onDismissAudioTrackDialog, + audioTrackDialogState = trackDialogState.audioTrackDialogState, + audioTrackDialogCallback = audioTrackDialogCallback, + ) + SubtitleTrackDialog( + onDismissRequest = onDismissSubtitleTrackDialog, + subtitleTrackDialogState = trackDialogState.subtitleTrackDialogState, + subtitleTrackDialogCallback = subtitleTrackDialogCallback, + ) + + val systemUiController = rememberSystemUiController() + LaunchedEffect( + trackDialogState.subtitleTrackDialogState().show, + trackDialogState.audioTrackDialogState().show, + ) { + delay(200) + systemUiController.isSystemBarsVisible = false + systemUiController.systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } + } + } +} + +@Composable +private fun AutoHiddenBox( + enabled: () -> Boolean, + show: () -> Boolean, + onBack: () -> Unit, + playState: () -> PlayState, + playStateCallback: PlayStateCallback, + bottomBarCallback: BottomBarCallback, + transformState: () -> TransformState, + transformStateCallback: TransformStateCallback, + onScreenshot: () -> Unit, + onRestartAutoHideControllerRunnable: () -> Unit, +) { + Box { + AnimatedVisibility( + visible = show(), + enter = fadeIn(), + exit = fadeOut(), + ) { + ConstraintLayout(modifier = Modifier.fillMaxSize()) { + val (topBar, bottomBar, screenshot, forward85s, resetTransform) = createRefs() + + TopBar( + modifier = Modifier.constrainAs(topBar) { top.linkTo(parent.top) }, + title = playState().title, + onBack = onBack, + ) + BottomBar( + modifier = Modifier.constrainAs(bottomBar) { bottom.linkTo(parent.bottom) }, + playStateCallback = playStateCallback, + playState = playState, + bottomBarCallback = bottomBarCallback, + onRestartAutoHideControllerRunnable = onRestartAutoHideControllerRunnable, + ) + + if (LocalPlayerShowScreenshotButton.current) { + Screenshot( + modifier = Modifier + .constrainAs(screenshot) { + bottom.linkTo(parent.bottom) + top.linkTo(parent.top) + end.linkTo(parent.end) + } + .padding(end = 20.dp), + onClick = onScreenshot, + ) + } + + // +85s button + if (LocalPlayerShow85sButton.current) { + Forward85s( + modifier = Modifier + .constrainAs(forward85s) { + bottom.linkTo(bottomBar.top) + end.linkTo(parent.end) + } + .padding(end = 20.dp), + onClick = { + with(playState()) { playStateCallback.onSeekTo(currentPosition + 85) } + }, + ) + } + + // Reset transform + if (transformState().run { + videoZoom != 1f || videoRotate != 0f || videoOffset != Offset.Zero + } + ) { + ResetTransform( + modifier = Modifier.constrainAs(resetTransform) { + bottom.linkTo(bottomBar.top) + top.linkTo(parent.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + verticalBias = 1f + }, + enabled = enabled, + onClick = { + with(transformStateCallback) { + onVideoOffset(Offset.Zero) + onVideoZoom(1f) + onVideoRotate(0f) + } + } + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/mpv/PointerInputDetector.kt b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/PointerInputDetector.kt similarity index 97% rename from app/src/main/java/com/skyd/anivu/ui/mpv/PointerInputDetector.kt rename to app/src/main/java/com/skyd/anivu/ui/mpv/controller/PointerInputDetector.kt index a4cc4648..6db5b4e0 100644 --- a/app/src/main/java/com/skyd/anivu/ui/mpv/PointerInputDetector.kt +++ b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/PointerInputDetector.kt @@ -1,4 +1,4 @@ -package com.skyd.anivu.ui.mpv +package com.skyd.anivu.ui.mpv.controller import android.content.Context import android.media.AudioManager @@ -21,10 +21,10 @@ import com.skyd.anivu.ext.detectDoubleFingerTransformGestures import com.skyd.anivu.ext.getScreenBrightness import com.skyd.anivu.model.preference.player.PlayerDoubleTapPreference import com.skyd.anivu.ui.local.LocalPlayerDoubleTap -import com.skyd.anivu.ui.mpv.state.PlayState -import com.skyd.anivu.ui.mpv.state.PlayStateCallback -import com.skyd.anivu.ui.mpv.state.TransformState -import com.skyd.anivu.ui.mpv.state.TransformStateCallback +import com.skyd.anivu.ui.mpv.controller.state.PlayState +import com.skyd.anivu.ui.mpv.controller.state.PlayStateCallback +import com.skyd.anivu.ui.mpv.controller.state.TransformState +import com.skyd.anivu.ui.mpv.controller.state.TransformStateCallback import kotlin.math.abs private val inStatusBarArea: PointerInputScope.(y: Float) -> Boolean = { y -> diff --git a/app/src/main/java/com/skyd/anivu/ui/mpv/controller/bar/BarIconButton.kt b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/bar/BarIconButton.kt new file mode 100644 index 00000000..b47fe2b3 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/bar/BarIconButton.kt @@ -0,0 +1,30 @@ +package com.skyd.anivu.ui.mpv.controller.bar + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp + +@Composable +internal fun BarIconButton( + onClick: () -> Unit, + imageVector: ImageVector, + contentDescription: String?, +) { + Icon( + modifier = Modifier + .padding(horizontal = 3.dp) + .clip(CircleShape) + .size(45.dp) + .clickable(onClick = onClick) + .padding(9.dp), + imageVector = imageVector, + contentDescription = contentDescription, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/mpv/controller/bar/BottomBar.kt b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/bar/BottomBar.kt new file mode 100644 index 00000000..1a9612e7 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/bar/BottomBar.kt @@ -0,0 +1,201 @@ +package com.skyd.anivu.ui.mpv.controller.bar + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ClosedCaption +import androidx.compose.material.icons.rounded.MusicNote +import androidx.compose.material.icons.rounded.Pause +import androidx.compose.material.icons.rounded.PlayArrow +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Slider +import androidx.compose.material3.SliderDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +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.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.materialkolor.ktx.toColor +import com.materialkolor.ktx.toHct +import com.skyd.anivu.R +import com.skyd.anivu.ext.alwaysLight +import com.skyd.anivu.ui.mpv.controller.ControllerBarGray +import com.skyd.anivu.ui.mpv.controller.state.PlayState +import com.skyd.anivu.ui.mpv.controller.state.PlayStateCallback +import kotlin.math.abs + + +@Immutable +data class BottomBarCallback( + val onAudioTrackClick: () -> Unit, + val onSubtitleTrackClick: () -> Unit, +) + +@Composable +fun BottomBar( + modifier: Modifier = Modifier, + playState: () -> PlayState, + playStateCallback: PlayStateCallback, + bottomBarCallback: BottomBarCallback, + onRestartAutoHideControllerRunnable: () -> Unit, +) { + val playPositionStateValue = playState() + + Column( + modifier = modifier + .fillMaxWidth() + .background( + brush = Brush.verticalGradient( + colors = listOf(Color.Transparent, ControllerBarGray) + ) + ) + .windowInsetsPadding( + WindowInsets.displayCutout.only( + WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom + ) + ) + .padding(top = 30.dp) + .padding(horizontal = 6.dp) + ) { + Row( + modifier = Modifier.padding(horizontal = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + val sliderInteractionSource = remember { MutableInteractionSource() } + var sliderValue by rememberSaveable { + mutableFloatStateOf(playPositionStateValue.currentPosition.toFloat()) + } + var valueIsChanging by rememberSaveable { mutableStateOf(false) } + if (!valueIsChanging && !playPositionStateValue.isSeeking && + sliderValue != playPositionStateValue.currentPosition.toFloat() + ) { + sliderValue = playPositionStateValue.currentPosition.toFloat() + } + Text( + text = playPositionStateValue.currentPosition.toDurationString(), + style = MaterialTheme.typography.labelLarge, + color = Color.White, + ) + Slider( + modifier = Modifier + .padding(6.dp) + .height(10.dp) + .weight(1f), + value = sliderValue, + onValueChange = { + valueIsChanging = true + onRestartAutoHideControllerRunnable() + sliderValue = it + }, + onValueChangeFinished = { + playStateCallback.onSeekTo(sliderValue.toInt()) + valueIsChanging = false + }, + valueRange = 0f..playPositionStateValue.duration.toFloat(), + interactionSource = sliderInteractionSource, + thumb = { + Box( + modifier = Modifier.fillMaxHeight(), + contentAlignment = Alignment.Center, + ) { + Spacer( + modifier = Modifier + .padding(horizontal = 3.dp) + .clip(CircleShape) + .size(14.dp) + .background( + MaterialTheme.colorScheme.primary + .alwaysLight(true) + .toHct() + .withTone(90.0) + .toColor() + ) + ) + } + }, + track = { + Spacer( + modifier = Modifier + .clip(RoundedCornerShape(6.dp)) + .fillMaxWidth() + .height(3.dp) + .background(SliderDefaults.colors().activeTrackColor) + ) + }, + ) + Text( + text = playPositionStateValue.duration.toDurationString(), + style = MaterialTheme.typography.labelLarge, + color = Color.White, + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 3.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + modifier = Modifier + .clip(CircleShape) + .size(50.dp) + .clickable(onClick = playStateCallback.onPlayStateChanged) + .padding(7.dp), + imageVector = if (playPositionStateValue.isPlaying) Icons.Rounded.Pause else Icons.Rounded.PlayArrow, + contentDescription = stringResource(if (playPositionStateValue.isPlaying) R.string.pause else R.string.play), + ) + + Spacer(modifier = Modifier.weight(1f)) + + BarIconButton( + onClick = bottomBarCallback.onAudioTrackClick, + imageVector = Icons.Rounded.MusicNote, + contentDescription = stringResource(R.string.player_audio_track), + ) + BarIconButton( + onClick = bottomBarCallback.onSubtitleTrackClick, + imageVector = Icons.Rounded.ClosedCaption, + contentDescription = stringResource(R.string.player_subtitle_track), + ) + } + } +} + +fun Int.toDurationString(sign: Boolean = false, splitter: String = ":"): String { + if (sign) return (if (this >= 0) "+" else "-") + abs(this).toDurationString() + + val hours = this / 3600 + val minutes = this % 3600 / 60 + val seconds = this % 60 + return if (hours == 0) "%02d$splitter%02d".format(minutes, seconds) + else "%d$splitter%02d$splitter%02d".format(hours, minutes, seconds) +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/mpv/controller/bar/TopBar.kt b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/bar/TopBar.kt new file mode 100644 index 00000000..2206cb2c --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/bar/TopBar.kt @@ -0,0 +1,78 @@ +package com.skyd.anivu.ui.mpv.controller.bar + +import androidx.compose.foundation.background +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ArrowBackIosNew +import androidx.compose.material3.Icon +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.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.skyd.anivu.R +import com.skyd.anivu.ui.mpv.controller.ControllerBarGray + +@Composable +internal fun TopBar( + modifier: Modifier = Modifier, + title: String, + onBack: () -> Unit +) { + Row( + modifier = modifier + .fillMaxWidth() + .background( + brush = Brush.verticalGradient( + colors = listOf(ControllerBarGray, Color.Transparent) + ) + ) + .windowInsetsPadding( + WindowInsets.displayCutout.only( + WindowInsetsSides.Horizontal + WindowInsetsSides.Top + ) + ) + .padding(bottom = 30.dp) + .padding(horizontal = 6.dp, vertical = 3.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier + .clip(CircleShape) + .size(56.dp) + .clickable(onClick = onBack) + .padding(15.dp), + imageVector = Icons.Outlined.ArrowBackIosNew, + contentDescription = stringResource(id = R.string.back), + ) + Spacer(modifier = Modifier.width(3.dp)) + Text( + modifier = Modifier + .weight(1f) + .basicMarquee(), + text = title, + style = MaterialTheme.typography.titleMedium, + color = Color.White, + maxLines = 1, + ) + Spacer(modifier = Modifier.width(3.dp)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/mpv/controller/button/Forward85s.kt b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/button/Forward85s.kt new file mode 100644 index 00000000..7e185d7e --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/button/Forward85s.kt @@ -0,0 +1,37 @@ +package com.skyd.anivu.ui.mpv.controller.button + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.TextUnitType +import androidx.compose.ui.unit.dp +import com.skyd.anivu.R +import com.skyd.anivu.ui.mpv.controller.ControllerLabelGray + + +@Composable +internal fun Forward85s( + modifier: Modifier = Modifier, + onClick: () -> Unit, +) { + Text( + modifier = modifier + .clip(RoundedCornerShape(6.dp)) + .background(color = ControllerLabelGray) + .clickable(onClick = onClick) + .padding(horizontal = 16.dp, vertical = 10.dp), + text = stringResource(id = R.string.player_forward_85s), + style = MaterialTheme.typography.labelLarge, + fontSize = TextUnit(18f, TextUnitType.Sp), + color = Color.White, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/mpv/controller/button/ResetTransform.kt b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/button/ResetTransform.kt new file mode 100644 index 00000000..cb9be31c --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/button/ResetTransform.kt @@ -0,0 +1,38 @@ +package com.skyd.anivu.ui.mpv.controller.button + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.TextUnitType +import androidx.compose.ui.unit.dp +import com.skyd.anivu.R +import com.skyd.anivu.ui.mpv.controller.ControllerLabelGray + + +@Composable +internal fun ResetTransform( + modifier: Modifier = Modifier, + enabled: () -> Boolean, + onClick: () -> Unit, +) { + Text( + modifier = modifier + .clip(RoundedCornerShape(6.dp)) + .background(color = ControllerLabelGray) + .clickable(enabled = enabled(), onClick = onClick) + .padding(horizontal = 16.dp, vertical = 10.dp), + text = stringResource(id = R.string.player_reset_zoom), + style = MaterialTheme.typography.labelLarge, + fontSize = TextUnit(16f, TextUnitType.Sp), + color = Color.White, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/mpv/controller/button/Screenshot.kt b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/button/Screenshot.kt new file mode 100644 index 00000000..a691729e --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/button/Screenshot.kt @@ -0,0 +1,35 @@ +package com.skyd.anivu.ui.mpv.controller.button + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.CameraAlt +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.skyd.anivu.R +import com.skyd.anivu.ui.mpv.controller.ControllerLabelGray + + +@Composable +internal fun Screenshot( + modifier: Modifier = Modifier, + onClick: () -> Unit, +) { + Icon( + modifier = modifier + .clip(RoundedCornerShape(6.dp)) + .background(color = ControllerLabelGray) + .clickable(onClick = onClick) + .padding(10.dp), + imageVector = Icons.Rounded.CameraAlt, + contentDescription = stringResource(id = R.string.player_screenshot), + tint = Color.White, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/mpv/controller/dialog/AudioTrackDialog.kt b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/dialog/AudioTrackDialog.kt new file mode 100644 index 00000000..63b98604 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/dialog/AudioTrackDialog.kt @@ -0,0 +1,73 @@ +package com.skyd.anivu.ui.mpv.controller.dialog + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +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.rounded.Check +import androidx.compose.material3.AlertDialogDefaults +import androidx.compose.material3.BasicAlertDialog +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.core.view.WindowInsetsControllerCompat +import com.skyd.anivu.R +import com.skyd.anivu.ui.component.rememberSystemUiController +import com.skyd.anivu.ui.mpv.controller.state.track.AudioTrackDialogCallback +import com.skyd.anivu.ui.mpv.controller.state.track.AudioTrackDialogState + + +@Composable +internal fun AudioTrackDialog( + onDismissRequest: () -> Unit, + audioTrackDialogState: () -> AudioTrackDialogState, + audioTrackDialogCallback: AudioTrackDialogCallback, +) { + val state = audioTrackDialogState() + + if (state.show) { + BasicAlertDialog(onDismissRequest = onDismissRequest) { + rememberSystemUiController().apply { + isSystemBarsVisible = false + systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } + Surface( + modifier = Modifier, + shape = AlertDialogDefaults.shape, + color = AlertDialogDefaults.containerColor, + tonalElevation = AlertDialogDefaults.TonalElevation, + ) { + Column( + modifier = Modifier + .padding(PaddingValues(16.dp)) + .verticalScroll(rememberScrollState()) + ) { + Text( + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(bottom = 6.dp), + text = stringResource(id = R.string.player_audio_track), + style = MaterialTheme.typography.headlineSmall, + ) + repeat(state.audioTrack.size) { index -> + val track = state.audioTrack[index] + TrackDialogListItem( + imageVector = if (state.currentAudioTrack.trackId == track.trackId) + Icons.Rounded.Check else null, + iconContentDescription = stringResource(id = R.string.item_selected), + text = track.name, + onClick = { audioTrackDialogCallback.onAudioTrackChanged(track) } + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/mpv/SubtitleTrack.kt b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/dialog/SubtitleTrackDialog.kt similarity index 69% rename from app/src/main/java/com/skyd/anivu/ui/mpv/SubtitleTrack.kt rename to app/src/main/java/com/skyd/anivu/ui/mpv/controller/dialog/SubtitleTrackDialog.kt index ac35a96d..81e4cfa1 100644 --- a/app/src/main/java/com/skyd/anivu/ui/mpv/SubtitleTrack.kt +++ b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/dialog/SubtitleTrackDialog.kt @@ -1,45 +1,34 @@ -package com.skyd.anivu.ui.mpv +package com.skyd.anivu.ui.mpv.controller.dialog import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Add import androidx.compose.material.icons.rounded.Check import androidx.compose.material3.AlertDialogDefaults import androidx.compose.material3.BasicAlertDialog -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.TextUnit -import androidx.compose.ui.unit.TextUnitType import androidx.compose.ui.unit.dp import androidx.core.view.WindowInsetsControllerCompat import com.skyd.anivu.R import com.skyd.anivu.ui.component.AniVuIconButton import com.skyd.anivu.ui.component.rememberSystemUiController -import com.skyd.anivu.ui.mpv.state.SubtitleTrackDialogCallback -import com.skyd.anivu.ui.mpv.state.SubtitleTrackDialogState +import com.skyd.anivu.ui.mpv.controller.state.track.SubtitleTrackDialogCallback +import com.skyd.anivu.ui.mpv.controller.state.track.SubtitleTrackDialogState +import com.skyd.anivu.ui.mpv.resolveUri @Composable @@ -92,7 +81,7 @@ internal fun SubtitleTrackDialog( } repeat(state.subtitleTrack.size) { index -> val track = state.subtitleTrack[index] - SubtitleItem( + TrackDialogListItem( imageVector = if (state.currentSubtitleTrack.trackId == track.trackId) Icons.Rounded.Check else null, iconContentDescription = stringResource(id = R.string.item_selected), @@ -104,38 +93,4 @@ internal fun SubtitleTrackDialog( } } } -} - -@Composable -private fun SubtitleItem( - imageVector: ImageVector?, - iconContentDescription: String? = null, - text: String, - onClick: () -> Unit -) { - Row( - modifier = Modifier - .clip(RoundedCornerShape(12.dp)) - .fillMaxWidth() - .clickable(onClick = onClick) - .padding(horizontal = 16.dp, vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - modifier = Modifier.weight(1f), - text = text, - style = MaterialTheme.typography.labelLarge, - fontSize = TextUnit(16f, TextUnitType.Sp), - ) - Spacer(modifier = Modifier.width(12.dp)) - if (imageVector != null) { - Icon( - modifier = Modifier.size(24.dp), - imageVector = imageVector, - contentDescription = iconContentDescription, - ) - } else { - Spacer(modifier = Modifier.height(24.dp)) - } - } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/mpv/controller/dialog/TrackDialogListItem.kt b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/dialog/TrackDialogListItem.kt new file mode 100644 index 00000000..c9c1e2ca --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/dialog/TrackDialogListItem.kt @@ -0,0 +1,57 @@ +package com.skyd.anivu.ui.mpv.controller.dialog + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +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.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.TextUnitType +import androidx.compose.ui.unit.dp + + +@Composable +internal fun TrackDialogListItem( + imageVector: ImageVector?, + iconContentDescription: String? = null, + text: String, + onClick: () -> Unit +) { + Row( + modifier = Modifier + .clip(RoundedCornerShape(12.dp)) + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.weight(1f), + text = text, + style = MaterialTheme.typography.labelLarge, + fontSize = TextUnit(16f, TextUnitType.Sp), + ) + Spacer(modifier = Modifier.width(12.dp)) + if (imageVector != null) { + Icon( + modifier = Modifier.size(24.dp), + imageVector = imageVector, + contentDescription = iconContentDescription, + ) + } else { + Spacer(modifier = Modifier.height(24.dp)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/mpv/controller/preview/BrightnessPreview.kt b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/preview/BrightnessPreview.kt new file mode 100644 index 00000000..3a49afc9 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/preview/BrightnessPreview.kt @@ -0,0 +1,68 @@ +package com.skyd.anivu.ui.mpv.controller.preview + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.BrightnessHigh +import androidx.compose.material.icons.rounded.BrightnessLow +import androidx.compose.material.icons.rounded.BrightnessMedium +import androidx.compose.material3.Icon +import androidx.compose.material3.LinearProgressIndicator +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.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.TextUnitType +import androidx.compose.ui.unit.dp +import com.skyd.anivu.ext.toPercentage +import com.skyd.anivu.ui.mpv.controller.ControllerLabelGray + +@Composable +internal fun BoxScope.BrightnessPreview( + value: () -> Float, + range: () -> ClosedFloatingPointRange, +) { + Row( + modifier = Modifier + .align(Alignment.Center) + .clip(RoundedCornerShape(6.dp)) + .background(color = ControllerLabelGray) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + val start = range().start + val endInclusive = range().endInclusive + val length = endInclusive - start + val icon = when (value()) { + in start..start + length / 3 -> Icons.Rounded.BrightnessLow + in start + length * 2 / 3..endInclusive -> Icons.Rounded.BrightnessHigh + else -> Icons.Rounded.BrightnessMedium + } + val percentValue = (value() - start) / length + Icon(modifier = Modifier.size(30.dp), imageVector = icon, contentDescription = null) + LinearProgressIndicator( + progress = { percentValue }, + modifier = Modifier + .padding(horizontal = 16.dp) + .width(100.dp), + drawStopIndicator = null, + ) + Text( + modifier = Modifier.animateContentSize(), + text = percentValue.toPercentage(format = "%.0f%%"), + style = MaterialTheme.typography.labelLarge, + fontSize = TextUnit(18f, TextUnitType.Sp), + color = Color.White, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/mpv/controller/preview/LongPressSpeedPreview.kt b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/preview/LongPressSpeedPreview.kt new file mode 100644 index 00000000..f41ce924 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/preview/LongPressSpeedPreview.kt @@ -0,0 +1,53 @@ +package com.skyd.anivu.ui.mpv.controller.preview + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.FastForward +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.BiasAlignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.TextUnitType +import androidx.compose.ui.unit.dp +import com.skyd.anivu.R +import com.skyd.anivu.ui.mpv.controller.ControllerLabelGray + + +@Composable +internal fun BoxScope.LongPressSpeedPreview(speed: () -> Float) { + Row( + modifier = Modifier + .align(BiasAlignment(0f, -0.6f)) + .clip(RoundedCornerShape(6.dp)) + .background(color = ControllerLabelGray) + .padding(horizontal = 16.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier.size(30.dp), + imageVector = Icons.Rounded.FastForward, + contentDescription = stringResource(id = R.string.player_long_press_playback_speed), + ) + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = "${speed()}x", + style = MaterialTheme.typography.labelLarge, + fontSize = TextUnit(18f, TextUnitType.Sp), + color = Color.White, + ) + } +} diff --git a/app/src/main/java/com/skyd/anivu/ui/mpv/controller/preview/SeekTimePreview.kt b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/preview/SeekTimePreview.kt new file mode 100644 index 00000000..5f5e5396 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/preview/SeekTimePreview.kt @@ -0,0 +1,55 @@ +package com.skyd.anivu.ui.mpv.controller.preview + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +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.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.TextUnitType +import androidx.compose.ui.unit.dp +import com.skyd.anivu.ui.mpv.controller.ControllerLabelGray +import com.skyd.anivu.ui.mpv.controller.bar.toDurationString + +@Composable +internal fun BoxScope.SeekTimePreview( + value: () -> Int, + duration: () -> Int, +) { + Row( + modifier = Modifier + .align(Alignment.Center) + .clip(RoundedCornerShape(6.dp)) + .background(color = ControllerLabelGray) + .padding(horizontal = 16.dp, vertical = 10.dp), + ) { + Text( + text = value() + .coerceIn(0..duration()) + .toDurationString(), + style = MaterialTheme.typography.labelLarge, + fontSize = TextUnit(18f, TextUnitType.Sp), + color = Color.White, + ) + Text( + modifier = Modifier.padding(horizontal = 6.dp), + text = "/", + style = MaterialTheme.typography.labelLarge, + fontSize = TextUnit(18f, TextUnitType.Sp), + color = Color.White, + ) + Text( + text = duration().toDurationString(), + style = MaterialTheme.typography.labelLarge, + fontSize = TextUnit(18f, TextUnitType.Sp), + color = Color.White, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/mpv/controller/preview/VolumePreview.kt b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/preview/VolumePreview.kt new file mode 100644 index 00000000..1e1b7e86 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/preview/VolumePreview.kt @@ -0,0 +1,70 @@ +package com.skyd.anivu.ui.mpv.controller.preview + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.VolumeDown +import androidx.compose.material.icons.automirrored.rounded.VolumeMute +import androidx.compose.material.icons.automirrored.rounded.VolumeUp +import androidx.compose.material3.Icon +import androidx.compose.material3.LinearProgressIndicator +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.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.TextUnitType +import androidx.compose.ui.unit.dp +import com.skyd.anivu.ext.toPercentage +import com.skyd.anivu.ui.mpv.controller.ControllerLabelGray + + +@Composable +internal fun BoxScope.VolumePreview( + value: () -> Int, + range: () -> IntRange, +) { + Row( + modifier = Modifier + .align(Alignment.Center) + .clip(RoundedCornerShape(6.dp)) + .background(color = ControllerLabelGray) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + val start = range().first + val endInclusive = range().last + val length = endInclusive - start + val v = value() + val icon = when { + v <= start -> Icons.AutoMirrored.Rounded.VolumeMute + v in start..start + length / 2 -> Icons.AutoMirrored.Rounded.VolumeDown + else -> Icons.AutoMirrored.Rounded.VolumeUp + } + val percentValue = (value() - start).toFloat() / length + Icon(modifier = Modifier.size(30.dp), imageVector = icon, contentDescription = null) + LinearProgressIndicator( + progress = { percentValue }, + modifier = Modifier + .padding(horizontal = 16.dp) + .width(100.dp), + drawStopIndicator = null, + ) + Text( + modifier = Modifier.animateContentSize(), + text = percentValue.toPercentage(format = "%.0f%%"), + style = MaterialTheme.typography.labelLarge, + fontSize = TextUnit(18f, TextUnitType.Sp), + color = Color.White, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/mpv/state/PlayState.kt b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/state/PlayState.kt similarity index 93% rename from app/src/main/java/com/skyd/anivu/ui/mpv/state/PlayState.kt rename to app/src/main/java/com/skyd/anivu/ui/mpv/controller/state/PlayState.kt index 784d0f12..3534f9d3 100644 --- a/app/src/main/java/com/skyd/anivu/ui/mpv/state/PlayState.kt +++ b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/state/PlayState.kt @@ -1,4 +1,4 @@ -package com.skyd.anivu.ui.mpv.state +package com.skyd.anivu.ui.mpv.controller.state import androidx.compose.runtime.Immutable diff --git a/app/src/main/java/com/skyd/anivu/ui/mpv/state/TransformState.kt b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/state/TransformState.kt similarity index 92% rename from app/src/main/java/com/skyd/anivu/ui/mpv/state/TransformState.kt rename to app/src/main/java/com/skyd/anivu/ui/mpv/controller/state/TransformState.kt index fbcd4c10..d4093453 100644 --- a/app/src/main/java/com/skyd/anivu/ui/mpv/state/TransformState.kt +++ b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/state/TransformState.kt @@ -1,4 +1,4 @@ -package com.skyd.anivu.ui.mpv.state +package com.skyd.anivu.ui.mpv.controller.state import androidx.compose.runtime.Immutable import androidx.compose.ui.geometry.Offset diff --git a/app/src/main/java/com/skyd/anivu/ui/mpv/controller/state/track/AudioTrackDialogState.kt b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/state/track/AudioTrackDialogState.kt new file mode 100644 index 00000000..d917a6b9 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/state/track/AudioTrackDialogState.kt @@ -0,0 +1,23 @@ +package com.skyd.anivu.ui.mpv.controller.state.track + +import androidx.compose.runtime.Immutable +import com.skyd.anivu.ui.mpv.MPVView + +data class AudioTrackDialogState( + val show: Boolean, + val currentAudioTrack: MPVView.Track, + val audioTrack: List, +) { + companion object { + val initial = AudioTrackDialogState( + show = false, + currentAudioTrack = MPVView.Track(0, ""), + audioTrack = emptyList(), + ) + } +} + +@Immutable +data class AudioTrackDialogCallback( + val onAudioTrackChanged: (MPVView.Track) -> Unit, +) \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/mpv/state/SubtitleTrackDialogState.kt b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/state/track/SubtitleTrackDialogState.kt similarity index 91% rename from app/src/main/java/com/skyd/anivu/ui/mpv/state/SubtitleTrackDialogState.kt rename to app/src/main/java/com/skyd/anivu/ui/mpv/controller/state/track/SubtitleTrackDialogState.kt index f2527912..03e4e46c 100644 --- a/app/src/main/java/com/skyd/anivu/ui/mpv/state/SubtitleTrackDialogState.kt +++ b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/state/track/SubtitleTrackDialogState.kt @@ -1,4 +1,4 @@ -package com.skyd.anivu.ui.mpv.state +package com.skyd.anivu.ui.mpv.controller.state.track import androidx.compose.runtime.Immutable import com.skyd.anivu.ui.mpv.MPVView diff --git a/app/src/main/java/com/skyd/anivu/ui/mpv/controller/state/track/TrackDialogState.kt b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/state/track/TrackDialogState.kt new file mode 100644 index 00000000..6b148251 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/mpv/controller/state/track/TrackDialogState.kt @@ -0,0 +1,16 @@ +package com.skyd.anivu.ui.mpv.controller.state.track + +import androidx.compose.runtime.Immutable + +@Immutable +data class TrackDialogState( + val audioTrackDialogState: () -> AudioTrackDialogState, + val subtitleTrackDialogState: () -> SubtitleTrackDialogState, +) { + companion object { + val initial = TrackDialogState( + audioTrackDialogState = { AudioTrackDialogState.initial }, + subtitleTrackDialogState = { SubtitleTrackDialogState.initial }, + ) + } +} \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index c8eefc66..a8ba1054 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -211,6 +211,7 @@ 添加字幕文件 硬件解码 优先使用硬件解码,失败时改用软件解码。 + 音轨 每 %d 分钟 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 96d71b71..cf69f7ea 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -219,6 +219,7 @@ Add subtitle file Hardware decoding Try hardware decoding first; fallback to software if it fails + Audio track Every %d minute Every %d minutes