Improve hotkey handling, add support for volume buttons and spacebar. Closes #60

This commit is contained in:
Syer10
2022-12-31 14:28:24 -05:00
parent 7ed64bd4b6
commit 1f2b8123ea
6 changed files with 49 additions and 75 deletions

View File

@@ -12,16 +12,8 @@ import android.os.Bundle
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.CompositionLocalProvider 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.base.theme.AppTheme
import ca.gosyer.jui.ui.reader.ReaderMenu 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() { class ReaderActivity : AppCompatActivity() {
@@ -35,9 +27,6 @@ class ReaderActivity : AppCompatActivity() {
} }
} }
private val hotkeyFlow = MutableSharedFlow<KeyEvent>()
private val hotkeyFlowHolder = StableHolder(hotkeyFlow.asSharedFlow())
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val hooks = AppComponent.getInstance(applicationContext).hooks val hooks = AppComponent.getInstance(applicationContext).hooks
@@ -55,26 +44,10 @@ class ReaderActivity : AppCompatActivity() {
ReaderMenu( ReaderMenu(
chapterIndex = chapterIndex, chapterIndex = chapterIndex,
mangaId = mangaId, mangaId = mangaId,
hotkeyFlowHolder = hotkeyFlowHolder,
onCloseRequest = onBackPressedDispatcher::onBackPressed 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)
}
}
} }

View File

@@ -17,6 +17,7 @@ import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.slideOutVertically import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.BoxWithConstraints
@@ -44,15 +45,22 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.graphics.FilterQuality
import androidx.compose.ui.input.key.Key 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.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.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp 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.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch 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 { expect class ReaderLauncher {
fun launch( fun launch(
chapterIndex: Int, chapterIndex: Int,
@@ -117,7 +113,6 @@ expect fun rememberReaderLauncher(): ReaderLauncher
fun ReaderMenu( fun ReaderMenu(
chapterIndex: Int, chapterIndex: Int,
mangaId: Long, mangaId: Long,
hotkeyFlowHolder: StableHolder<SharedFlow<KeyEvent>>,
onCloseRequest: () -> Unit onCloseRequest: () -> Unit
) { ) {
val viewModels = LocalViewModels.current val viewModels = LocalViewModels.current
@@ -147,18 +142,29 @@ fun ReaderMenu(
val currentPageOffset by vm.currentPageOffset.collectAsState() val currentPageOffset by vm.currentPageOffset.collectAsState()
val readerSettingsMenuOpen by vm.readerSettingsMenuOpen.collectAsState() val readerSettingsMenuOpen by vm.readerSettingsMenuOpen.collectAsState()
LaunchedEffect(hotkeyFlowHolder) { val focusRequester = remember { FocusRequester() }
hotkeyFlowHolder.item.collectLatest { var hasFocus by remember { mutableStateOf(false) }
when (it.key) { Surface(
Key.W, Key.DirectionUp -> vm.navigate(Navigation.PREV) Modifier
Key.S, Key.DirectionDown -> vm.navigate(Navigation.NEXT) .focusRequester(focusRequester)
Key.A, Key.DirectionLeft -> vm.navigate(Navigation.LEFT) .onFocusChanged {
Key.D, Key.DirectionRight -> vm.navigate(Navigation.RIGHT) 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) -> Crossfade(state to chapter) { (state, chapter) ->
if (state is ReaderChapter.State.Loaded && chapter != null) { if (state is ReaderChapter.State.Loaded && chapter != null) {
if (pages.isNotEmpty()) { if (pages.isNotEmpty()) {
@@ -233,6 +239,13 @@ fun ReaderMenu(
} }
} }
} }
LaunchedEffect(hasFocus) {
if (!hasFocus) {
focusRequester.requestFocus()
}
}
} }
@Composable @Composable

View File

@@ -148,7 +148,7 @@ class ReaderMenuViewModel @Inject constructor(
} }
} }
fun navigate(navigationRegion: Navigation) { fun navigate(navigationRegion: Navigation): Boolean {
scope.launch { scope.launch {
val moveTo = when (navigationRegion) { val moveTo = when (navigationRegion) {
Navigation.MENU -> { Navigation.MENU -> {
@@ -165,11 +165,20 @@ class ReaderMenuViewModel @Inject constructor(
Direction.Left -> MoveTo.Next Direction.Left -> MoveTo.Next
else -> MoveTo.Previous 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) { if (moveTo != null) {
_pageEmitter.emit(PageMove.Direction(moveTo)) _pageEmitter.emit(PageMove.Direction(moveTo))
} }
} }
return true
} }
fun navigate(page: Int) { fun navigate(page: Int) {

View File

@@ -12,4 +12,6 @@ sealed class Navigation(val name: String) {
object NEXT : Navigation("Next") object NEXT : Navigation("Next")
object LEFT : Navigation("Left") object LEFT : Navigation("Left")
object RIGHT : Navigation("Right") object RIGHT : Navigation("Right")
object UP : Navigation("Up")
object DOWN : Navigation("Down")
} }

View File

@@ -13,24 +13,15 @@ import androidx.compose.runtime.currentCompositionLocalContext
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment 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.res.painterResource
import androidx.compose.ui.window.Window import androidx.compose.ui.window.Window
import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.WindowPosition
import androidx.compose.ui.window.rememberWindowState import androidx.compose.ui.window.rememberWindowState
import ca.gosyer.jui.presentation.build.BuildKonfig import ca.gosyer.jui.presentation.build.BuildKonfig
import ca.gosyer.jui.ui.base.model.StableHolder
import ca.gosyer.jui.ui.util.lang.launchApplication import ca.gosyer.jui.ui.util.lang.launchApplication
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
actual class ReaderLauncher { actual class ReaderLauncher {
@@ -50,9 +41,6 @@ actual class ReaderLauncher {
DisposableEffect(isOpen) { DisposableEffect(isOpen) {
isOpen?.let { (chapterIndex, mangaId) -> isOpen?.let { (chapterIndex, mangaId) ->
launchApplication { launchApplication {
val scope = rememberCoroutineScope()
val hotkeyFlow = remember { MutableSharedFlow<KeyEvent>() }
val hotkeyFlowHolder = remember { StableHolder(hotkeyFlow.asSharedFlow()) }
val windowState = rememberWindowState( val windowState = rememberWindowState(
position = WindowPosition.Aligned(Alignment.Center) position = WindowPosition.Aligned(Alignment.Center)
) )
@@ -63,18 +51,10 @@ actual class ReaderLauncher {
title = "${BuildKonfig.NAME} - Reader", title = "${BuildKonfig.NAME} - Reader",
icon = icon, icon = icon,
state = windowState, state = windowState,
onKeyEvent = {
if (it.type != KeyEventType.KeyDown) return@Window false
scope.launch {
hotkeyFlow.emit(it)
}
it.key in supportedKeyList
}
) { ) {
ReaderMenu( ReaderMenu(
chapterIndex = chapterIndex, chapterIndex = chapterIndex,
mangaId = mangaId, mangaId = mangaId,
hotkeyFlowHolder = hotkeyFlowHolder,
onCloseRequest = ::exitApplication onCloseRequest = ::exitApplication
) )
} }

View File

@@ -8,12 +8,10 @@ package ca.gosyer.jui.ui.reader
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import ca.gosyer.jui.ui.base.model.StableHolder
import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import kotlinx.coroutines.flow.MutableSharedFlow
class ReaderScreen(val chapterIndex: Int, val mangaId: Long) : Screen { class ReaderScreen(val chapterIndex: Int, val mangaId: Long) : Screen {
@@ -23,7 +21,6 @@ class ReaderScreen(val chapterIndex: Int, val mangaId: Long) : Screen {
ReaderMenu( ReaderMenu(
chapterIndex, chapterIndex,
mangaId, mangaId,
remember { StableHolder(MutableSharedFlow()) },
navigator::pop navigator::pop
) )
} }