Thin screen toolbar with search

This commit is contained in:
Syer10
2022-02-04 19:58:58 -05:00
parent 53061d95bd
commit 2ca87bf93e
9 changed files with 592 additions and 107 deletions

View File

@@ -39,6 +39,8 @@
<string name="action_refresh_manga">Refresh</string> <string name="action_refresh_manga">Refresh</string>
<string name="action_retry">Retry</string> <string name="action_retry">Retry</string>
<string name="action_close">Close</string> <string name="action_close">Close</string>
<string name="action_search">Search</string>
<string name="action_searching">Search…</string>
<!-- Locations --> <!-- Locations -->
<string name="location_library">Library</string> <string name="location_library">Library</string>

View File

@@ -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
}

View File

@@ -14,9 +14,10 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row 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.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth 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.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.AppBarDefaults import androidx.compose.material.AppBarDefaults
import androidx.compose.material.Card import androidx.compose.material.Card
import androidx.compose.material.ContentAlpha import androidx.compose.material.ContentAlpha
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.IconButton import androidx.compose.material.IconButton
import androidx.compose.material.LocalContentAlpha
import androidx.compose.material.LocalContentColor import androidx.compose.material.LocalContentColor
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.ProvideTextStyle
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.contentColorFor import androidx.compose.material.contentColorFor
import androidx.compose.material.icons.Icons 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.Close
import androidx.compose.material.icons.rounded.Search
import androidx.compose.material.icons.rounded.Sort import androidx.compose.material.icons.rounded.Sort
import androidx.compose.material.ripple.rememberRipple import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable 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.remember
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.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector 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.semantics.Role
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
@@ -64,6 +70,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.uicore.components.BoxWithTooltipSurface import ca.gosyer.uicore.components.BoxWithTooltipSurface
import ca.gosyer.uicore.components.keyboardHandler
import ca.gosyer.uicore.resources.stringResource import ca.gosyer.uicore.resources.stringResource
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.Navigator
@@ -75,13 +82,60 @@ fun Toolbar(
closable: Boolean = (navigator?.size ?: 0) > 1, closable: Boolean = (navigator?.size ?: 0) > 1,
onClose: () -> Unit = { navigator?.pop() }, onClose: () -> Unit = { navigator?.pop() },
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
actions: @Composable RowScope.() -> Unit = {}, actions: @Composable () -> List<ActionItem> = { emptyList() },
backgroundColor: Color = MaterialTheme.colors.surface, // CustomColors.current.bars, backgroundColor: Color = MaterialTheme.colors.surface, // CustomColors.current.bars,
contentColor: Color = contentColorFor(backgroundColor), // CustomColors.current.onBars, contentColor: Color = contentColorFor(backgroundColor), // CustomColors.current.onBars,
elevation: Dp = AppBarDefaults.TopAppBarElevation, elevation: Dp = Dp.Hairline,
searchText: String? = null, searchText: String? = null,
search: ((String) -> Unit)? = null, search: ((String) -> Unit)? = null,
searchSubmit: (() -> 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( Surface(
modifier = modifier, modifier = modifier,
@@ -118,9 +172,153 @@ fun Toolbar(
} }
Row { Row {
actions() ActionMenu(actions()) { onClick: () -> Unit, name: String, icon: ImageVector, enabled: Boolean ->
TextActionIcon(
onClick = onClick,
text = name,
icon = icon,
enabled = enabled
)
}
if (closable) { 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(), searchText.orEmpty(),
onValueChange = { search?.invoke(it) }, onValueChange = { search?.invoke(it) },
singleLine = true, singleLine = true,
modifier = Modifier.fillMaxWidth().then( modifier = Modifier.fillMaxWidth()
if (searchSubmit != null) { .keyboardHandler(singleLine = true) {
Modifier.onPreviewKeyEvent { event -> searchSubmit?.invoke()
(event.key == Key.Enter && event.type == KeyEventType.KeyDown).also { it.clearFocus()
if (it) { },
searchSubmit()
}
}
}
} else Modifier
),
textStyle = TextStyle(contentColor, 18.sp), textStyle = TextStyle(contentColor, 18.sp),
cursorBrush = SolidColor(contentColor.copy(alpha = 0.50F)), cursorBrush = SolidColor(contentColor.copy(alpha = 0.50F)),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search) keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search)

View File

@@ -35,6 +35,7 @@ import androidx.compose.material.icons.rounded.MoreVert
import androidx.compose.material.icons.rounded.Pause import androidx.compose.material.icons.rounded.Pause
import androidx.compose.material.icons.rounded.PlayArrow import androidx.compose.material.icons.rounded.PlayArrow
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.download.model.DownloaderStatus
import ca.gosyer.data.models.Chapter import ca.gosyer.data.models.Chapter
import ca.gosyer.i18n.MR 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.ui.base.navigation.Toolbar
import ca.gosyer.uicore.components.DropdownIconButton import ca.gosyer.uicore.components.DropdownIconButton
import ca.gosyer.uicore.components.MangaListItem import ca.gosyer.uicore.components.MangaListItem
@@ -74,12 +75,12 @@ fun DownloadsScreenContent(
Toolbar( Toolbar(
stringResource(MR.strings.location_downloads), stringResource(MR.strings.location_downloads),
actions = { actions = {
if (downloadStatus == DownloaderStatus.Started) { getActionItems(
ActionIcon(onClick = pauseDownloading, stringResource(MR.strings.action_pause), Icons.Rounded.Pause) downloadStatus = downloadStatus,
} else { startDownloading = startDownloading,
ActionIcon(onClick = startDownloading, stringResource(MR.strings.action_continue), Icons.Rounded.PlayArrow) pauseDownloading = pauseDownloading,
} clearQueue = clearQueue
ActionIcon(onClick = clearQueue, stringResource(MR.strings.action_clear_queue), Icons.Rounded.ClearAll) )
} }
) )
} }
@@ -174,3 +175,29 @@ fun DownloadsItem(
Spacer(Modifier.width(16.dp)) 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)
)
}

View File

@@ -32,6 +32,7 @@ import androidx.compose.material.Text
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Translate import androidx.compose.material.icons.rounded.Translate
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -48,7 +49,7 @@ import ca.gosyer.data.models.Extension
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.presentation.build.BuildKonfig import ca.gosyer.presentation.build.BuildKonfig
import ca.gosyer.ui.base.WindowDialog 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.ui.base.navigation.Toolbar
import ca.gosyer.uicore.components.LoadingScreen import ca.gosyer.uicore.components.LoadingScreen
import ca.gosyer.uicore.image.KamelImage import ca.gosyer.uicore.image.KamelImage
@@ -132,15 +133,10 @@ fun ExtensionsToolbar(
searchText = searchText, searchText = searchText,
search = search, search = search,
actions = { actions = {
TextActionIcon( getActionItems(
{ currentEnabledLangs = currentEnabledLangs,
val enabledLangs = MutableStateFlow(currentEnabledLangs.value) getSourceLanguages = getSourceLanguages,
LanguageDialog(enabledLangs, getSourceLanguages().toList()) { setEnabledLanguages = setEnabledLanguages
setEnabledLanguages(enabledLangs.value)
}
},
stringResource(MR.strings.enabled_languages),
Icons.Rounded.Translate
) )
} }
) )
@@ -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)
}
}
)
}

View File

@@ -6,7 +6,6 @@
package ca.gosyer.ui.manga.components package ca.gosyer.ui.manga.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.VerticalScrollbar
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight 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.material.icons.rounded.Refresh
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp 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.data.models.Manga
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.ui.base.chapter.ChapterDownloadItem 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.base.navigation.Toolbar
import ca.gosyer.ui.reader.openReaderMenu import ca.gosyer.ui.reader.openReaderMenu
import ca.gosyer.uicore.components.ErrorScreen import ca.gosyer.uicore.components.ErrorScreen
@@ -73,28 +73,14 @@ fun MangaScreenContent(
Toolbar( Toolbar(
stringResource(MR.strings.location_manga), stringResource(MR.strings.location_manga),
actions = { actions = {
AnimatedVisibility(categoriesExist && manga?.inLibrary == true) { getActionItems(
TextActionIcon( refreshManga = refreshManga,
setCategories, refreshMangaEnabled = !isLoading,
stringResource(MR.strings.edit_categories), categoryItemVisible = categoriesExist && manga?.inLibrary == true,
Icons.Rounded.Label setCategories = setCategories,
) inLibrary = manga?.inLibrary == true,
} toggleFavorite = toggleFavorite,
TextActionIcon( favoritesButtonEnabled = manga != null
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
) )
} }
) )
@@ -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
)
)
}

View File

@@ -26,6 +26,7 @@ import androidx.compose.material.icons.rounded.NewReleases
import androidx.compose.material.icons.rounded.Settings import androidx.compose.material.icons.rounded.Settings
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.FilterQuality 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.Manga
import ca.gosyer.data.models.Source import ca.gosyer.data.models.Source
import ca.gosyer.i18n.MR 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.base.navigation.Toolbar
import ca.gosyer.ui.sources.browse.filter.SourceFiltersMenu import ca.gosyer.ui.sources.browse.filter.SourceFiltersMenu
import ca.gosyer.ui.sources.browse.filter.model.SourceFiltersView import ca.gosyer.ui.sources.browse.filter.model.SourceFiltersView
@@ -149,45 +150,22 @@ fun SourceToolbar(
search = onSearch, search = onSearch,
searchSubmit = onSubmitSearch, searchSubmit = onSubmitSearch,
actions = { actions = {
if (source.isConfigurable) { getActionItems(
TextActionIcon( isConfigurable = source.isConfigurable,
{ onSourceSettingsClick = {
onSourceSettingsClick(source.id) onSourceSettingsClick(source.id)
}, },
stringResource(MR.strings.location_settings), isLatest = isLatest,
Icons.Rounded.Settings showLatestButton = showLatestButton,
) showFilterButton = showFilterButton,
} onToggleFiltersClick = {
if (showFilterButton) {
TextActionIcon(
{
onToggleFiltersClick(!showingFilters) onToggleFiltersClick(!showingFilters)
}, },
stringResource(MR.strings.filter_source), onClickMode = {
Icons.Rounded.FilterList,
!isLatest
)
}
if (showLatestButton) {
TextActionIcon(
{
onClickMode(!isLatest) 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
} }
) )
} }
}
) )
} }
@@ -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
)
}

View File

@@ -33,6 +33,7 @@ import androidx.compose.material.Text
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Translate import androidx.compose.material.icons.rounded.Translate
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
@@ -41,7 +42,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ca.gosyer.data.models.Source import ca.gosyer.data.models.Source
import ca.gosyer.i18n.MR 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.base.navigation.Toolbar
import ca.gosyer.ui.extensions.components.LanguageDialog import ca.gosyer.ui.extensions.components.LanguageDialog
import ca.gosyer.uicore.components.LoadingScreen import ca.gosyer.uicore.components.LoadingScreen
@@ -109,15 +110,13 @@ fun SourceHomeScreenToolbar(
Toolbar( Toolbar(
stringResource(MR.strings.location_sources), stringResource(MR.strings.location_sources),
actions = { actions = {
TextActionIcon( getActionItems(
{ onEnabledLanguagesClick = {
val enabledLangs = MutableStateFlow(sourceLanguages.value) val enabledLangs = MutableStateFlow(sourceLanguages.value)
LanguageDialog(enabledLangs, onGetEnabledLanguages().toList()) { LanguageDialog(enabledLangs, onGetEnabledLanguages().toList()) {
onSetEnabledLanguages(enabledLangs.value) 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
)
)
}

View File

@@ -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
}
}