From 1f2b8123ea811939c17fc7722f4f4401be247d8d Mon Sep 17 00:00:00 2001 From: Syer10 Date: Sat, 31 Dec 2022 14:28:24 -0500 Subject: [PATCH] Improve hotkey handling, add support for volume buttons and spacebar. Closes #60 --- .../ca/gosyer/jui/android/ReaderActivity.kt | 27 -------- .../ca/gosyer/jui/ui/reader/ReaderMenu.kt | 61 +++++++++++-------- .../jui/ui/reader/ReaderMenuViewModel.kt | 11 +++- .../gosyer/jui/ui/reader/model/Navigation.kt | 2 + .../gosyer/jui/ui/reader/DesktopReaderMenu.kt | 20 ------ .../ca/gosyer/jui/ui/reader/IosReaderMenu.kt | 3 - 6 files changed, 49 insertions(+), 75 deletions(-) diff --git a/android/src/main/kotlin/ca/gosyer/jui/android/ReaderActivity.kt b/android/src/main/kotlin/ca/gosyer/jui/android/ReaderActivity.kt index 2735b144..ed3b7177 100644 --- a/android/src/main/kotlin/ca/gosyer/jui/android/ReaderActivity.kt +++ b/android/src/main/kotlin/ca/gosyer/jui/android/ReaderActivity.kt @@ -12,16 +12,8 @@ import android.os.Bundle import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.input.key.KeyEvent -import androidx.compose.ui.input.key.key -import androidx.lifecycle.lifecycleScope -import ca.gosyer.jui.ui.base.model.StableHolder import ca.gosyer.jui.ui.base.theme.AppTheme import ca.gosyer.jui.ui.reader.ReaderMenu -import ca.gosyer.jui.ui.reader.supportedKeyList -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.launch class ReaderActivity : AppCompatActivity() { @@ -35,9 +27,6 @@ class ReaderActivity : AppCompatActivity() { } } - private val hotkeyFlow = MutableSharedFlow() - private val hotkeyFlowHolder = StableHolder(hotkeyFlow.asSharedFlow()) - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val hooks = AppComponent.getInstance(applicationContext).hooks @@ -55,26 +44,10 @@ class ReaderActivity : AppCompatActivity() { ReaderMenu( chapterIndex = chapterIndex, mangaId = mangaId, - hotkeyFlowHolder = hotkeyFlowHolder, onCloseRequest = onBackPressedDispatcher::onBackPressed ) } } } } - - override fun onKeyUp(keyCode: Int, event: android.view.KeyEvent?): Boolean { - @Suppress("KotlinConstantConditions") - event ?: return super.onKeyUp(keyCode, event) - val composeKeyEvent = KeyEvent(event) - lifecycleScope.launch { - hotkeyFlow.emit(composeKeyEvent) - } - - return if (composeKeyEvent.key in supportedKeyList) { - true - } else { - super.onKeyUp(keyCode, event) - } - } } diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ReaderMenu.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ReaderMenu.kt index 61ed5f66..e544c598 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ReaderMenu.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ReaderMenu.kt @@ -17,6 +17,7 @@ import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.Image import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints @@ -44,15 +45,22 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.input.key.Key -import androidx.compose.ui.input.key.KeyEvent +import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.input.key.type import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp @@ -86,20 +94,8 @@ import ca.gosyer.jui.uicore.resources.stringResource import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -val supportedKeyList = listOf( - Key.W, - Key.DirectionUp, - Key.S, - Key.DirectionDown, - Key.A, - Key.DirectionLeft, - Key.D, - Key.DirectionRight -) - expect class ReaderLauncher { fun launch( chapterIndex: Int, @@ -117,7 +113,6 @@ expect fun rememberReaderLauncher(): ReaderLauncher fun ReaderMenu( chapterIndex: Int, mangaId: Long, - hotkeyFlowHolder: StableHolder>, onCloseRequest: () -> Unit ) { val viewModels = LocalViewModels.current @@ -147,18 +142,29 @@ fun ReaderMenu( val currentPageOffset by vm.currentPageOffset.collectAsState() val readerSettingsMenuOpen by vm.readerSettingsMenuOpen.collectAsState() - LaunchedEffect(hotkeyFlowHolder) { - hotkeyFlowHolder.item.collectLatest { - when (it.key) { - Key.W, Key.DirectionUp -> vm.navigate(Navigation.PREV) - Key.S, Key.DirectionDown -> vm.navigate(Navigation.NEXT) - Key.A, Key.DirectionLeft -> vm.navigate(Navigation.LEFT) - Key.D, Key.DirectionRight -> vm.navigate(Navigation.RIGHT) + val focusRequester = remember { FocusRequester() } + var hasFocus by remember { mutableStateOf(false) } + Surface( + Modifier + .focusRequester(focusRequester) + .onFocusChanged { + hasFocus = it.hasFocus } + .focusable() + .onKeyEvent { + if (it.type != KeyEventType.KeyDown) return@onKeyEvent false + when (it.key) { + Key.W, Key.DirectionUp -> vm.navigate(Navigation.PREV) + Key.S, Key.DirectionDown -> vm.navigate(Navigation.NEXT) + Key.A, Key.DirectionLeft -> vm.navigate(Navigation.LEFT) + Key.D, Key.DirectionRight -> vm.navigate(Navigation.RIGHT) + Key.VolumeDown -> vm.navigate(Navigation.DOWN) + Key.VolumeUp -> vm.navigate(Navigation.UP) + Key.Spacebar -> vm.navigate(Navigation.NEXT) + else -> false + } } - } - - Surface { + ) { Crossfade(state to chapter) { (state, chapter) -> if (state is ReaderChapter.State.Loaded && chapter != null) { if (pages.isNotEmpty()) { @@ -233,6 +239,13 @@ fun ReaderMenu( } } } + + + LaunchedEffect(hasFocus) { + if (!hasFocus) { + focusRequester.requestFocus() + } + } } @Composable diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ReaderMenuViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ReaderMenuViewModel.kt index 5da33bd7..10961e01 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ReaderMenuViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ReaderMenuViewModel.kt @@ -148,7 +148,7 @@ class ReaderMenuViewModel @Inject constructor( } } - fun navigate(navigationRegion: Navigation) { + fun navigate(navigationRegion: Navigation): Boolean { scope.launch { val moveTo = when (navigationRegion) { Navigation.MENU -> { @@ -165,11 +165,20 @@ class ReaderMenuViewModel @Inject constructor( Direction.Left -> MoveTo.Next else -> MoveTo.Previous } + Navigation.DOWN -> when (readerModeSettings.direction.value) { + Direction.Up -> MoveTo.Previous + else -> MoveTo.Next + } + Navigation.UP -> when (readerModeSettings.direction.value) { + Direction.Up -> MoveTo.Next + else -> MoveTo.Previous + } } if (moveTo != null) { _pageEmitter.emit(PageMove.Direction(moveTo)) } } + return true } fun navigate(page: Int) { diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/model/Navigation.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/model/Navigation.kt index f55af788..c67a8299 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/model/Navigation.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/model/Navigation.kt @@ -12,4 +12,6 @@ sealed class Navigation(val name: String) { object NEXT : Navigation("Next") object LEFT : Navigation("Left") object RIGHT : Navigation("Right") + object UP : Navigation("Up") + object DOWN : Navigation("Down") } diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/reader/DesktopReaderMenu.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/reader/DesktopReaderMenu.kt index ee4167ed..4f9d121f 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/reader/DesktopReaderMenu.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/reader/DesktopReaderMenu.kt @@ -13,24 +13,15 @@ import androidx.compose.runtime.currentCompositionLocalContext import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment -import androidx.compose.ui.input.key.KeyEvent -import androidx.compose.ui.input.key.KeyEventType -import androidx.compose.ui.input.key.key -import androidx.compose.ui.input.key.type import androidx.compose.ui.res.painterResource import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.rememberWindowState import ca.gosyer.jui.presentation.build.BuildKonfig -import ca.gosyer.jui.ui.base.model.StableHolder import ca.gosyer.jui.ui.util.lang.launchApplication import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.launch actual class ReaderLauncher { @@ -50,9 +41,6 @@ actual class ReaderLauncher { DisposableEffect(isOpen) { isOpen?.let { (chapterIndex, mangaId) -> launchApplication { - val scope = rememberCoroutineScope() - val hotkeyFlow = remember { MutableSharedFlow() } - val hotkeyFlowHolder = remember { StableHolder(hotkeyFlow.asSharedFlow()) } val windowState = rememberWindowState( position = WindowPosition.Aligned(Alignment.Center) ) @@ -63,18 +51,10 @@ actual class ReaderLauncher { title = "${BuildKonfig.NAME} - Reader", icon = icon, state = windowState, - onKeyEvent = { - if (it.type != KeyEventType.KeyDown) return@Window false - scope.launch { - hotkeyFlow.emit(it) - } - it.key in supportedKeyList - } ) { ReaderMenu( chapterIndex = chapterIndex, mangaId = mangaId, - hotkeyFlowHolder = hotkeyFlowHolder, onCloseRequest = ::exitApplication ) } diff --git a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/reader/IosReaderMenu.kt b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/reader/IosReaderMenu.kt index c14f75c1..fce851af 100644 --- a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/reader/IosReaderMenu.kt +++ b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/reader/IosReaderMenu.kt @@ -8,12 +8,10 @@ package ca.gosyer.jui.ui.reader import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import ca.gosyer.jui.ui.base.model.StableHolder import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.currentOrThrow -import kotlinx.coroutines.flow.MutableSharedFlow class ReaderScreen(val chapterIndex: Int, val mangaId: Long) : Screen { @@ -23,7 +21,6 @@ class ReaderScreen(val chapterIndex: Int, val mangaId: Long) : Screen { ReaderMenu( chapterIndex, mangaId, - remember { StableHolder(MutableSharedFlow()) }, navigator::pop ) }