diff --git a/app/src/main/java/live/mehiz/mpvkt/ui/player/PlayerActivity.kt b/app/src/main/java/live/mehiz/mpvkt/ui/player/PlayerActivity.kt index 405cc5b..1fc01c5 100644 --- a/app/src/main/java/live/mehiz/mpvkt/ui/player/PlayerActivity.kt +++ b/app/src/main/java/live/mehiz/mpvkt/ui/player/PlayerActivity.kt @@ -8,6 +8,7 @@ import android.net.Uri import android.os.Bundle import android.os.ParcelFileDescriptor import android.util.Log +import android.view.KeyEvent import android.view.WindowManager import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity @@ -323,4 +324,13 @@ class PlayerActivity : AppCompatActivity() { PlayerOrientation.SensorLandscape -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE } } + + override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { + when (keyCode) { + KeyEvent.KEYCODE_VOLUME_UP -> { viewModel.changeVolumeBy(1) } + KeyEvent.KEYCODE_VOLUME_DOWN -> { viewModel.changeVolumeBy(-1) } + else -> { super.onKeyDown(keyCode, event) } + } + return true + } } diff --git a/app/src/main/java/live/mehiz/mpvkt/ui/player/PlayerViewModel.kt b/app/src/main/java/live/mehiz/mpvkt/ui/player/PlayerViewModel.kt index a7fffe6..01ac0ab 100644 --- a/app/src/main/java/live/mehiz/mpvkt/ui/player/PlayerViewModel.kt +++ b/app/src/main/java/live/mehiz/mpvkt/ui/player/PlayerViewModel.kt @@ -61,6 +61,10 @@ class PlayerViewModel( val areControlsLocked = _areControlsLocked.asStateFlow() val playerUpdate = MutableStateFlow(PlayerUpdates.None) + val isBrightnessSliderShown = MutableStateFlow(false) + val isVolumeSliderShown = MutableStateFlow(false) + val currentBrightness = MutableStateFlow(activity.window.attributes.screenBrightness) + val currentVolume = MutableStateFlow(activity.audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)) val sheetShown = MutableStateFlow(Sheets.None) val gestureSeekAmount = MutableStateFlow(0) @@ -217,7 +221,7 @@ class PlayerViewModel( fun pause() { activity.player.paused = true - _paused.value = true + _paused.update { true } } fun unpause() { @@ -272,20 +276,35 @@ class PlayerViewModel( isLoading.update { true } } - fun changeBrightnessWithDrag( - dragAmount: Float, + fun changeBrightnessBy(change: Float) { + changeBrightnessTo(currentBrightness.value + change) + } + + fun changeBrightnessTo( + brightness: Float, ) { + isBrightnessSliderShown.update { true } + currentBrightness.update { brightness.coerceIn(0f, 1f) } activity.window.attributes = activity.window.attributes.apply { - screenBrightness = dragAmount.coerceIn(0f, 1f) + screenBrightness = brightness.coerceIn(0f, 1f) } } - fun changeVolumeWithDrag(dragAmount: Float) { + fun changeVolumeBy(change: Int) { + changeVolumeTo(currentVolume.value + change) + } + + val maxVolume = activity.audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) + fun changeVolumeTo(volume: Int) { + isVolumeSliderShown.update { true } + val newVolume = volume.coerceIn(0..maxVolume) activity.audioManager.setStreamVolume( AudioManager.STREAM_MUSIC, - dragAmount.toInt(), - AudioManager.FLAG_SHOW_UI, + newVolume, + 0, ) + println(activity.audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)) + currentVolume.update { newVolume } } fun changeVideoAspect(aspect: VideoAspect) { diff --git a/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/GestureHandler.kt b/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/GestureHandler.kt index e9efe3a..e1878c5 100644 --- a/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/GestureHandler.kt +++ b/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/GestureHandler.kt @@ -92,6 +92,8 @@ fun GestureHandler( viewModel.showSeekBar() } var isLongPressing by remember { mutableStateOf(false) } + val currentVolume by viewModel.currentVolume.collectAsState() + val currentBrightness by viewModel.currentBrightness.collectAsState() Box( modifier = Modifier .fillMaxSize() @@ -163,23 +165,32 @@ fun GestureHandler( } .pointerInput(Unit) { if ((!brightnessGesture && !volumeGesture) || areControlsLocked) return@pointerInput - var dragAmount = 0f + var startingY = 0f + var originalVolume = currentVolume + var originalBrightness = currentBrightness detectVerticalDragGestures( - onDragEnd = { dragAmount = 0f }, + onDragEnd = { startingY = 0f }, + onDragStart = { + startingY = it.y + originalVolume = currentVolume + originalBrightness = currentBrightness + } ) { change, amount -> - dragAmount -= amount / 10 when { volumeGesture && brightnessGesture -> { - if (change.position.x < size.width / 2) viewModel.changeBrightnessWithDrag(dragAmount) - else viewModel.changeVolumeWithDrag(dragAmount) + if (change.position.x < size.width / 2) { + viewModel.changeBrightnessTo(originalBrightness + ((startingY - change.position.y) * 0.005f)) + } else { + viewModel.changeVolumeTo(originalVolume + ((startingY - change.position.y) * 0.1f).toInt()) + } } brightnessGesture -> { - viewModel.changeBrightnessWithDrag(dragAmount) + viewModel.changeBrightnessTo(originalBrightness + ((startingY - change.position.y) * 0.005f)) } // it's not always true, AS is drunk volumeGesture -> { - viewModel.changeVolumeWithDrag(dragAmount) + viewModel.changeVolumeTo(originalVolume + ((startingY - change.position.y) * 0.1f).toInt()) } else -> {} diff --git a/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/PlayerControls.kt b/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/PlayerControls.kt index 244e083..ffe0d23 100644 --- a/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/PlayerControls.kt +++ b/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/PlayerControls.kt @@ -25,6 +25,7 @@ import androidx.compose.material.icons.filled.LockOpen import androidx.compose.material.ripple.LocalRippleTheme import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -53,10 +54,12 @@ import live.mehiz.mpvkt.preferences.PlayerPreferences import live.mehiz.mpvkt.preferences.preference.collectAsState import live.mehiz.mpvkt.ui.player.PlayerUpdates import live.mehiz.mpvkt.ui.player.PlayerViewModel +import live.mehiz.mpvkt.ui.player.controls.components.BrightnessSlider import live.mehiz.mpvkt.ui.player.controls.components.ControlsButton import live.mehiz.mpvkt.ui.player.controls.components.DoubleSpeedPlayerUpdate import live.mehiz.mpvkt.ui.player.controls.components.SeekbarWithTimers import live.mehiz.mpvkt.ui.player.controls.components.TextPlayerUpdate +import live.mehiz.mpvkt.ui.player.controls.components.VolumeSlider import live.mehiz.mpvkt.ui.theme.PlayerRippleTheme import live.mehiz.mpvkt.ui.theme.spacing import org.koin.compose.koinInject @@ -103,6 +106,7 @@ class PlayerControls(private val viewModel: PlayerViewModel) { CompositionLocalProvider( LocalRippleTheme provides PlayerRippleTheme, LocalPlayerButtonsClickEvent provides { resetControls = !resetControls }, + LocalContentColor provides Color.White ) { ConstraintLayout( modifier = Modifier @@ -120,8 +124,46 @@ class PlayerControls(private val viewModel: PlayerViewModel) { bottomLeftControls, bottomRightControls, unlockControlsButton, + brightnessSlider, + volumeSlider ) = createRefs() + val isBrightnessSliderShown by viewModel.isBrightnessSliderShown.collectAsState() + val isVolumeSliderShown by viewModel.isVolumeSliderShown.collectAsState() + val brightness by viewModel.currentBrightness.collectAsState() + val volume by viewModel.currentVolume.collectAsState() + LaunchedEffect(volume, brightness) { + delay(1000) + if (isVolumeSliderShown) viewModel.isVolumeSliderShown.update { false } + if (isBrightnessSliderShown) viewModel.isBrightnessSliderShown.update { false } + } + AnimatedVisibility( + isBrightnessSliderShown, + enter = slideInHorizontally { it } + fadeIn(), + exit = slideOutHorizontally { it } + fadeOut(), + modifier = Modifier.constrainAs(brightnessSlider) { + end.linkTo(parent.end, spacing.medium) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + } + ) { BrightnessSlider(brightness, 0f..1f) } + + AnimatedVisibility( + isVolumeSliderShown, + enter = slideInHorizontally { -it } + fadeIn(), + exit = slideOutHorizontally { -it } + fadeOut(), + modifier = Modifier.constrainAs(volumeSlider) { + start.linkTo(parent.start, spacing.medium) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + } + ) { + VolumeSlider( + volume, + 0..viewModel.maxVolume + ) + } + val currentPlayerUpdate by viewModel.playerUpdate.collectAsState() val aspectRatio by playerPreferences.videoAspect.collectAsState() LaunchedEffect(currentPlayerUpdate, aspectRatio) { @@ -139,8 +181,7 @@ class PlayerControls(private val viewModel: PlayerViewModel) { linkTo(parent.top, parent.bottom, bias = 0.2f) }, ) { - val latestOne by remember { mutableStateOf(currentPlayerUpdate) } - when (latestOne) { + when (currentPlayerUpdate) { PlayerUpdates.DoubleSpeed -> DoubleSpeedPlayerUpdate() PlayerUpdates.AspectRatio -> TextPlayerUpdate(stringResource(aspectRatio.titleRes)) else -> {} diff --git a/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/components/PlayerUpdates.kt b/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/components/PlayerUpdates.kt index 858c8c6..bbbc63d 100644 --- a/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/components/PlayerUpdates.kt +++ b/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/components/PlayerUpdates.kt @@ -34,13 +34,7 @@ fun PlayerUpdate( .padding(vertical = 8.dp, horizontal = 16.dp) .animateContentSize(), contentAlignment = Alignment.Center, - ) { - CompositionLocalProvider( - LocalContentColor provides Color.White, - ) { - content() - } - } + ) { content() } } @Composable diff --git a/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/components/VerticalSliders.kt b/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/components/VerticalSliders.kt new file mode 100644 index 0000000..032f20b --- /dev/null +++ b/app/src/main/java/live/mehiz/mpvkt/ui/player/controls/components/VerticalSliders.kt @@ -0,0 +1,149 @@ +package live.mehiz.mpvkt.ui.player.controls.components + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.VolumeDown +import androidx.compose.material.icons.automirrored.filled.VolumeMute +import androidx.compose.material.icons.automirrored.filled.VolumeOff +import androidx.compose.material.icons.automirrored.filled.VolumeUp +import androidx.compose.material.icons.filled.BrightnessHigh +import androidx.compose.material.icons.filled.BrightnessLow +import androidx.compose.material.icons.filled.BrightnessMedium +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +val percentage: (Float, ClosedFloatingPointRange) -> Float = { value, range -> + ((value - range.start) / (range.endInclusive - range.start)).coerceIn(0f, 1f) +} + +val percentageInt: (Int, ClosedRange) -> Float = { value, range -> + ((value - range.start - 0f) / (range.endInclusive - range.start)).coerceIn(0f, 1f) +} + +@Composable +@Preview +fun VerticalSlider( + value: Float = 5f, + range: ClosedFloatingPointRange = 0f..100f +) { + require(range.contains(value)) { "Value must be within the provided range" } + Box( + modifier = Modifier + .height(120.dp) + .aspectRatio(0.2f) + .clip(RoundedCornerShape(16.dp)) + .background(MaterialTheme.colorScheme.background), + contentAlignment = Alignment.BottomCenter + ) { + val targetHeight by animateFloatAsState(percentage(value, range), label = "") + Box( + Modifier + .fillMaxWidth() + .fillMaxHeight(targetHeight) + .background(MaterialTheme.colorScheme.tertiary) + ) + } +} + + +@Composable +@Preview +fun VerticalSlider( + value: Int = 5, + range: ClosedRange = 0..100 +) { + require(range.contains(value)) { "Value must be within the provided range" } + Box( + modifier = Modifier + .height(120.dp) + .aspectRatio(0.2f) + .clip(RoundedCornerShape(16.dp)) + .background(MaterialTheme.colorScheme.background), + contentAlignment = Alignment.BottomCenter + ) { + val targetHeight by animateFloatAsState(percentageInt(value, range), label = "") + Box( + Modifier + .fillMaxWidth() + .fillMaxHeight(targetHeight) + .background(MaterialTheme.colorScheme.tertiary) + ) + } +} + +@Composable +fun BrightnessSlider( + brightness: Float, + range: ClosedFloatingPointRange, +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + (brightness * 100).toInt().toString(), + style = MaterialTheme.typography.bodySmall + ) + VerticalSlider( + brightness, + range + ) + Icon( + when (percentage(brightness, range)) { + in 0f..0.3f -> Icons.Default.BrightnessLow + in 0.3f..0.6f -> Icons.Default.BrightnessMedium + in 0.6f..1f -> Icons.Default.BrightnessHigh + else -> Icons.Default.BrightnessMedium + }, + null + ) + } +} + + +@Composable +fun VolumeSlider( + volume: Int, + range: ClosedRange +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + volume.toString(), + style = MaterialTheme.typography.bodySmall + ) + VerticalSlider( + volume, + range + ) + Icon( + when(percentageInt(volume, range)) { + 0f -> Icons.AutoMirrored.Default.VolumeOff + in 0f..0.3f -> Icons.AutoMirrored.Default.VolumeMute + in 0.3f..0.6f -> Icons.AutoMirrored.Default.VolumeDown + in 0.6f..1f -> Icons.AutoMirrored.Default.VolumeUp + else -> Icons.AutoMirrored.Default.VolumeOff + }, + null + ) + } +}