diff --git a/feature/channel/src/main/java/com/m3u/feature/channel/ChannelMask.kt b/feature/channel/src/main/java/com/m3u/feature/channel/ChannelMask.kt index e889e0899..4bd682837 100644 --- a/feature/channel/src/main/java/com/m3u/feature/channel/ChannelMask.kt +++ b/feature/channel/src/main/java/com/m3u/feature/channel/ChannelMask.kt @@ -16,9 +16,7 @@ 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.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBarsIgnoringVisibility @@ -79,15 +77,14 @@ import com.m3u.feature.channel.MaskCenterState.Play import com.m3u.feature.channel.MaskCenterState.Replay import com.m3u.feature.channel.components.MaskTextButton import com.m3u.feature.channel.components.PlayerMask -import com.m3u.feature.channel.components.VerticalGestureArea import com.m3u.i18n.R.string import com.m3u.material.components.mask.MaskButton import com.m3u.material.components.mask.MaskCircleButton import com.m3u.material.components.mask.MaskPanel import com.m3u.material.components.mask.MaskState import com.m3u.material.effects.currentBackStackEntry -import com.m3u.material.ktx.tv import com.m3u.material.ktx.thenIf +import com.m3u.material.ktx.tv import com.m3u.material.model.LocalSpacing import com.m3u.ui.FontFamilies import com.m3u.ui.Image @@ -105,6 +102,7 @@ import kotlin.time.toDuration internal fun ChannelMask( cover: String, title: String, + gesture: MaskGesture?, playlistTitle: String, playerState: PlayerState, volume: Float, @@ -120,8 +118,7 @@ internal fun ChannelMask( openOrClosePanel: () -> Unit, onEnterPipMode: () -> Unit, onVolume: (Float) -> Unit, - onBrightness: (Float) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val preferences = hiltPreferences() val helper = LocalHelper.current @@ -134,7 +131,6 @@ internal fun ChannelMask( LocalOnBackPressedDispatcherOwner.current ).onBackPressedDispatcher - var gesture: MaskGesture? by remember { mutableStateOf(null) } // because they will be updated frequently, // they must be wrapped with rememberUpdatedState when using them. @@ -147,7 +143,6 @@ internal fun ChannelMask( muted -> stringResource(string.feat_channel_tooltip_unmute) else -> stringResource(string.feat_channel_tooltip_mute) } - val brightnessOrVolumeText by remember { derivedStateOf { when (gesture) { @@ -221,9 +216,12 @@ internal fun ChannelMask( } } - Box { + Box( + modifier = modifier.fillMaxSize() + ) { MaskPanel( - state = maskState + state = maskState, + modifier = Modifier.align(Alignment.Center) ) PlayerMask( @@ -324,36 +322,6 @@ internal fun ChannelMask( .fillMaxSize() .windowInsetsPadding(WindowInsets.systemBarsIgnoringVisibility) ) { - VerticalGestureArea( - percent = currentBrightness, - onDragStart = { - maskState.lock(MaskGesture.BRIGHTNESS) - gesture = MaskGesture.BRIGHTNESS - }, - onDragEnd = { - maskState.unlock(MaskGesture.BRIGHTNESS, 400.milliseconds) - gesture = null - }, - onDrag = onBrightness, - modifier = Modifier - .fillMaxHeight() - .fillMaxWidth(0.18f) - ) - VerticalGestureArea( - percent = currentVolume, - onDragStart = { - maskState.lock(MaskGesture.VOLUME) - gesture = MaskGesture.VOLUME - }, - onDragEnd = { - maskState.unlock(MaskGesture.VOLUME, 400.milliseconds) - gesture = null - }, - onDrag = onVolume, - modifier = Modifier - .fillMaxHeight() - .fillMaxWidth(0.18f) - ) } val maskCenterState = MaskCenterState.of( playerState.playState, @@ -560,8 +528,8 @@ internal fun ChannelMask( } } }, - modifier = modifier ) + } } @@ -572,7 +540,7 @@ private fun MaskCenterButton( modifier: Modifier = Modifier, onPlay: () -> Unit, onPause: () -> Unit, - onRetry: () -> Unit + onRetry: () -> Unit, ) { Box(modifier, contentAlignment = Alignment.Center) { when (maskCenterState) { @@ -610,7 +578,7 @@ private enum class MaskCenterState { isPlaying: Boolean, alwaysShowReplay: Boolean, isPanelExpanded: Boolean, - playerError: Exception? + playerError: Exception?, ): MaskCenterState? = when { isPanelExpanded -> null playState == Player.STATE_BUFFERING -> Loading diff --git a/feature/channel/src/main/java/com/m3u/feature/channel/ChannelScreen.kt b/feature/channel/src/main/java/com/m3u/feature/channel/ChannelScreen.kt index d68f2bbe6..3f23317a1 100644 --- a/feature/channel/src/main/java/com/m3u/feature/channel/ChannelScreen.kt +++ b/feature/channel/src/main/java/com/m3u/feature/channel/ChannelScreen.kt @@ -5,7 +5,15 @@ import android.content.Intent import android.graphics.Rect import android.net.Uri import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.VolumeDown +import androidx.compose.material.icons.automirrored.rounded.VolumeOff +import androidx.compose.material.icons.automirrored.rounded.VolumeUp +import androidx.compose.material.icons.rounded.DarkMode +import androidx.compose.material.icons.rounded.LightMode import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -13,6 +21,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment @@ -34,7 +43,9 @@ import com.m3u.data.database.model.Playlist import com.m3u.feature.channel.components.CoverPlaceholder import com.m3u.feature.channel.components.DlnaDevicesBottomSheet import com.m3u.feature.channel.components.FormatsBottomSheet +import com.m3u.feature.channel.components.MaskGestureValuePanel import com.m3u.feature.channel.components.PlayerPanel +import com.m3u.feature.channel.components.VerticalGestureArea import com.m3u.i18n.R.string import com.m3u.material.components.Background import com.m3u.material.components.PullPanelLayout @@ -42,6 +53,7 @@ import com.m3u.material.components.PullPanelLayoutValue import com.m3u.material.components.mask.MaskInterceptor import com.m3u.material.components.mask.MaskState import com.m3u.material.components.mask.rememberMaskState +import com.m3u.material.components.mask.toggle import com.m3u.material.components.rememberPullPanelLayoutState import com.m3u.material.ktx.checkPermissionOrRationale import com.m3u.ui.Player @@ -159,7 +171,8 @@ fun ChannelRoute( Background( color = Color.Black, - contentColor = Color.White + contentColor = Color.White, + modifier = modifier ) { PullPanelLayout( state = pullPanelLayoutState, @@ -192,7 +205,7 @@ fun ChannelRoute( viewModel.onRemindProgramme(it) } }, - onCancelRemindProgramme = viewModel::onCancelRemindProgramme + onCancelRemindProgramme = viewModel::onCancelRemindProgramme, ) }, content = { @@ -229,7 +242,6 @@ fun ChannelRoute( maskState.unlockAll() pullPanelLayoutState.collapse() }, - modifier = modifier ) } ) @@ -291,8 +303,16 @@ private fun ChannelPlayer( val cover = channel?.cover.orEmpty() val playlistTitle = playlist?.title ?: "--" val favourite = channel?.favourite ?: false - + var gesture: MaskGesture? by remember { mutableStateOf(null) } + val currentBrightness by rememberUpdatedState(brightness) + val currentVolume by rememberUpdatedState(volume) val preferences = hiltPreferences() + var isBrightnessValueChange by remember { + mutableStateOf(false) + } + var isVolumeValueChange by remember { + mutableStateOf(false) + } Background( color = Color.Black, @@ -308,6 +328,40 @@ private fun ChannelPlayer( state = state, modifier = Modifier.fillMaxSize() ) + VerticalGestureArea( + percent = currentBrightness, + onDragStart = { + gesture = MaskGesture.BRIGHTNESS + isBrightnessValueChange = true + }, + onDragEnd = { + gesture = null + isBrightnessValueChange = false + }, + onDrag = onBrightness, + onClick = maskState::toggle, + modifier = Modifier + .fillMaxHeight() + .fillMaxWidth(0.18f) + ) + + VerticalGestureArea( + percent = currentVolume, + onDragStart = { + gesture = MaskGesture.VOLUME + isVolumeValueChange = true + }, + onDragEnd = { + gesture = null + isVolumeValueChange = false + }, + onDrag = onVolume, + onClick = maskState::toggle, + modifier = Modifier + .align(Alignment.TopEnd) + .fillMaxHeight() + .fillMaxWidth(0.18f) + ) val shouldShowPlaceholder = !preferences.noPictureMode && cover.isNotEmpty() && playerState.videoSize.isEmpty @@ -335,10 +389,32 @@ private fun ChannelPlayer( openChooseFormat = openChooseFormat, openOrClosePanel = openOrClosePanel, onVolume = onVolume, - onBrightness = onBrightness, onEnterPipMode = onEnterPipMode, + gesture = gesture ) + if (gesture != null) { + MaskGestureValuePanel( + value = when (gesture) { + MaskGesture.BRIGHTNESS -> "${currentBrightness.times(100).toInt()}%" + else -> "${currentVolume.times(100).toInt()}" + }, + icon = when (gesture) { + MaskGesture.BRIGHTNESS -> when { + brightness < 0.5f -> Icons.Rounded.DarkMode + else -> Icons.Rounded.LightMode + } + + else -> when { + volume == 0f -> Icons.AutoMirrored.Rounded.VolumeOff + volume < 0.5f -> Icons.AutoMirrored.Rounded.VolumeDown + else -> Icons.AutoMirrored.Rounded.VolumeUp + } + }, + modifier = Modifier.align(Alignment.Center) + ) + } + LaunchedEffect(playerState.playerError) { if (playerState.playerError != null) { maskState.wake() @@ -346,4 +422,4 @@ private fun ChannelPlayer( } } } -} +} \ No newline at end of file diff --git a/feature/channel/src/main/java/com/m3u/feature/channel/components/MaskGestureValuePanel.kt b/feature/channel/src/main/java/com/m3u/feature/channel/components/MaskGestureValuePanel.kt new file mode 100644 index 000000000..6917c57e2 --- /dev/null +++ b/feature/channel/src/main/java/com/m3u/feature/channel/components/MaskGestureValuePanel.kt @@ -0,0 +1,52 @@ +package com.m3u.feature.channel.components + +import androidx.compose.foundation.layout.Arrangement +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.material3.Icon +import androidx.compose.material3.Surface +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.dp +import androidx.compose.ui.unit.sp +import androidx.tv.material3.MaterialTheme +import com.m3u.material.model.LocalSpacing +import com.m3u.ui.MonoText + +@Composable +internal fun MaskGestureValuePanel( + icon: ImageVector, + value: String, + modifier: Modifier = Modifier, +) { + val spacing = LocalSpacing.current + Surface( + modifier = modifier + .clip(shape = RoundedCornerShape(10.dp)), + color = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface + ) { + Row( + modifier = Modifier.padding(vertical = 5.dp, horizontal = 10.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = icon, + contentDescription = "icon-gesture", + modifier = Modifier.size(24.dp), + ) + Spacer(modifier = Modifier.width(spacing.extraSmall)) + MonoText( + text = value, fontSize = 14.sp + ) + } + } +} diff --git a/feature/channel/src/main/java/com/m3u/feature/channel/components/MaskValueButton.kt b/feature/channel/src/main/java/com/m3u/feature/channel/components/MaskValueButton.kt index 11074f9f8..104ab8627 100644 --- a/feature/channel/src/main/java/com/m3u/feature/channel/components/MaskValueButton.kt +++ b/feature/channel/src/main/java/com/m3u/feature/channel/components/MaskValueButton.kt @@ -17,8 +17,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import com.m3u.material.components.IconButton import com.m3u.material.components.mask.MaskState -import com.m3u.material.ktx.tv import com.m3u.material.ktx.thenIf +import com.m3u.material.ktx.tv import com.m3u.ui.FontFamilies @Composable diff --git a/feature/channel/src/main/java/com/m3u/feature/channel/components/PlayerMask.kt b/feature/channel/src/main/java/com/m3u/feature/channel/components/PlayerMask.kt index f5a6677a0..dd6d1f92d 100644 --- a/feature/channel/src/main/java/com/m3u/feature/channel/components/PlayerMask.kt +++ b/feature/channel/src/main/java/com/m3u/feature/channel/components/PlayerMask.kt @@ -1,5 +1,6 @@ package com.m3u.feature.channel.components +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row diff --git a/feature/channel/src/main/java/com/m3u/feature/channel/components/VerticalGestureArea.kt b/feature/channel/src/main/java/com/m3u/feature/channel/components/VerticalGestureArea.kt index 5cc4efa0d..8e5ae0d21 100644 --- a/feature/channel/src/main/java/com/m3u/feature/channel/components/VerticalGestureArea.kt +++ b/feature/channel/src/main/java/com/m3u/feature/channel/components/VerticalGestureArea.kt @@ -1,14 +1,18 @@ package com.m3u.feature.channel.components +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import com.m3u.core.architecture.preferences.hiltPreferences import com.m3u.feature.channel.ChannelMaskUtils.detectVerticalGesture +import com.m3u.material.components.mask.toggle import com.m3u.material.ktx.tv import com.m3u.material.ktx.thenIf @@ -18,6 +22,7 @@ internal fun VerticalGestureArea( onDragStart: () -> Unit, onDragEnd: () -> Unit, onDrag: (percent: Float) -> Unit, + onClick: () -> Unit, modifier: Modifier = Modifier ) { val preferences = hiltPreferences() @@ -40,6 +45,11 @@ internal fun VerticalGestureArea( } ) } + .clickable( + onClick = onClick, + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) ) } } diff --git a/material/src/main/java/com/m3u/material/components/mask/MaskPanel.kt b/material/src/main/java/com/m3u/material/components/mask/MaskPanel.kt index b7b048cfa..0be3868d5 100644 --- a/material/src/main/java/com/m3u/material/components/mask/MaskPanel.kt +++ b/material/src/main/java/com/m3u/material/components/mask/MaskPanel.kt @@ -4,7 +4,8 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -15,15 +16,16 @@ import com.m3u.material.components.OuterColumn fun MaskPanel( state: MaskState, modifier: Modifier = Modifier, - verticalArrangement: Arrangement.Vertical = Arrangement.Bottom, - horizontalAlignment: Alignment.Horizontal = Alignment.Start, - content: @Composable ColumnScope.() -> Unit = {} + verticalArrangement: Arrangement.Vertical = Arrangement.Center, + horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally, + content: @Composable ColumnScope.() -> Unit = {}, ) { OuterColumn( verticalArrangement = verticalArrangement, horizontalAlignment = horizontalAlignment, modifier = Modifier - .fillMaxSize() + .fillMaxWidth(0.64f) + .fillMaxHeight() .clickable( onClick = state::toggle, interactionSource = remember { MutableInteractionSource() },