diff --git a/i18n/src/commonMain/resources/MR/values/base/strings.xml b/i18n/src/commonMain/resources/MR/values/base/strings.xml
index 3eed4ca0..7b94ba1b 100644
--- a/i18n/src/commonMain/resources/MR/values/base/strings.xml
+++ b/i18n/src/commonMain/resources/MR/values/base/strings.xml
@@ -39,6 +39,8 @@
Refresh
Retry
Close
+ Search
+ Search…
Library
diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/navigation/ActionMenu.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/navigation/ActionMenu.kt
new file mode 100644
index 00000000..d0d31c3d
--- /dev/null
+++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/navigation/ActionMenu.kt
@@ -0,0 +1,153 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package ca.gosyer.ui.base.navigation
+
+import androidx.compose.material.DropdownMenu
+import androidx.compose.material.DropdownMenuItem
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.LocalContentAlpha
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.material.TextButton
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.key
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.util.fastForEach
+
+// Originally from https://gist.github.com/MachFour/369ebb56a66e2f583ebfb988dda2decf
+
+// Essentially a wrapper around a lambda function to give it a name and icon
+// akin to Android menu XML entries.
+// As an item on the action bar, the action will be displayed with an IconButton
+// with the given icon, if not null. Otherwise, the string from the name resource is used.
+// In overflow menu, item will always be displayed as text.
+@Stable
+data class ActionItem(
+ val name: String,
+ val icon: ImageVector? = null,
+ val overflowMode: OverflowMode = OverflowMode.IF_NECESSARY,
+ val enabled: Boolean = true,
+ val doAction: () -> Unit,
+) {
+ // allow 'calling' the action like a function
+ operator fun invoke() = doAction()
+}
+
+// Whether action items are allowed to overflow into a dropdown menu - or NOT SHOWN to hide
+enum class OverflowMode {
+ NEVER_OVERFLOW, IF_NECESSARY, ALWAYS_OVERFLOW, NOT_SHOWN
+}
+
+// Note: should be used in a RowScope
+@Composable
+fun ActionMenu(
+ items: List,
+ numIcons: Int = 3, // includes overflow menu icon; may be overridden by NEVER_OVERFLOW
+ menuVisible: MutableState = remember { mutableStateOf(false) },
+ iconItem: @Composable (onClick: () -> Unit, name: String, icon: ImageVector, enabled: Boolean) -> Unit
+) {
+ if (items.isEmpty()) {
+ return
+ }
+ // decide how many action items to show as icons
+ val (appbarActions, overflowActions) = derivedStateOf {
+ separateIntoIconAndOverflow(items, numIcons)
+ }.value
+
+ appbarActions.fastForEach { item ->
+ key(item.hashCode()) {
+ if (item.icon != null) {
+ iconItem(item.doAction, item.name, item.icon, item.enabled)
+ } else {
+ TextButton(onClick = item.doAction, enabled = item.enabled) {
+ Text(
+ text = item.name,
+ color = MaterialTheme.colors.onPrimary.copy(alpha = LocalContentAlpha.current),
+ )
+ }
+ }
+ }
+ }
+
+ if (overflowActions.isNotEmpty()) {
+ IconButton(onClick = { menuVisible.value = true }) {
+ Icon(Icons.Default.MoreVert, "More actions")
+ }
+ DropdownMenu(
+ expanded = menuVisible.value,
+ onDismissRequest = { menuVisible.value = false },
+ ) {
+ overflowActions.fastForEach { item ->
+ key(item.hashCode()) {
+ DropdownMenuItem(
+ onClick = {
+ menuVisible.value = false
+ item()
+ },
+ enabled = item.enabled
+ ) {
+ //Icon(item.icon, item.name) just have text in the overflow menu
+ Text(item.name)
+ }
+ }
+ }
+ }
+ }
+}
+
+private fun separateIntoIconAndOverflow(
+ items: List,
+ numIcons: Int
+): Pair, List> {
+ var (iconCount, overflowCount, preferIconCount) = Triple(0, 0, 0)
+ for (item in items) {
+ when (item.overflowMode) {
+ OverflowMode.NEVER_OVERFLOW -> iconCount++
+ OverflowMode.IF_NECESSARY -> preferIconCount++
+ OverflowMode.ALWAYS_OVERFLOW -> overflowCount++
+ OverflowMode.NOT_SHOWN -> {}
+ }
+ }
+
+ val needsOverflow = iconCount + preferIconCount > numIcons || overflowCount > 0
+ val actionIconSpace = numIcons - (if (needsOverflow) 1 else 0)
+
+ val iconActions = ArrayList()
+ val overflowActions = ArrayList()
+
+ var iconsAvailableBeforeOverflow = actionIconSpace - iconCount
+ for (item in items) {
+ when (item.overflowMode) {
+ OverflowMode.NEVER_OVERFLOW -> {
+ iconActions.add(item)
+ }
+ OverflowMode.ALWAYS_OVERFLOW -> {
+ overflowActions.add(item)
+ }
+ OverflowMode.IF_NECESSARY -> {
+ if (iconsAvailableBeforeOverflow > 0) {
+ iconActions.add(item)
+ iconsAvailableBeforeOverflow--
+ } else {
+ overflowActions.add(item)
+ }
+ }
+ OverflowMode.NOT_SHOWN -> {
+ // skip
+ }
+ }
+ }
+ return iconActions to overflowActions
+}
\ No newline at end of file
diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/navigation/Toolbar.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/navigation/Toolbar.kt
index 96daadb9..7ea70c16 100644
--- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/navigation/Toolbar.kt
+++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/navigation/Toolbar.kt
@@ -14,9 +14,10 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@@ -26,34 +27,39 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.AppBarDefaults
import androidx.compose.material.Card
import androidx.compose.material.ContentAlpha
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
+import androidx.compose.material.LocalContentAlpha
import androidx.compose.material.LocalContentColor
+import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
+import androidx.compose.material.ProvideTextStyle
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.contentColorFor
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Close
+import androidx.compose.material.icons.rounded.Search
import androidx.compose.material.icons.rounded.Sort
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+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.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.input.key.Key
-import androidx.compose.ui.input.key.KeyEventType
-import androidx.compose.ui.input.key.key
-import androidx.compose.ui.input.key.onPreviewKeyEvent
-import androidx.compose.ui.input.key.type
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction
@@ -64,6 +70,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import ca.gosyer.i18n.MR
import ca.gosyer.uicore.components.BoxWithTooltipSurface
+import ca.gosyer.uicore.components.keyboardHandler
import ca.gosyer.uicore.resources.stringResource
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator
@@ -75,13 +82,60 @@ fun Toolbar(
closable: Boolean = (navigator?.size ?: 0) > 1,
onClose: () -> Unit = { navigator?.pop() },
modifier: Modifier = Modifier,
- actions: @Composable RowScope.() -> Unit = {},
+ actions: @Composable () -> List = { emptyList() },
backgroundColor: Color = MaterialTheme.colors.surface, // CustomColors.current.bars,
contentColor: Color = contentColorFor(backgroundColor), // CustomColors.current.onBars,
- elevation: Dp = AppBarDefaults.TopAppBarElevation,
+ elevation: Dp = Dp.Hairline,
searchText: String? = null,
search: ((String) -> Unit)? = null,
searchSubmit: (() -> Unit)? = null,
+) {
+ BoxWithConstraints {
+ if (maxWidth > 600.dp) {
+ WideToolbar(
+ name = name,
+ closable = closable,
+ onClose = onClose,
+ modifier = modifier,
+ actions = actions,
+ backgroundColor = backgroundColor,
+ contentColor = contentColor,
+ elevation = elevation,
+ searchText = searchText,
+ search = search,
+ searchSubmit = searchSubmit
+ )
+ } else {
+ ThinToolbar(
+ name = name,
+ closable = closable,
+ onClose = onClose,
+ modifier = modifier,
+ actions = actions,
+ backgroundColor = backgroundColor,
+ contentColor = contentColor,
+ elevation = elevation,
+ searchText = searchText,
+ search = search,
+ searchSubmit = searchSubmit
+ )
+ }
+ }
+}
+
+@Composable
+private fun WideToolbar(
+ name: String,
+ closable: Boolean,
+ onClose: () -> Unit,
+ modifier: Modifier,
+ actions: @Composable () -> List = { emptyList() },
+ backgroundColor: Color,
+ contentColor: Color,
+ elevation: Dp,
+ searchText: String?,
+ search: ((String) -> Unit)?,
+ searchSubmit: (() -> Unit)?,
) {
Surface(
modifier = modifier,
@@ -118,9 +172,153 @@ fun Toolbar(
}
Row {
- actions()
+ ActionMenu(actions()) { onClick: () -> Unit, name: String, icon: ImageVector, enabled: Boolean ->
+ TextActionIcon(
+ onClick = onClick,
+ text = name,
+ icon = icon,
+ enabled = enabled
+ )
+ }
if (closable) {
- TextActionIcon(onClick = onClose, stringResource(MR.strings.action_close), Icons.Rounded.Close)
+ TextActionIcon(
+ onClick = onClose,
+ text = stringResource(MR.strings.action_close),
+ icon = Icons.Rounded.Close
+ )
+ }
+ }
+ }
+ }
+}
+
+
+
+@Composable
+private fun ThinToolbar(
+ name: String,
+ closable: Boolean,
+ onClose: () -> Unit,
+ modifier: Modifier,
+ actions: @Composable () -> List = { emptyList() },
+ backgroundColor: Color,
+ contentColor: Color,
+ elevation: Dp,
+ searchText: String?,
+ search: ((String) -> Unit)?,
+ searchSubmit: (() -> Unit)?,
+) {
+ var searchMode by remember { mutableStateOf(!searchText.isNullOrEmpty()) }
+ Surface(
+ color = backgroundColor,
+ contentColor = contentColor,
+ elevation = elevation,
+ shape = RectangleShape,
+ modifier = modifier
+ ) {
+ CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
+ Row(
+ Modifier.fillMaxWidth()
+ .padding(AppBarDefaults.ContentPadding)
+ .height(56.dp),
+ horizontalArrangement = Arrangement.Start,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ if (!closable && !searchMode) {
+ Spacer(Modifier.width(12.dp))
+ } else {
+ Row(Modifier.width(68.dp), verticalAlignment = Alignment.CenterVertically) {
+ CompositionLocalProvider(
+ LocalContentAlpha provides ContentAlpha.high,
+ ) {
+ IconButton(
+ onClick = {
+ if (searchMode) {
+ search?.invoke("")
+ searchSubmit?.invoke()
+ searchMode = false
+ } else {
+ onClose()
+ }
+ }
+ ) {
+ Icon(
+ Icons.Rounded.ArrowBack,
+ stringResource(MR.strings.action_close)
+ )
+ }
+ }
+ }
+ }
+
+ if (searchMode) {
+ Row(
+ Modifier.fillMaxHeight().weight(1f),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ BasicTextField(
+ value = searchText.orEmpty(),
+ onValueChange = search ?: {},
+ modifier = Modifier.fillMaxWidth()
+ .keyboardHandler(singleLine = true) {
+ searchSubmit?.invoke()
+ it.clearFocus()
+ },
+ textStyle = LocalTextStyle.current.copy(color = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.medium)),
+ cursorBrush = SolidColor(MaterialTheme.colors.primary),
+ keyboardActions = KeyboardActions { searchSubmit?.invoke() },
+ maxLines = 1,
+ singleLine = true,
+ decorationBox = { innerTextField ->
+ Box(contentAlignment = Alignment.CenterStart) {
+ if (searchText.isNullOrEmpty()) {
+ Text(stringResource(MR.strings.action_searching))
+ }
+ innerTextField()
+ }
+ },
+ keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search)
+ )
+ }
+ } else {
+ Row(
+ Modifier.fillMaxHeight().weight(1f),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ ProvideTextStyle(value = MaterialTheme.typography.h6) {
+ CompositionLocalProvider(
+ LocalContentAlpha provides ContentAlpha.high,
+ ) {
+ Text(name)
+ }
+ }
+ }
+ }
+
+ CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
+ Row(
+ Modifier.fillMaxHeight(),
+ horizontalArrangement = Arrangement.End,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ if (search != null && !searchMode) {
+ IconButton(onClick = { searchMode = true }) {
+ Icon(Icons.Rounded.Search, stringResource(MR.strings.action_search))
+ }
+ }
+ ActionMenu(
+ actions(),
+ if (searchMode) {
+ 1
+ } else {
+ 3
+ }
+ ) { onClick: () -> Unit, name: String, icon: ImageVector, enabled: Boolean ->
+ IconButton(onClick = onClick, enabled = enabled) {
+ Icon(icon, name)
+ }
+ }
+ }
}
}
}
@@ -147,17 +345,11 @@ private fun SearchBox(
searchText.orEmpty(),
onValueChange = { search?.invoke(it) },
singleLine = true,
- modifier = Modifier.fillMaxWidth().then(
- if (searchSubmit != null) {
- Modifier.onPreviewKeyEvent { event ->
- (event.key == Key.Enter && event.type == KeyEventType.KeyDown).also {
- if (it) {
- searchSubmit()
- }
- }
- }
- } else Modifier
- ),
+ modifier = Modifier.fillMaxWidth()
+ .keyboardHandler(singleLine = true) {
+ searchSubmit?.invoke()
+ it.clearFocus()
+ },
textStyle = TextStyle(contentColor, 18.sp),
cursorBrush = SolidColor(contentColor.copy(alpha = 0.50F)),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search)
diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/downloads/components/DownloadsScreenContent.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/downloads/components/DownloadsScreenContent.kt
index ec38cdbf..8286c982 100644
--- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/downloads/components/DownloadsScreenContent.kt
+++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/downloads/components/DownloadsScreenContent.kt
@@ -35,6 +35,7 @@ import androidx.compose.material.icons.rounded.MoreVert
import androidx.compose.material.icons.rounded.Pause
import androidx.compose.material.icons.rounded.PlayArrow
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -46,7 +47,7 @@ import ca.gosyer.data.download.model.DownloadChapter
import ca.gosyer.data.download.model.DownloaderStatus
import ca.gosyer.data.models.Chapter
import ca.gosyer.i18n.MR
-import ca.gosyer.ui.base.navigation.ActionIcon
+import ca.gosyer.ui.base.navigation.ActionItem
import ca.gosyer.ui.base.navigation.Toolbar
import ca.gosyer.uicore.components.DropdownIconButton
import ca.gosyer.uicore.components.MangaListItem
@@ -74,12 +75,12 @@ fun DownloadsScreenContent(
Toolbar(
stringResource(MR.strings.location_downloads),
actions = {
- if (downloadStatus == DownloaderStatus.Started) {
- ActionIcon(onClick = pauseDownloading, stringResource(MR.strings.action_pause), Icons.Rounded.Pause)
- } else {
- ActionIcon(onClick = startDownloading, stringResource(MR.strings.action_continue), Icons.Rounded.PlayArrow)
- }
- ActionIcon(onClick = clearQueue, stringResource(MR.strings.action_clear_queue), Icons.Rounded.ClearAll)
+ getActionItems(
+ downloadStatus = downloadStatus,
+ startDownloading = startDownloading,
+ pauseDownloading = pauseDownloading,
+ clearQueue = clearQueue
+ )
}
)
}
@@ -174,3 +175,29 @@ fun DownloadsItem(
Spacer(Modifier.width(16.dp))
}
}
+
+@Stable
+@Composable
+private fun getActionItems(
+ downloadStatus: DownloaderStatus,
+ startDownloading: () -> Unit,
+ pauseDownloading: () -> Unit,
+ clearQueue: () -> Unit,
+) : List {
+ return listOf(
+ if (downloadStatus == DownloaderStatus.Started) {
+ ActionItem(
+ stringResource(MR.strings.action_pause),
+ Icons.Rounded.Pause,
+ doAction = pauseDownloading
+ )
+ } else {
+ ActionItem(
+ stringResource(MR.strings.action_continue),
+ Icons.Rounded.PlayArrow,
+ doAction = startDownloading
+ )
+ },
+ ActionItem(stringResource(MR.strings.action_clear_queue), Icons.Rounded.ClearAll, doAction = clearQueue)
+ )
+}
\ No newline at end of file
diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/extensions/components/ExtensionsScreenContent.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/extensions/components/ExtensionsScreenContent.kt
index 80de831b..c1b58759 100644
--- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/extensions/components/ExtensionsScreenContent.kt
+++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/extensions/components/ExtensionsScreenContent.kt
@@ -32,6 +32,7 @@ import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Translate
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
@@ -48,7 +49,7 @@ import ca.gosyer.data.models.Extension
import ca.gosyer.i18n.MR
import ca.gosyer.presentation.build.BuildKonfig
import ca.gosyer.ui.base.WindowDialog
-import ca.gosyer.ui.base.navigation.TextActionIcon
+import ca.gosyer.ui.base.navigation.ActionItem
import ca.gosyer.ui.base.navigation.Toolbar
import ca.gosyer.uicore.components.LoadingScreen
import ca.gosyer.uicore.image.KamelImage
@@ -132,15 +133,10 @@ fun ExtensionsToolbar(
searchText = searchText,
search = search,
actions = {
- TextActionIcon(
- {
- val enabledLangs = MutableStateFlow(currentEnabledLangs.value)
- LanguageDialog(enabledLangs, getSourceLanguages().toList()) {
- setEnabledLanguages(enabledLangs.value)
- }
- },
- stringResource(MR.strings.enabled_languages),
- Icons.Rounded.Translate
+ getActionItems(
+ currentEnabledLangs = currentEnabledLangs,
+ getSourceLanguages = getSourceLanguages,
+ setEnabledLanguages = setEnabledLanguages
)
}
)
@@ -235,3 +231,23 @@ fun LanguageDialog(enabledLangsFlow: MutableStateFlow>, availableLan
}
}
}
+
+@Stable
+@Composable
+private fun getActionItems(
+ currentEnabledLangs: StateFlow>,
+ getSourceLanguages: () -> Set,
+ setEnabledLanguages: (Set) -> Unit
+): List {
+ return listOf(
+ ActionItem(
+ stringResource(MR.strings.enabled_languages),
+ Icons.Rounded.Translate
+ ) {
+ val enabledLangs = MutableStateFlow(currentEnabledLangs.value)
+ LanguageDialog(enabledLangs, getSourceLanguages().toList()) {
+ setEnabledLanguages(enabledLangs.value)
+ }
+ }
+ )
+}
\ No newline at end of file
diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/components/MangaScreenContent.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/components/MangaScreenContent.kt
index c9fc0358..1747f6f6 100644
--- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/components/MangaScreenContent.kt
+++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/manga/components/MangaScreenContent.kt
@@ -6,7 +6,6 @@
package ca.gosyer.ui.manga.components
-import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.VerticalScrollbar
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
@@ -25,6 +24,7 @@ import androidx.compose.material.icons.rounded.Label
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.Stable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@@ -32,7 +32,7 @@ import ca.gosyer.data.models.Category
import ca.gosyer.data.models.Manga
import ca.gosyer.i18n.MR
import ca.gosyer.ui.base.chapter.ChapterDownloadItem
-import ca.gosyer.ui.base.navigation.TextActionIcon
+import ca.gosyer.ui.base.navigation.ActionItem
import ca.gosyer.ui.base.navigation.Toolbar
import ca.gosyer.ui.reader.openReaderMenu
import ca.gosyer.uicore.components.ErrorScreen
@@ -73,28 +73,14 @@ fun MangaScreenContent(
Toolbar(
stringResource(MR.strings.location_manga),
actions = {
- AnimatedVisibility(categoriesExist && manga?.inLibrary == true) {
- TextActionIcon(
- setCategories,
- stringResource(MR.strings.edit_categories),
- Icons.Rounded.Label
- )
- }
- TextActionIcon(
- toggleFavorite,
- stringResource(if (manga?.inLibrary == true) MR.strings.action_remove_favorite else MR.strings.action_favorite),
- if (manga?.inLibrary == true) {
- Icons.Rounded.Favorite
- } else {
- Icons.Rounded.FavoriteBorder
- },
- manga != null
- )
- TextActionIcon(
- refreshManga,
- stringResource(MR.strings.action_refresh_manga),
- Icons.Rounded.Refresh,
- !isLoading
+ getActionItems(
+ refreshManga = refreshManga,
+ refreshMangaEnabled = !isLoading,
+ categoryItemVisible = categoriesExist && manga?.inLibrary == true,
+ setCategories = setCategories,
+ inLibrary = manga?.inLibrary == true,
+ toggleFavorite = toggleFavorite,
+ favoritesButtonEnabled = manga != null
)
}
)
@@ -150,3 +136,41 @@ fun MangaScreenContent(
}
}
}
+
+@Composable
+@Stable
+private fun getActionItems(
+ refreshManga: () -> Unit,
+ refreshMangaEnabled: Boolean,
+ categoryItemVisible: Boolean,
+ setCategories: () -> Unit,
+ inLibrary: Boolean,
+ toggleFavorite: () -> Unit,
+ favoritesButtonEnabled: Boolean
+): List {
+ return listOfNotNull(
+ ActionItem(
+ name = stringResource(MR.strings.action_refresh_manga),
+ icon = Icons.Rounded.Refresh,
+ doAction = refreshManga,
+ enabled = refreshMangaEnabled
+ ),
+ if (categoryItemVisible) {
+ ActionItem(
+ name = stringResource(MR.strings.edit_categories),
+ icon = Icons.Rounded.Label,
+ doAction = setCategories
+ )
+ } else null,
+ ActionItem(
+ name = stringResource(if (inLibrary) MR.strings.action_remove_favorite else MR.strings.action_favorite),
+ icon = if (inLibrary) {
+ Icons.Rounded.Favorite
+ } else {
+ Icons.Rounded.FavoriteBorder
+ },
+ doAction = toggleFavorite,
+ enabled = favoritesButtonEnabled
+ )
+ )
+}
\ No newline at end of file
diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/browse/components/SourceScreenContent.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/browse/components/SourceScreenContent.kt
index 75d83260..dc7c95c7 100644
--- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/browse/components/SourceScreenContent.kt
+++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/browse/components/SourceScreenContent.kt
@@ -26,6 +26,7 @@ import androidx.compose.material.icons.rounded.NewReleases
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.Stable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.FilterQuality
@@ -34,7 +35,7 @@ import androidx.compose.ui.unit.dp
import ca.gosyer.data.models.Manga
import ca.gosyer.data.models.Source
import ca.gosyer.i18n.MR
-import ca.gosyer.ui.base.navigation.TextActionIcon
+import ca.gosyer.ui.base.navigation.ActionItem
import ca.gosyer.ui.base.navigation.Toolbar
import ca.gosyer.ui.sources.browse.filter.SourceFiltersMenu
import ca.gosyer.ui.sources.browse.filter.model.SourceFiltersView
@@ -149,44 +150,21 @@ fun SourceToolbar(
search = onSearch,
searchSubmit = onSubmitSearch,
actions = {
- if (source.isConfigurable) {
- TextActionIcon(
- {
- onSourceSettingsClick(source.id)
- },
- stringResource(MR.strings.location_settings),
- Icons.Rounded.Settings
- )
- }
- if (showFilterButton) {
- TextActionIcon(
- {
- onToggleFiltersClick(!showingFilters)
- },
- stringResource(MR.strings.filter_source),
- Icons.Rounded.FilterList,
- !isLatest
- )
- }
- if (showLatestButton) {
- TextActionIcon(
- {
- onClickMode(!isLatest)
- },
- stringResource(
- if (isLatest) {
- MR.strings.move_to_browse
- } else {
- MR.strings.move_to_latest
- }
- ),
- if (isLatest) {
- Icons.Rounded.Explore
- } else {
- Icons.Rounded.NewReleases
- }
- )
- }
+ getActionItems(
+ isConfigurable = source.isConfigurable,
+ onSourceSettingsClick = {
+ onSourceSettingsClick(source.id)
+ },
+ isLatest = isLatest,
+ showLatestButton = showLatestButton,
+ showFilterButton = showFilterButton,
+ onToggleFiltersClick = {
+ onToggleFiltersClick(!showingFilters)
+ },
+ onClickMode = {
+ onClickMode(!isLatest)
+ }
+ )
}
)
}
@@ -227,3 +205,50 @@ private fun MangaTable(
}
}
}
+
+@Composable
+@Stable
+private fun getActionItems(
+ isConfigurable: Boolean,
+ onSourceSettingsClick: () -> Unit,
+ isLatest: Boolean,
+ showLatestButton: Boolean,
+ showFilterButton: Boolean,
+ onToggleFiltersClick: () -> Unit,
+ onClickMode: () -> Unit
+): List {
+ return listOfNotNull(
+ if (isConfigurable) {
+ ActionItem(
+ name = stringResource(MR.strings.location_settings),
+ icon = Icons.Rounded.Settings,
+ doAction = onSourceSettingsClick
+ )
+ } else null,
+ if (showFilterButton) {
+ ActionItem(
+ name = stringResource(MR.strings.filter_source),
+ icon = Icons.Rounded.FilterList,
+ doAction = onToggleFiltersClick,
+ enabled = !isLatest
+ )
+ } else null,
+ if (showLatestButton) {
+ ActionItem(
+ name = stringResource(
+ if (isLatest) {
+ MR.strings.move_to_browse
+ } else {
+ MR.strings.move_to_latest
+ }
+ ),
+ icon = if (isLatest) {
+ Icons.Rounded.Explore
+ } else {
+ Icons.Rounded.NewReleases
+ },
+ doAction = onClickMode
+ )
+ } else null
+ )
+}
\ No newline at end of file
diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/home/components/SourceHomeScreenContent.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/home/components/SourceHomeScreenContent.kt
index 88ff6e49..932eb1c9 100644
--- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/home/components/SourceHomeScreenContent.kt
+++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/sources/home/components/SourceHomeScreenContent.kt
@@ -33,6 +33,7 @@ import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Translate
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
@@ -41,7 +42,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import ca.gosyer.data.models.Source
import ca.gosyer.i18n.MR
-import ca.gosyer.ui.base.navigation.TextActionIcon
+import ca.gosyer.ui.base.navigation.ActionItem
import ca.gosyer.ui.base.navigation.Toolbar
import ca.gosyer.ui.extensions.components.LanguageDialog
import ca.gosyer.uicore.components.LoadingScreen
@@ -109,15 +110,13 @@ fun SourceHomeScreenToolbar(
Toolbar(
stringResource(MR.strings.location_sources),
actions = {
- TextActionIcon(
- {
+ getActionItems(
+ onEnabledLanguagesClick = {
val enabledLangs = MutableStateFlow(sourceLanguages.value)
LanguageDialog(enabledLangs, onGetEnabledLanguages().toList()) {
onSetEnabledLanguages(enabledLangs.value)
}
- },
- stringResource(MR.strings.enabled_languages),
- Icons.Rounded.Translate
+ }
)
}
)
@@ -176,3 +175,17 @@ fun SourceItem(
}
}
}
+
+@Composable
+@Stable
+private fun getActionItems(
+ onEnabledLanguagesClick: () -> Unit
+): List {
+ return listOf(
+ ActionItem(
+ stringResource(MR.strings.enabled_languages),
+ Icons.Rounded.Translate,
+ doAction = onEnabledLanguagesClick
+ )
+ )
+}
\ No newline at end of file
diff --git a/ui-core/src/jvmMain/kotlin/ca/gosyer/uicore/components/KeyboardHandler.kt b/ui-core/src/jvmMain/kotlin/ca/gosyer/uicore/components/KeyboardHandler.kt
new file mode 100644
index 00000000..9fb0be53
--- /dev/null
+++ b/ui-core/src/jvmMain/kotlin/ca/gosyer/uicore/components/KeyboardHandler.kt
@@ -0,0 +1,33 @@
+package ca.gosyer.uicore.components
+
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.focus.FocusDirection
+import androidx.compose.ui.focus.FocusManager
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.input.key.KeyEventType
+import androidx.compose.ui.input.key.key
+import androidx.compose.ui.input.key.onPreviewKeyEvent
+import androidx.compose.ui.input.key.type
+import androidx.compose.ui.platform.LocalFocusManager
+
+/**
+ * A modifier to handle keyboard keys properly
+ */
+@OptIn(ExperimentalComposeUiApi::class)
+fun Modifier.keyboardHandler(
+ singleLine: Boolean = false,
+ action: (FocusManager) -> Unit = { it.moveFocus(FocusDirection.Down) }
+) = composed {
+ val focusManager = LocalFocusManager.current
+ Modifier.onPreviewKeyEvent {
+ if (
+ (it.key == Key.Tab || (singleLine && it.key == Key.Enter)) &&
+ it.type == KeyEventType.KeyDown
+ ) {
+ action(focusManager)
+ true
+ } else false
+ }
+}