Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.screen.SearchableSettings
import eu.kanade.tachiyomi.ui.player.PausedLongPressAction
import eu.kanade.tachiyomi.ui.player.SingleActionGesture
import eu.kanade.tachiyomi.ui.player.settings.GesturePreferences
import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableList
Expand All @@ -40,12 +42,43 @@ object PlayerSettingsGesturesScreen : SearchableSettings {
@Composable
override fun getPreferences(): List<Preference> {
val gesturePreferences = remember { Injekt.get<GesturePreferences>() }
val playerPreferences = remember { Injekt.get<PlayerPreferences>() }

return listOf(
getSlidersGroup(gesturePreferences = gesturePreferences),
getSeekingGroup(gesturePreferences = gesturePreferences),
getDoubleTapGroup(gesturePreferences = gesturePreferences),
getMediaControlsGroup(gesturePreferences = gesturePreferences),
getLongPressGroup(gesturePreferences = gesturePreferences, playerPreferences = playerPreferences),
)
}

@Composable
private fun getLongPressGroup(
gesturePreferences: GesturePreferences,
playerPreferences: PlayerPreferences,
): Preference.PreferenceGroup {
val pausedLongPress = gesturePreferences.pausedLongPressGesture()
val adjustSpeedOnDrag = playerPreferences.adjustSpeedOnDrag()

return Preference.PreferenceGroup(
title = stringResource(MR.strings.paused_long_press_action),
preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = pausedLongPress,
title = stringResource(MR.strings.paused_long_press_action),
entries = listOf(
PausedLongPressAction.DoNothing,
PausedLongPressAction.Screenshot,
PausedLongPressAction.Play2x,
).associateWith { stringResource(it.stringRes) }.toPersistentMap(),
),
Preference.PreferenceItem.SwitchPreference(
pref = adjustSpeedOnDrag,
title = stringResource(MR.strings.pref_adjust_speed_on_drag),
subtitle = stringResource(MR.strings.pref_adjust_speed_on_drag_summary),
),
),
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ enum class Sheets {
Screenshot,
}

enum class PausedLongPressAction(val stringRes: StringResource) {
DoNothing(MR.strings.paused_long_press_action_do_nothing),
Screenshot(MR.strings.paused_long_press_action_screenshot),
Play2x(MR.strings.paused_long_press_action_play2x),
}

enum class Panels {
None,
SubtitleSettings,
Expand All @@ -125,7 +131,7 @@ sealed class Dialogs {

sealed class PlayerUpdates {
data object None : PlayerUpdates()
data object DoubleSpeed : PlayerUpdates()
data class DoubleSpeed(val speed: Float, val isDragging: Boolean) : PlayerUpdates()
data object AspectRatio : PlayerUpdates()
data class ShowText(val value: String) : PlayerUpdates()
data class ShowTextResource(val textResource: StringResource) : PlayerUpdates()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package eu.kanade.tachiyomi.ui.player.controls

import androidx.compose.animation.core.animateFloatAsState
import kotlin.math.abs
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.gestures.detectTapGestures
Expand Down Expand Up @@ -57,6 +58,7 @@
import eu.kanade.presentation.player.components.RightSideOvalShape
import eu.kanade.presentation.theme.playerRippleConfiguration
import eu.kanade.tachiyomi.ui.player.Panels
import eu.kanade.tachiyomi.ui.player.PausedLongPressAction
import eu.kanade.tachiyomi.ui.player.PlayerUpdates
import eu.kanade.tachiyomi.ui.player.PlayerViewModel
import eu.kanade.tachiyomi.ui.player.Sheets
Expand All @@ -74,232 +76,293 @@
import uy.kohesive.injekt.api.get

@Composable
fun GestureHandler(
viewModel: PlayerViewModel,
interactionSource: MutableInteractionSource,
modifier: Modifier = Modifier,
) {
val playerPreferences = remember { Injekt.get<PlayerPreferences>() }
val gesturePreferences = remember { Injekt.get<GesturePreferences>() }
val audioPreferences = remember { Injekt.get<AudioPreferences>() }

val panelShown by viewModel.panelShown.collectAsState()
val allowGesturesInPanels by playerPreferences.allowGestures().collectAsState()
val duration by viewModel.duration.collectAsState()
val position by viewModel.pos.collectAsState()
val controlsShown by viewModel.controlsShown.collectAsState()
val areControlsLocked by viewModel.areControlsLocked.collectAsState()
val seekAmount by viewModel.doubleTapSeekAmount.collectAsState()
val isSeekingForwards by viewModel.isSeekingForwards.collectAsState()
var isDoubleTapSeeking by remember { mutableStateOf(false) }

LaunchedEffect(seekAmount) {
delay(800)
isDoubleTapSeeking = false
viewModel.updateSeekAmount(0)
viewModel.updateSeekText(null)
delay(100)
viewModel.hideSeekBar()
}

val gestureVolumeBrightness = gesturePreferences.gestureVolumeBrightness().get()
val swapVolumeBrightness by gesturePreferences.swapVolumeBrightness().collectAsState()
val seekGesture by gesturePreferences.gestureHorizontalSeek().collectAsState()
val preciseSeeking by gesturePreferences.playerSmoothSeek().collectAsState()
val showSeekbar by gesturePreferences.showSeekBar().collectAsState()
val pausedLongPressAction by gesturePreferences.pausedLongPressGesture().collectAsState()
val adjustSpeedOnDrag by playerPreferences.adjustSpeedOnDrag().collectAsState()
var isLongPressing by remember { mutableStateOf(false) }
var originalSpeed by remember { mutableStateOf(1f) }
val currentVolume by viewModel.currentVolume.collectAsState()
val currentMPVVolume by viewModel.currentMPVVolume.collectAsState()
val currentBrightness by viewModel.currentBrightness.collectAsState()
val volumeBoostingCap = audioPreferences.volumeBoostCap().get()
val haptics = LocalHapticFeedback.current

Box(
modifier = modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.safeGestures)
.pointerInput(Unit) {
val originalSpeed = viewModel.playbackSpeed.value
.pointerInput(areControlsLocked, pausedLongPressAction) {
originalSpeed = viewModel.playbackSpeed.value
detectTapGestures(
onTap = {
if (controlsShown) viewModel.hideControls() else viewModel.showControls()
},
onDoubleTap = {
if (areControlsLocked || isDoubleTapSeeking) return@detectTapGestures
if (it.x > size.width * 3 / 5) {
if (!isSeekingForwards) viewModel.updateSeekAmount(0)
viewModel.handleRightDoubleTap()
isDoubleTapSeeking = true
} else if (it.x < size.width * 2 / 5) {
if (isSeekingForwards) viewModel.updateSeekAmount(0)
viewModel.handleLeftDoubleTap()
isDoubleTapSeeking = true
} else {
viewModel.handleCenterDoubleTap()
}
},
onPress = {
if (panelShown != Panels.None && !allowGesturesInPanels) {
viewModel.panelShown.update { Panels.None }
}
val press = PressInteraction.Press(
it.copy(x = if (it.x > size.width * 3 / 5) it.x - size.width * 0.6f else it.x),
)
if (!areControlsLocked && isDoubleTapSeeking && seekAmount != 0) {
if (it.x > size.width * 3 / 5) {
if (!isSeekingForwards) viewModel.updateSeekAmount(0)
viewModel.handleRightDoubleTap()
} else if (it.x < size.width * 2 / 5) {
if (isSeekingForwards) viewModel.updateSeekAmount(0)
viewModel.handleLeftDoubleTap()
} else {
viewModel.handleCenterDoubleTap()
}
} else {
isDoubleTapSeeking = false
}
interactionSource.emit(press)
tryAwaitRelease()
if (isLongPressing) {
val released = tryAwaitRelease()
if (isLongPressing && released) {
isLongPressing = false
MPVLib.setPropertyDouble("speed", originalSpeed.toDouble())
viewModel.playerUpdate.update { PlayerUpdates.None }
}
interactionSource.emit(PressInteraction.Release(press))
},
onLongPress = {
if (areControlsLocked) return@detectTapGestures
if (!isLongPressing) {
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
isLongPressing = true
viewModel.pause()
viewModel.sheetShown.update { Sheets.Screenshot }
if (viewModel.paused.value) {
when (pausedLongPressAction) {
PausedLongPressAction.Screenshot -> {
viewModel.pause()
viewModel.sheetShown.update { Sheets.Screenshot }
}
PausedLongPressAction.Play2x -> {
originalSpeed = MPVLib.getPropertyDouble("speed").toFloat()
MPVLib.setPropertyDouble("speed", 2.0)
viewModel.playerUpdate.update { PlayerUpdates.DoubleSpeed(2.0f, isDragging = false) }
viewModel.unpause()
}
PausedLongPressAction.DoNothing -> {}
}
} else {
originalSpeed = MPVLib.getPropertyDouble("speed").toFloat()
MPVLib.setPropertyDouble("speed", 2.0)
viewModel.playerUpdate.update { PlayerUpdates.DoubleSpeed(2.0f, isDragging = false) }
}
}
},
)
}
.pointerInput(areControlsLocked) {
.pointerInput(areControlsLocked, seekGesture) {
if (!seekGesture || areControlsLocked) return@pointerInput
var startingPosition = position.toInt()
var startingX = 0f
var wasPlayerAlreadyPause = false
detectHorizontalDragGestures(
onDragStart = {
startingPosition = position.toInt()
startingX = it.x
wasPlayerAlreadyPause = viewModel.paused.value
viewModel.pause()
},
onDragEnd = {
viewModel.gestureSeekAmount.update { null }
viewModel.hideSeekBar()
if (!wasPlayerAlreadyPause) viewModel.unpause()
},
onDragCancel = {
viewModel.gestureSeekAmount.update { null }
viewModel.hideSeekBar()
if (!wasPlayerAlreadyPause) viewModel.unpause()
},
) { change, dragAmount ->
if (position <= 0f && dragAmount < 0) return@detectHorizontalDragGestures
if (position >= duration && dragAmount > 0) return@detectHorizontalDragGestures
calculateNewHorizontalGestureValue(startingPosition, startingX, change.position.x, 0.15f).let {
viewModel.gestureSeekAmount.update { _ ->
Pair(
startingPosition,
(it - startingPosition)
.coerceIn(0 - startingPosition, (duration - startingPosition).toInt()),
)
}
viewModel.seekTo(it.coerceIn(0, duration.toInt()), preciseSeeking)
}

if (showSeekbar) viewModel.showSeekBar()
}
}
.pointerInput(areControlsLocked, adjustSpeedOnDrag) {
if (!adjustSpeedOnDrag || areControlsLocked) return@pointerInput
awaitPointerEventScope {
var currentDragSpeed = 2.0f
var lastHapticSpeed = 2.0f
var wasLongPressing = false

while (true) {
val event = awaitPointerEvent(androidx.compose.ui.input.pointer.PointerEventPass.Main)
val isCurrentlyLongPressing = isLongPressing

if (isCurrentlyLongPressing && !wasLongPressing) {
currentDragSpeed = 2.0f
lastHapticSpeed = 2.0f
}

if (isCurrentlyLongPressing && event.changes.any { it.pressed }) {
val change = event.changes.first { it.pressed }
val dragAmount = change.position.x - change.previousPosition.x

if (dragAmount != 0f) {
currentDragSpeed = (currentDragSpeed + dragAmount * 0.005f).coerceIn(0.5f, 4.0f)
val snappedSpeed = (Math.round(currentDragSpeed * 2.0f) / 2.0f).coerceIn(0.5f, 4.0f)
if (abs(snappedSpeed - lastHapticSpeed) >= 0.5f) {
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
lastHapticSpeed = snappedSpeed
}
MPVLib.setPropertyDouble("speed", snappedSpeed.toDouble())
viewModel.playerUpdate.update { PlayerUpdates.DoubleSpeed(snappedSpeed, isDragging = true) }
}
}

wasLongPressing = isCurrentlyLongPressing
}
}
}
.pointerInput(areControlsLocked) {
if (!gestureVolumeBrightness || areControlsLocked) return@pointerInput
var startingY = 0f
var mpvVolumeStartingY = 0f
var originalVolume = currentVolume
var originalMPVVolume = currentMPVVolume
var originalBrightness = currentBrightness
val brightnessGestureSens = 0.001f
val volumeGestureSens = 0.03f
val mpvVolumeGestureSens = 0.02f
val isIncreasingVolumeBoost: (Float) -> Boolean = {
volumeBoostingCap > 0 &&
currentVolume == viewModel.maxVolume &&
currentMPVVolume - 100 < volumeBoostingCap &&
it < 0
}
val isDecreasingVolumeBoost: (Float) -> Boolean = {
volumeBoostingCap > 0 &&
currentVolume == viewModel.maxVolume &&
currentMPVVolume - 100 in 1..volumeBoostingCap &&
it > 0
}
detectVerticalDragGestures(
onDragEnd = { startingY = 0f },
onDragStart = {
startingY = 0f
mpvVolumeStartingY = 0f
originalVolume = currentVolume
originalMPVVolume = currentMPVVolume
originalBrightness = currentBrightness
},
) { change, amount ->
val changeVolume: () -> Unit = {
if (isIncreasingVolumeBoost(amount) || isDecreasingVolumeBoost(amount)) {
if (mpvVolumeStartingY == 0f) {
startingY = 0f
originalVolume = currentVolume
mpvVolumeStartingY = change.position.y
}
viewModel.changeMPVVolumeTo(
calculateNewVerticalGestureValue(
originalMPVVolume,
mpvVolumeStartingY,
change.position.y,
mpvVolumeGestureSens,
)
.coerceIn(100..volumeBoostingCap + 100),
)
} else {
if (startingY == 0f) {
mpvVolumeStartingY = 0f
originalMPVVolume = currentMPVVolume
startingY = change.position.y
}
viewModel.changeVolumeTo(
calculateNewVerticalGestureValue(
originalVolume,
startingY,
change.position.y,
volumeGestureSens,
),
)
}
viewModel.displayVolumeSlider()
}
val changeBrightness: () -> Unit = {
if (startingY == 0f) startingY = change.position.y
viewModel.changeBrightnessTo(
calculateNewVerticalGestureValue(
originalBrightness,
startingY,
change.position.y,
brightnessGestureSens,
),
)
viewModel.displayBrightnessSlider()
}
if (swapVolumeBrightness) {
if (change.position.x > size.width / 2) changeBrightness() else changeVolume()
} else {
if (change.position.x < size.width / 2) changeBrightness() else changeVolume()
}
}
},
)
}

@Composable

Check warning on line 365 in app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/GestureHandler.kt

View check run for this annotation

codefactor.io / CodeFactor

app/src/main/java/eu/kanade/tachiyomi/ui/player/controls/GestureHandler.kt#L79-L365

Very Complex Method
fun DoubleTapToSeekOvals(
amount: Int,
text: String?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
import eu.kanade.presentation.more.settings.screen.player.custombutton.getButtons
Expand All @@ -70,6 +71,7 @@ import eu.kanade.tachiyomi.ui.player.controls.components.BrightnessOverlay
import eu.kanade.tachiyomi.ui.player.controls.components.BrightnessSlider
import eu.kanade.tachiyomi.ui.player.controls.components.ControlsButton
import eu.kanade.tachiyomi.ui.player.controls.components.SeekbarWithTimers
import eu.kanade.tachiyomi.ui.player.controls.components.DoubleSpeedIndicator
import eu.kanade.tachiyomi.ui.player.controls.components.TextPlayerUpdate
import eu.kanade.tachiyomi.ui.player.controls.components.VolumeSlider
import eu.kanade.tachiyomi.ui.player.controls.components.sheets.toFixed
Expand Down Expand Up @@ -294,7 +296,7 @@ fun PlayerControls(
viewModel.playerUpdate.update { PlayerUpdates.None }
}
AnimatedVisibility(
currentPlayerUpdate !is PlayerUpdates.None,
currentPlayerUpdate !is PlayerUpdates.None && currentPlayerUpdate !is PlayerUpdates.DoubleSpeed,
enter = fadeIn(playerControlsEnterAnimationSpec()),
exit = fadeOut(playerControlsExitAnimationSpec()),
modifier = Modifier.constrainAs(playerUpdates) {
Expand All @@ -303,7 +305,6 @@ fun PlayerControls(
},
) {
when (currentPlayerUpdate) {
// is PlayerUpdates.DoubleSpeed -> DoubleSpeedPlayerUpdate()
is PlayerUpdates.AspectRatio -> TextPlayerUpdate(stringResource(aspectRatio.titleRes))
is PlayerUpdates.ShowText -> TextPlayerUpdate(
(currentPlayerUpdate as PlayerUpdates.ShowText).value,
Expand All @@ -315,6 +316,26 @@ fun PlayerControls(
}
}

val doubleSpeedIndicatorAnchor = createRef()
AnimatedVisibility(
visible = currentPlayerUpdate is PlayerUpdates.DoubleSpeed,
enter = fadeIn(playerControlsEnterAnimationSpec()),
exit = fadeOut(playerControlsExitAnimationSpec()),
modifier = Modifier.constrainAs(doubleSpeedIndicatorAnchor) {
top.linkTo(parent.top, margin = 48.dp)
start.linkTo(parent.start)
end.linkTo(parent.end)
},
) {
if (currentPlayerUpdate is PlayerUpdates.DoubleSpeed) {
val playerUpdate = currentPlayerUpdate as PlayerUpdates.DoubleSpeed
DoubleSpeedIndicator(
speed = playerUpdate.speed,
isDragging = playerUpdate.isDragging,
)
}
}

AnimatedVisibility(
controlsShown && areControlsLocked,
enter = fadeIn(),
Expand Down
Loading