mirror of
https://github.com/Suwayomi/TachideskJUI.git
synced 2025-12-10 23:02:04 +01:00
Thin screen toolbar with search
This commit is contained in:
@@ -39,6 +39,8 @@
|
||||
<string name="action_refresh_manga">Refresh</string>
|
||||
<string name="action_retry">Retry</string>
|
||||
<string name="action_close">Close</string>
|
||||
<string name="action_search">Search</string>
|
||||
<string name="action_searching">Search…</string>
|
||||
|
||||
<!-- Locations -->
|
||||
<string name="location_library">Library</string>
|
||||
|
||||
@@ -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<ActionItem>,
|
||||
numIcons: Int = 3, // includes overflow menu icon; may be overridden by NEVER_OVERFLOW
|
||||
menuVisible: MutableState<Boolean> = 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<ActionItem>,
|
||||
numIcons: Int
|
||||
): Pair<List<ActionItem>, List<ActionItem>> {
|
||||
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<ActionItem>()
|
||||
val overflowActions = ArrayList<ActionItem>()
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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<ActionItem> = { 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<ActionItem> = { 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<ActionItem> = { 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)
|
||||
|
||||
@@ -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<ActionItem> {
|
||||
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)
|
||||
)
|
||||
}
|
||||
@@ -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<Set<String>>, availableLan
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
@Composable
|
||||
private fun getActionItems(
|
||||
currentEnabledLangs: StateFlow<Set<String>>,
|
||||
getSourceLanguages: () -> Set<String>,
|
||||
setEnabledLanguages: (Set<String>) -> Unit
|
||||
): List<ActionItem> {
|
||||
return listOf(
|
||||
ActionItem(
|
||||
stringResource(MR.strings.enabled_languages),
|
||||
Icons.Rounded.Translate
|
||||
) {
|
||||
val enabledLangs = MutableStateFlow(currentEnabledLangs.value)
|
||||
LanguageDialog(enabledLangs, getSourceLanguages().toList()) {
|
||||
setEnabledLanguages(enabledLangs.value)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -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<ActionItem> {
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -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<ActionItem> {
|
||||
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
|
||||
)
|
||||
}
|
||||
@@ -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<ActionItem> {
|
||||
return listOf(
|
||||
ActionItem(
|
||||
stringResource(MR.strings.enabled_languages),
|
||||
Icons.Rounded.Translate,
|
||||
doAction = onEnabledLanguagesClick
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user