diff --git a/app/shared/foundation/android/platform/PlatformComponentAccessors.android.kt b/app/shared/foundation/android/platform/PlatformComponentAccessors.android.kt new file mode 100644 index 0000000000..6192138bf5 --- /dev/null +++ b/app/shared/foundation/android/platform/PlatformComponentAccessors.android.kt @@ -0,0 +1,41 @@ +package me.him188.ani.app.platform + +import android.media.AudioManager as SystemAudioManager + +actual fun getComponentAccessorsImpl(context: Context): PlatformComponentAccessors = + AndroidPlatformComponentAccessors(context) + +private class AndroidPlatformComponentAccessors( + private val context: Context +) : PlatformComponentAccessors { + override val audioManager: AudioManager by lazy { + AndroidAudioManager( + context.getSystemService(Context.AUDIO_SERVICE) as SystemAudioManager + ) + } +} + +private class AndroidAudioManager( + private val delegate: SystemAudioManager, +) : AudioManager { + private val StreamType.android: Int + get() { + return when (this) { + StreamType.MUSIC -> SystemAudioManager.STREAM_MUSIC + } + } + + override fun getVolume(streamType: StreamType): Float { + return delegate.getStreamVolume(streamType.android).toFloat() / delegate.getStreamMaxVolume(streamType.android) + } + + override fun setVolume(streamType: StreamType, levelPercentage: Float) { + val max = delegate.getStreamMaxVolume(streamType.android) + return delegate.setStreamVolume( + streamType.android, + (levelPercentage * max).toInt() + .coerceIn(minimumValue = 0, maximumValue = max), + SystemAudioManager.FLAG_SHOW_UI + ) + } +} \ No newline at end of file diff --git a/app/shared/foundation/common/platform/AudioManager.kt b/app/shared/foundation/common/platform/AudioManager.kt new file mode 100644 index 0000000000..ddb69d4b0d --- /dev/null +++ b/app/shared/foundation/common/platform/AudioManager.kt @@ -0,0 +1,14 @@ +package me.him188.ani.app.platform + +interface AudioManager { + /** + * @return 0..1 + */ + fun getVolume(streamType: StreamType): Float + + fun setVolume(streamType: StreamType, levelPercentage: Float) +} + +enum class StreamType { + MUSIC, +} \ No newline at end of file diff --git a/app/shared/foundation/common/platform/PlatformComponentAccessors.kt b/app/shared/foundation/common/platform/PlatformComponentAccessors.kt new file mode 100644 index 0000000000..000fd04e00 --- /dev/null +++ b/app/shared/foundation/common/platform/PlatformComponentAccessors.kt @@ -0,0 +1,13 @@ +package me.him188.ani.app.platform + +interface PlatformComponentAccessors { + val audioManager: AudioManager? +} + +fun getComponentAccessors( + context: Context +): PlatformComponentAccessors = getComponentAccessorsImpl(context) + +expect fun getComponentAccessorsImpl( + context: Context +): PlatformComponentAccessors \ No newline at end of file diff --git a/app/shared/foundation/desktop/platform/PlatformComponentAccessors.desktop.kt b/app/shared/foundation/desktop/platform/PlatformComponentAccessors.desktop.kt new file mode 100644 index 0000000000..88cd9a0053 --- /dev/null +++ b/app/shared/foundation/desktop/platform/PlatformComponentAccessors.desktop.kt @@ -0,0 +1,3 @@ +package me.him188.ani.app.platform + +actual fun getComponentAccessorsImpl(context: Context): PlatformComponentAccessors = TODO("Not yet implemented") \ No newline at end of file diff --git a/app/shared/video-player/androidMain/ui/VideoScaffold.android.kt b/app/shared/video-player/androidMain/ui/VideoScaffold.android.kt index f0bce8809b..aebfbf43eb 100644 --- a/app/shared/video-player/androidMain/ui/VideoScaffold.android.kt +++ b/app/shared/video-player/androidMain/ui/VideoScaffold.android.kt @@ -1,6 +1,7 @@ package me.him188.ani.app.videoplayer.ui import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -53,7 +54,8 @@ private fun PreviewVideoScaffoldFullscreen() = ProvideCompositionLocalsForPrevie }, onClickScreen = {}, - onDoubleClickScreen = {} + onDoubleClickScreen = {}, + Modifier.fillMaxSize() ) }, floatingMessage = { diff --git a/app/shared/video-player/commonMain/ui/PlayerControllerOverlay.kt b/app/shared/video-player/commonMain/ui/PlayerControllerOverlay.kt index 2b4fda9177..9fff49526b 100644 --- a/app/shared/video-player/commonMain/ui/PlayerControllerOverlay.kt +++ b/app/shared/video-player/commonMain/ui/PlayerControllerOverlay.kt @@ -26,6 +26,7 @@ import me.him188.ani.app.ui.foundation.ProvideCompositionLocalsForPreview import me.him188.ani.app.ui.theme.aniDarkColorTheme import me.him188.ani.app.ui.theme.slightlyWeaken import me.him188.ani.app.videoplayer.DummyPlayerController +import me.him188.ani.app.videoplayer.ui.top.PlayerTopBar /** @@ -78,7 +79,7 @@ internal fun PreviewPlayerControllerOverlayImpl() { Box(modifier = Modifier.background(Color.Black)) { PlayerControllerOverlay( topBar = { - PlayerNavigationBar( + PlayerTopBar( title = null, actions = { }, diff --git a/app/shared/video-player/commonMain/ui/guesture/SteppedDraggable.kt b/app/shared/video-player/commonMain/ui/guesture/SteppedDraggable.kt new file mode 100644 index 0000000000..755be9095b --- /dev/null +++ b/app/shared/video-player/commonMain/ui/guesture/SteppedDraggable.kt @@ -0,0 +1,182 @@ +package me.him188.ani.app.videoplayer.ui.guesture + +import androidx.annotation.MainThread +import androidx.compose.foundation.MutatePriority +import androidx.compose.foundation.gestures.DragScope +import androidx.compose.foundation.gestures.DraggableState +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.draggable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.debugInspectorInfo +import androidx.compose.ui.unit.Dp +import kotlinx.coroutines.CoroutineScope + + +interface SteppedDraggableState : DraggableState { + fun onDragStarted(offset: Offset, orientation: Orientation) + fun onDragStopped(velocity: Float) +} + +enum class StepDirection { + /** + * - [Orientation.Horizontal]: To the right + * - [Orientation.Vertical]: Down + */ + FORWARD, + BACKWARD, +} + +private class SteppedDraggableStateImpl( + @MainThread private val onStep: (StepDirection) -> Unit, + private val stepSizePx: Float, +) : SteppedDraggableState { + var startOffset: Float by mutableFloatStateOf(Float.NaN) + var currentOffset: Float by mutableFloatStateOf(0f) + var lastCallbackOffset: Float by mutableFloatStateOf(0f) + override fun onDragStarted(offset: Offset, orientation: Orientation) { + startOffset = if (orientation == Orientation.Horizontal) { + offset.x + } else { + offset.y + } + currentOffset = startOffset + } + + override fun onDragStopped(velocity: Float) { + startOffset = Float.NaN + currentOffset = 0f + } + + override fun dispatchRawDelta(delta: Float) { + draggableState.dispatchRawDelta(delta) + } + + override suspend fun drag(dragPriority: MutatePriority, block: suspend DragScope.() -> Unit) { + draggableState.drag(dragPriority, block) + } + + private val draggableState: DraggableState = DraggableState { delta -> + currentOffset += delta + val deltaOffset = currentOffset - startOffset + val step = (deltaOffset / stepSizePx).toInt() + val callbackOffset = step * stepSizePx + if (callbackOffset != lastCallbackOffset) { + if (callbackOffset > lastCallbackOffset) { + onStep(StepDirection.BACKWARD) // delta is inverted + } else { + onStep(StepDirection.FORWARD) + } + lastCallbackOffset = callbackOffset + } + } +} + +@Composable +fun rememberSteppedDraggableState( + stepSize: Dp, + @MainThread onStep: (StepDirection) -> Unit, +): SteppedDraggableState { + val onStepState by rememberUpdatedState(onStep) + val stepSizePx by rememberUpdatedState(with(LocalDensity.current) { stepSize.toPx() }) + return remember { + SteppedDraggableStateImpl( + onStep = { onStepState(it) }, + stepSizePx = stepSizePx, + ) + } +} + +fun Modifier.steppedDraggable( + state: SteppedDraggableState, + orientation: Orientation, + enabled: Boolean = true, + interactionSource: MutableInteractionSource? = null, + startDragImmediately: Boolean = false, + onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit = {}, + onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit = {}, + reverseDirection: Boolean = false, +): Modifier = composed( + inspectorInfo = debugInspectorInfo { + name = "steppedDraggable" + properties["state"] = state + properties["orientation"] = orientation + properties["enabled"] = enabled + properties["interactionSource"] = interactionSource + properties["startDragImmediately"] = startDragImmediately + properties["onDragStarted"] = onDragStarted + properties["onDragStopped"] = onDragStopped + properties["reverseDirection"] = reverseDirection + } +) { + val onDragStartedState by rememberUpdatedState(onDragStarted) + val onDragStoppedState by rememberUpdatedState(onDragStopped) + draggable( + state = state, + orientation = orientation, + enabled = enabled, + interactionSource = interactionSource, + startDragImmediately = startDragImmediately, + onDragStarted = { offset -> + state.onDragStarted(offset, orientation) + onDragStartedState(offset) + }, + onDragStopped = { + state.onDragStopped(it) + onDragStoppedState(it) + }, + reverseDirection = reverseDirection, + ) +} + + +//fun Modifier.combinedSteppedDraggable( +// division: List>, +// orientation: Orientation, +// enabled: Boolean = true, +// interactionSource: MutableInteractionSource? = null, +// startDragImmediately: Boolean = false, +// onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit = {}, +// onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit = {}, +// reverseDirection: Boolean = false, +//): Modifier = composed( +// inspectorInfo = debugInspectorInfo { +// name = "steppedDraggable" +// properties["division"] = division +// properties["orientation"] = orientation +// properties["enabled"] = enabled +// properties["interactionSource"] = interactionSource +// properties["startDragImmediately"] = startDragImmediately +// properties["onDragStarted"] = onDragStarted +// properties["onDragStopped"] = onDragStopped +// properties["reverseDirection"] = reverseDirection +// } +//) { +// val onDragStartedState by rememberUpdatedState(onDragStarted) +// val onDragStoppedState by rememberUpdatedState(onDragStopped) +// draggable( +// state = state.draggableState, +// orientation = orientation, +// enabled = enabled, +// interactionSource = interactionSource, +// startDragImmediately = startDragImmediately, +// onDragStarted = { offset -> +// state.onDragStarted(offset, orientation) +// onDragStartedState(offset) +// }, +// onDragStopped = { +// state.onDragStopped(it) +// onDragStoppedState(it) +// }, +// reverseDirection = reverseDirection, +// ) +//} diff --git a/app/shared/video-player/commonMain/ui/guesture/SwipeVolumeControl.kt b/app/shared/video-player/commonMain/ui/guesture/SwipeVolumeControl.kt new file mode 100644 index 0000000000..b95e0af7f4 --- /dev/null +++ b/app/shared/video-player/commonMain/ui/guesture/SwipeVolumeControl.kt @@ -0,0 +1,59 @@ +package me.him188.ani.app.videoplayer.ui.guesture + +import androidx.annotation.MainThread +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.platform.debugInspectorInfo +import androidx.compose.ui.unit.Dp +import me.him188.ani.app.platform.AudioManager +import me.him188.ani.app.platform.StreamType + +interface LevelController { + @MainThread + fun increaseLevel() + + @MainThread + fun decreaseLevel() +} + +fun AudioManager.asLevelController( + streamType: StreamType, +): LevelController = object : LevelController { + override fun increaseLevel() { + val current = getVolume(streamType) + setVolume(streamType, (current + 0.05f).coerceAtMost(1f)) + } + + override fun decreaseLevel() { + val current = getVolume(streamType) + setVolume(streamType, (current - 0.05f).coerceAtLeast(0f)) + } +} + +fun Modifier.swipeLevelControl( + controller: LevelController, + stepSize: Dp, + orientation: Orientation, +): Modifier = composed( + inspectorInfo = debugInspectorInfo { + name = "swipeLevelControl" + properties["controller"] = controller + properties["stepSize"] = stepSize + properties["orientation"] = orientation + } +) { + steppedDraggable( + rememberSteppedDraggableState( + stepSize = stepSize, + onStep = { direction -> + when (direction) { + StepDirection.FORWARD -> controller.increaseLevel() + StepDirection.BACKWARD -> controller.decreaseLevel() + } + }, + ), + orientation = orientation, + ) + +} \ No newline at end of file diff --git a/app/shared/video-player/commonMain/ui/guesture/VideoGestureHost.kt b/app/shared/video-player/commonMain/ui/guesture/VideoGestureHost.kt index 15c1b56a81..5bb02fc5b6 100644 --- a/app/shared/video-player/commonMain/ui/guesture/VideoGestureHost.kt +++ b/app/shared/video-player/commonMain/ui/guesture/VideoGestureHost.kt @@ -22,14 +22,20 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow +import androidx.compose.ui.unit.coerceAtLeast import androidx.compose.ui.unit.dp +import me.him188.ani.app.platform.LocalContext +import me.him188.ani.app.platform.StreamType +import me.him188.ani.app.platform.getComponentAccessors import me.him188.ani.app.ui.theme.aniDarkColorTheme import me.him188.ani.app.ui.theme.slightlyWeaken import me.him188.ani.app.videoplayer.ui.guesture.SwipeSeekerState.Companion.swipeToSeek @@ -97,10 +103,11 @@ fun SeekPositionIndicator( @Composable fun VideoGestureHost( seekerState: SwipeSeekerState, - onClickScreen: () -> Unit, - onDoubleClickScreen: () -> Unit, + onClickScreen: () -> Unit = {}, + onDoubleClickScreen: () -> Unit = {}, modifier: Modifier = Modifier, ) { + BoxWithConstraints { Row(Modifier.align(Alignment.TopCenter).padding(top = 80.dp)) { AnimatedVisibility( @@ -111,8 +118,17 @@ fun VideoGestureHost( SeekPositionIndicator(seekerState.deltaSeconds) } } + val maxHeight = maxHeight - Row( + + val context by rememberUpdatedState(LocalContext.current) + val audioController by remember { + derivedStateOf { + getComponentAccessors(context = context).audioManager?.asLevelController(StreamType.MUSIC) + } + } + + Box( modifier .combinedClickable( remember { MutableInteractionSource() }, @@ -120,20 +136,18 @@ fun VideoGestureHost( onClick = onClickScreen, onDoubleClick = onDoubleClickScreen, ) + .then( + audioController?.let { + Modifier.swipeLevelControl( + it, + ((maxHeight - 100.dp) / 20).coerceAtLeast(4.dp), + Orientation.Vertical + ) + } ?: Modifier + ) .swipeToSeek(seekerState, Orientation.Horizontal) .fillMaxSize() - ) { - Box( - Modifier.weight(1f) -// .draggable(state, Orientation.Vertical) - ) - - Box(Modifier.weight(1f)) - - Box( - Modifier.weight(1f) - ) - } + ) } }