Skip to content

Commit

Permalink
refactor(controls): re-organize project composables
Browse files Browse the repository at this point in the history
  • Loading branch information
abdallahmehiz committed Jun 16, 2024
1 parent ef92e16 commit da79b2b
Show file tree
Hide file tree
Showing 15 changed files with 548 additions and 375 deletions.
24 changes: 24 additions & 0 deletions app/src/main/java/live/mehiz/mpvkt/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,39 @@ package live.mehiz.mpvkt

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.getValue
import androidx.compose.runtime.internal.enableLiveLiterals
import cafe.adriel.voyager.navigator.Navigator
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.take
import live.mehiz.mpvkt.preferences.AppearancePreferences
import live.mehiz.mpvkt.preferences.preference.collectAsState
import live.mehiz.mpvkt.ui.home.HomeScreen
import live.mehiz.mpvkt.ui.theme.DarkMode
import live.mehiz.mpvkt.ui.theme.MpvKtTheme
import org.koin.android.ext.android.inject
import org.koin.androidx.scope.createScope
import org.koin.compose.koinInject
import org.koin.java.KoinJavaComponent.inject

class MainActivity : ComponentActivity() {
val appearancePreferences by inject<AppearancePreferences>()
override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)
setContent {
val dark by appearancePreferences.darkMode.collectAsState()
val isSystemInDarkTheme = isSystemInDarkTheme()
enableEdgeToEdge(
SystemBarStyle.auto(
0xFFF,
0xFFF
) { dark == DarkMode.Dark || (dark == DarkMode.System && isSystemInDarkTheme) },
)
MpvKtTheme {
// TODO: add transitions back once these two issues get fixed, thanks Google, very cool!
// https://github.com/adrielcafe/voyager/issues/410
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ class PlayerViewModel(
private val _areControlsLocked = MutableStateFlow(false)
val areControlsLocked = _areControlsLocked.asStateFlow()

val sheetShown = MutableStateFlow(Sheets.None)
val gestureSeekAmount = MutableStateFlow(0)

fun getDecoder() {
_currentDecoder.update { getDecoderFromValue(activity.player.hwdecActive) }
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package live.mehiz.mpvkt.ui.player.controls

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Lock
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import kotlinx.coroutines.flow.update
import live.mehiz.mpvkt.preferences.PlayerPreferences
import live.mehiz.mpvkt.ui.player.PlayerViewModel
import live.mehiz.mpvkt.ui.player.Sheets
import live.mehiz.mpvkt.ui.player.controls.components.ControlsButton
import live.mehiz.mpvkt.ui.player.controls.components.CurrentChapter
import org.koin.compose.koinInject

@Composable
fun BottomLeftPlayerControls(viewModel: PlayerViewModel) {
val playerPreferences = koinInject<PlayerPreferences>()
val currentChapter by viewModel.currentChapter.collectAsState()
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
ControlsButton(
Icons.Default.Lock,
onClick = { viewModel.lockControls() },
)
AnimatedVisibility(
currentChapter != null && playerPreferences.currentChaptersIndicator.get(),
enter = fadeIn(),
exit = fadeOut(),
) {
CurrentChapter(
currentChapter!!,
onClick = { viewModel.sheetShown.update { Sheets.Chapters } },
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package live.mehiz.mpvkt.ui.player.controls

import androidx.compose.foundation.layout.Row
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AspectRatio
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import live.mehiz.mpvkt.preferences.PlayerPreferences
import live.mehiz.mpvkt.preferences.preference.collectAsState
import live.mehiz.mpvkt.ui.player.PlayerViewModel
import live.mehiz.mpvkt.ui.player.VideoAspect
import live.mehiz.mpvkt.ui.player.controls.components.ControlsButton
import org.koin.compose.koinInject

@Composable
fun BottomRightPlayerControls(viewModel: PlayerViewModel) {
val playerPreferences = koinInject<PlayerPreferences>()
val aspect by playerPreferences.videoAspect.collectAsState()
Row {
ControlsButton(
Icons.Default.AspectRatio,
onClick = {
when (aspect) {
VideoAspect.Fit -> viewModel.changeVideoAspect(VideoAspect.Stretch)
VideoAspect.Stretch -> viewModel.changeVideoAspect(VideoAspect.Crop)
VideoAspect.Crop -> viewModel.changeVideoAspect(VideoAspect.Fit)
}
},
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
package live.mehiz.mpvkt.ui.player.controls

import android.view.View
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.detectVerticalDragGestures
import androidx.compose.foundation.indication
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
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.ripple.LocalRippleTheme
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
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.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.viewinterop.AndroidView
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.update
import live.mehiz.mpvkt.preferences.PlayerPreferences
import live.mehiz.mpvkt.preferences.preference.collectAsState
import live.mehiz.mpvkt.presentation.LeftSideOvalShape
import live.mehiz.mpvkt.presentation.RightSideOvalShape
import live.mehiz.mpvkt.ui.player.PlayerViewModel
import live.mehiz.mpvkt.ui.player.controls.components.DoubleTapSecondsView
import live.mehiz.mpvkt.ui.theme.PlayerRippleTheme
import org.koin.compose.koinInject

@Composable
fun GestureHandler(
viewModel: PlayerViewModel,
) {
val playerPreferences = koinInject<PlayerPreferences>()
val duration by viewModel.duration.collectAsState()
val position by viewModel.pos.collectAsState()
val controlsShown by viewModel.controlsShown.collectAsState()
val areControlsLocked by viewModel.areControlsLocked.collectAsState()
var seekAmount by remember { mutableIntStateOf(0) }
var isSeekingForwards by remember { mutableStateOf(true) }
var targetAlpha by remember { mutableFloatStateOf(0f) }
val alpha by animateFloatAsState(
targetAlpha,
animationSpec = tween(300),
label = "doubletapseekalpha",
)
LaunchedEffect(seekAmount) {
delay(600)
targetAlpha = 0f
delay(200)
seekAmount = 0
delay(100)
viewModel.hideSeekBar()
}
val interactionSource = remember { MutableInteractionSource() }
val doubleTapToPause by playerPreferences.doubleTapToPause.collectAsState()
val doubleTapToSeek by playerPreferences.doubleTapToSeek.collectAsState()
val doubleTapToSeekDuration by playerPreferences.doubleTapToSeekDuration.collectAsState()
val brightnessGesture = playerPreferences.brightnessGesture.get()
val volumeGesture = playerPreferences.volumeGesture.get()
val seekGesture = playerPreferences.horizontalSeekGesture.get()

val doubleTapSeek: (Offset, IntSize) -> Unit = { offset, size ->
targetAlpha = 0.2f
val isForward = offset.x > 3 * size.width / 5
if (isSeekingForwards != isForward) seekAmount = 0
isSeekingForwards = isForward
if (!((duration <= position && isSeekingForwards) || (position <= 0f && !isSeekingForwards))) {
seekAmount += if (isSeekingForwards) doubleTapToSeekDuration else -doubleTapToSeekDuration
}
viewModel.seekBy(if (isSeekingForwards) doubleTapToSeekDuration else -doubleTapToSeekDuration)
viewModel.showSeekBar()
}
Box(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectTapGestures(
onTap = {
if (controlsShown) viewModel.hideControls()
else viewModel.showControls()
},
onDoubleTap = {
if (areControlsLocked) return@detectTapGestures
if (!doubleTapToSeek && doubleTapToPause) {
viewModel.pauseUnpause()
return@detectTapGestures
}
// divided by 2 fifths
if (doubleTapToSeek && (it.x > size.width * 3 / 5 || it.x < size.width * 2 / 5)) {
doubleTapSeek(it, size)
} else if (doubleTapToPause) {
viewModel.pauseUnpause()
}
},
onPress = {
val press = PressInteraction.Press(
it.copy(x = if (it.x > size.width * 3 / 5) it.x - size.width * 0.6f else it.x),
)
interactionSource.emit(press)
tryAwaitRelease()
interactionSource.emit(PressInteraction.Release(press))
},
)
}
.pointerInput(Unit) {
if (!seekGesture || areControlsLocked) return@pointerInput
var startingPosition = position
detectHorizontalDragGestures(
onDragStart = {
startingPosition = position
viewModel.pause()
},
onDragEnd = {
viewModel.gestureSeekAmount.update { 0 }
viewModel.unpause()
},
) { change, dragAmount ->
if ((position >= duration && dragAmount > 0) || (position <= 0f && dragAmount < 0)) {
return@detectHorizontalDragGestures
}
viewModel.showSeekBar()
val seekBy = ((dragAmount * 150f / size.width).coerceIn(
0f - position,
duration - position,
)).toInt()
viewModel.seekBy(seekBy)
viewModel.gestureSeekAmount.update { (position - startingPosition).toInt() }
}
}
.pointerInput(Unit) {
if ((!brightnessGesture && !volumeGesture) || areControlsLocked) return@pointerInput
var dragAmount = 0f
detectVerticalDragGestures(
onDragEnd = { dragAmount = 0f },
) { change, amount ->
dragAmount -= amount / 10
when {
volumeGesture && brightnessGesture -> {
if (change.position.x < size.width / 2) viewModel.changeBrightnessWithDrag(dragAmount)
else viewModel.changeVolumeWithDrag(dragAmount)
}

brightnessGesture -> {
viewModel.changeBrightnessWithDrag(dragAmount)
}
// it's not always true, AS is drunk
volumeGesture -> {
viewModel.changeVolumeWithDrag(dragAmount)
}

else -> {}
}
}
},
contentAlignment = if (isSeekingForwards) Alignment.CenterEnd else Alignment.CenterStart,
) {
CompositionLocalProvider(
LocalRippleTheme provides PlayerRippleTheme,
) {
if (seekAmount != 0) {
Box(
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth(0.4f), // 2 fifths
contentAlignment = Alignment.Center,
) {
Box(
modifier = Modifier
.fillMaxSize()
.clip(if (isSeekingForwards) RightSideOvalShape else LeftSideOvalShape)
.background(Color.White.copy(alpha))
.indication(interactionSource, rememberRipple()),
)
AndroidView(
factory = { DoubleTapSecondsView(it, null) },
update = {
if (seekAmount != 0) {
it.isForward = isSeekingForwards
it.seconds = seekAmount
it.visibility = View.VISIBLE
it.start()
} else {
it.visibility = View.GONE
}
},
)
}
}
}
}
}
Loading

0 comments on commit da79b2b

Please sign in to comment.