Settings: M3 and two pane ui (#8211)

* Settings: M3 and two pane ui

* TrackingLoginDialog: Move close button

* Use small top bar

* Revert "Update voyager to v1.0.0-rc02"

This reverts commit 570fec6ea6.

https://github.com/adrielcafe/voyager/issues/62
This commit is contained in:
Ivan Iskandar 2022-10-16 23:15:01 +07:00 committed by GitHub
parent 6635dd2990
commit 5c5468f9af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 482 additions and 256 deletions

View File

@ -141,12 +141,12 @@ android {
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
jvmTarget = JavaVersion.VERSION_11.toString()
}
sqldelight {

View File

@ -56,6 +56,7 @@ import androidx.compose.ui.unit.dp
* @sample androidx.compose.material3.samples.ScaffoldWithSimpleSnackbar
*
* Tachiyomi changes:
* * Pass scroll behavior to top bar by default
* * Remove height constraint for expanded app bar
* * Also take account of fab height when providing inner padding
*
@ -80,6 +81,7 @@ import androidx.compose.ui.unit.dp
@Composable
fun Scaffold(
modifier: Modifier = Modifier,
topBarScrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()),
topBar: @Composable (TopAppBarScrollBehavior) -> Unit = {},
bottomBar: @Composable () -> Unit = {},
snackbarHost: @Composable () -> Unit = {},
@ -89,21 +91,16 @@ fun Scaffold(
contentColor: Color = contentColorFor(containerColor),
content: @Composable (PaddingValues) -> Unit,
) {
/**
* Tachiyomi: Pass scroll behavior to topBar
*/
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
androidx.compose.material3.Surface(
modifier = Modifier
.nestedScroll(scrollBehavior.nestedScrollConnection)
.nestedScroll(topBarScrollBehavior.nestedScrollConnection)
.then(modifier),
color = containerColor,
contentColor = contentColor,
) {
ScaffoldLayout(
fabPosition = floatingActionButtonPosition,
topBar = { topBar(scrollBehavior) },
topBar = { topBar(topBarScrollBehavior) },
bottomBar = bottomBar,
content = content,
snackbar = snackbarHost,

View File

@ -0,0 +1,35 @@
package eu.kanade.presentation.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun TwoPanelBox(
modifier: Modifier = Modifier,
startContent: @Composable BoxScope.() -> Unit,
endContent: @Composable BoxScope.() -> Unit,
) {
BoxWithConstraints(modifier = modifier.fillMaxSize()) {
val firstWidth = (maxWidth / 2).coerceAtMost(450.dp)
val secondWidth = maxWidth - firstWidth
Box(
modifier = Modifier
.align(Alignment.TopStart)
.width(firstWidth),
content = startContent,
)
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.width(secondWidth),
content = endContent,
)
}
}

View File

@ -6,7 +6,6 @@ import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
@ -15,13 +14,11 @@ import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
@ -47,7 +44,6 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.presentation.components.ChapterDownloadAction
import eu.kanade.presentation.components.ExtendedFloatingActionButton
@ -55,6 +51,7 @@ import eu.kanade.presentation.components.LazyColumn
import eu.kanade.presentation.components.MangaBottomActionMenu
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.components.SwipeRefresh
import eu.kanade.presentation.components.TwoPanelBox
import eu.kanade.presentation.components.VerticalFastScroller
import eu.kanade.presentation.manga.components.ChapterHeader
import eu.kanade.presentation.manga.components.ExpandableMangaDescription
@ -501,79 +498,74 @@ fun MangaScreenLargeImpl(
}
},
) { contentPadding ->
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
val firstWidth = (maxWidth / 2).coerceAtMost(450.dp)
val secondWidth = maxWidth - firstWidth
Column(
modifier = Modifier
.align(Alignment.TopStart)
.width(firstWidth)
.verticalScroll(rememberScrollState()),
) {
MangaInfoBox(
windowWidthSizeClass = windowWidthSizeClass,
appBarPadding = contentPadding.calculateTopPadding(),
title = state.manga.title,
author = state.manga.author,
artist = state.manga.artist,
sourceName = remember { state.source.getNameForMangaInfo() },
isStubSource = remember { state.source is SourceManager.StubSource },
coverDataProvider = { state.manga },
status = state.manga.status,
onCoverClick = onCoverClicked,
doSearch = onSearch,
)
MangaActionRow(
favorite = state.manga.favorite,
trackingCount = state.trackingCount,
onAddToLibraryClicked = onAddToLibraryClicked,
onWebViewClicked = onWebViewClicked,
onTrackingClicked = onTrackingClicked,
onEditCategory = onEditCategoryClicked,
)
ExpandableMangaDescription(
defaultExpandState = true,
description = state.manga.description,
tagsProvider = { state.manga.genre },
onTagClicked = onTagClicked,
)
}
VerticalFastScroller(
listState = chapterListState,
modifier = Modifier
.align(Alignment.TopEnd)
.width(secondWidth),
topContentPadding = contentPadding.calculateTopPadding(),
) {
LazyColumn(
modifier = Modifier.fillMaxHeight(),
state = chapterListState,
contentPadding = PaddingValues(
top = contentPadding.calculateTopPadding(),
bottom = contentPadding.calculateBottomPadding(),
),
TwoPanelBox(
startContent = {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState()),
) {
item(
key = MangaScreenItem.CHAPTER_HEADER,
contentType = MangaScreenItem.CHAPTER_HEADER,
) {
ChapterHeader(
chapterCount = chapters.size,
onClick = onFilterButtonClicked,
)
}
sharedChapterItems(
chapters = chapters,
onChapterClicked = onChapterClicked,
onDownloadChapter = onDownloadChapter,
onChapterSelected = onChapterSelected,
MangaInfoBox(
windowWidthSizeClass = windowWidthSizeClass,
appBarPadding = contentPadding.calculateTopPadding(),
title = state.manga.title,
author = state.manga.author,
artist = state.manga.artist,
sourceName = remember { state.source.getNameForMangaInfo() },
isStubSource = remember { state.source is SourceManager.StubSource },
coverDataProvider = { state.manga },
status = state.manga.status,
onCoverClick = onCoverClicked,
doSearch = onSearch,
)
MangaActionRow(
favorite = state.manga.favorite,
trackingCount = state.trackingCount,
onAddToLibraryClicked = onAddToLibraryClicked,
onWebViewClicked = onWebViewClicked,
onTrackingClicked = onTrackingClicked,
onEditCategory = onEditCategoryClicked,
)
ExpandableMangaDescription(
defaultExpandState = true,
description = state.manga.description,
tagsProvider = { state.manga.genre },
onTagClicked = onTagClicked,
)
}
}
}
},
endContent = {
VerticalFastScroller(
listState = chapterListState,
topContentPadding = contentPadding.calculateTopPadding(),
) {
LazyColumn(
modifier = Modifier.fillMaxHeight(),
state = chapterListState,
contentPadding = PaddingValues(
top = contentPadding.calculateTopPadding(),
bottom = contentPadding.calculateBottomPadding(),
),
) {
item(
key = MangaScreenItem.CHAPTER_HEADER,
contentType = MangaScreenItem.CHAPTER_HEADER,
) {
ChapterHeader(
chapterCount = chapters.size,
onClick = onFilterButtonClicked,
)
}
sharedChapterItems(
chapters = chapters,
onChapterClicked = onChapterClicked,
onDownloadChapter = onDownloadChapter,
onChapterSelected = onChapterSelected,
)
}
}
},
)
}
}
}

View File

@ -2,25 +2,48 @@ package eu.kanade.presentation.more.settings
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.components.AppBar
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.Scaffold
import eu.kanade.tachiyomi.R
@Composable
fun PreferenceScaffold(
@StringRes titleRes: Int,
actions: @Composable RowScope.() -> Unit = {},
onBackPressed: () -> Unit = {},
onBackPressed: (() -> Unit)? = null,
itemsProvider: @Composable () -> List<Preference>,
) {
Scaffold(
topBar = { scrollBehavior ->
AppBar(
title = stringResource(titleRes),
navigateUp = onBackPressed,
topBar = {
TopAppBar(
title = {
Text(
text = stringResource(id = titleRes),
modifier = Modifier.padding(start = 8.dp),
)
},
navigationIcon = {
if (onBackPressed != null) {
IconButton(onClick = onBackPressed) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = stringResource(R.string.abc_action_bar_up_description),
)
}
}
},
actions = actions,
scrollBehavior = scrollBehavior,
scrollBehavior = it,
)
},
content = { contentPadding ->

View File

@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
@ -12,7 +11,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import eu.kanade.presentation.components.Divider
import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.presentation.more.settings.screen.SearchableSettings
import eu.kanade.presentation.more.settings.widget.PreferenceGroupHeader
@ -55,9 +53,6 @@ fun PreferenceScreen(
item {
Column {
if (i != 0) {
Divider(modifier = Modifier.padding(bottom = 8.dp))
}
PreferenceGroupHeader(title = preference.title)
}
}
@ -68,7 +63,9 @@ fun PreferenceScreen(
)
}
item {
Spacer(modifier = Modifier.height(12.dp))
if (i < items.lastIndex) {
Spacer(modifier = Modifier.height(12.dp))
}
}
}

View File

@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.RowScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.PreferenceScaffold
import eu.kanade.presentation.util.LocalBackPress
@ -26,10 +25,10 @@ interface SearchableSettings : Screen {
@Composable
override fun Content() {
val handleBack = LocalBackPress.currentOrThrow
val handleBack = LocalBackPress.current
PreferenceScaffold(
titleRes = getTitleRes(),
onBackPressed = handleBack::invoke,
onBackPressed = if (handleBack != null) handleBack::invoke else null,
actions = { AppBarAction() },
itemsProvider = { getPreferences() },
)

View File

@ -1,7 +1,13 @@
package eu.kanade.presentation.more.settings.screen
import androidx.annotation.StringRes
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.outlined.ChromeReaderMode
import androidx.compose.material.icons.outlined.Code
import androidx.compose.material.icons.outlined.CollectionsBookmark
@ -13,103 +19,210 @@ import androidx.compose.material.icons.outlined.Security
import androidx.compose.material.icons.outlined.SettingsBackupRestore
import androidx.compose.material.icons.outlined.Sync
import androidx.compose.material.icons.outlined.Tune
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.NonRestartableComposable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastFirstOrNull
import androidx.core.graphics.ColorUtils
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.PreferenceScaffold
import eu.kanade.presentation.components.LazyColumn
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
import eu.kanade.presentation.util.LocalBackPress
import eu.kanade.tachiyomi.R
object SettingsMainScreen : SearchableSettings {
@Composable
@ReadOnlyComposable
@StringRes
override fun getTitleRes() = R.string.label_settings
@Composable
@NonRestartableComposable
override fun getPreferences(): List<Preference> {
val navigator = LocalNavigator.currentOrThrow
return listOf(
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_category_general),
icon = Icons.Outlined.Tune,
onClick = { navigator.push(SettingsGeneralScreen()) },
),
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_category_appearance),
icon = Icons.Outlined.Palette,
onClick = { navigator.push(SettingsAppearanceScreen()) },
),
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_category_library),
icon = Icons.Outlined.CollectionsBookmark,
onClick = { navigator.push(SettingsLibraryScreen()) },
),
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_category_reader),
icon = Icons.Outlined.ChromeReaderMode,
onClick = { navigator.push(SettingsReaderScreen()) },
),
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_category_downloads),
icon = Icons.Outlined.GetApp,
onClick = { navigator.push(SettingsDownloadScreen()) },
),
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_category_tracking),
icon = Icons.Outlined.Sync,
onClick = { navigator.push(SettingsTrackingScreen()) },
),
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.browse),
icon = Icons.Outlined.Explore,
onClick = { navigator.push(SettingsBrowseScreen()) },
),
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.label_backup),
icon = Icons.Outlined.SettingsBackupRestore,
onClick = { navigator.push(SettingsBackupScreen()) },
),
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_category_security),
icon = Icons.Outlined.Security,
onClick = { navigator.push(SettingsSecurityScreen()) },
),
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_category_advanced),
icon = Icons.Outlined.Code,
onClick = { navigator.push(SettingsAdvancedScreen()) },
),
)
}
object SettingsMainScreen : Screen {
@Composable
override fun Content() {
Content(twoPane = false)
}
@Composable
private fun getPalerSurface(): Color {
val surface = MaterialTheme.colorScheme.surface
val dark = isSystemInDarkTheme()
return remember(surface, dark) {
val arr = FloatArray(3)
ColorUtils.colorToHSL(surface.toArgb(), arr)
arr[2] = if (dark) {
arr[2] - 0.05f
} else {
arr[2] + 0.02f
}.coerceIn(0f, 1f)
Color.hsl(arr[0], arr[1], arr[2])
}
}
@Composable
fun Content(twoPane: Boolean) {
val navigator = LocalNavigator.currentOrThrow
val backPress = LocalBackPress.currentOrThrow
PreferenceScaffold(
titleRes = getTitleRes(),
actions = {
AppBarActions(
listOf(
AppBar.Action(
title = stringResource(R.string.action_search),
icon = Icons.Outlined.Search,
onClick = { navigator.push(SettingsSearchScreen()) },
),
),
)
val containerColor = if (twoPane) getPalerSurface() else MaterialTheme.colorScheme.surface
Scaffold(
topBar = { scrollBehavior ->
// https://issuetracker.google.com/issues/249688556
MaterialTheme(
colorScheme = MaterialTheme.colorScheme.copy(surface = containerColor),
) {
TopAppBar(
title = {
Text(
text = stringResource(R.string.label_settings),
modifier = Modifier.padding(start = 8.dp),
)
},
navigationIcon = {
IconButton(onClick = backPress::invoke) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = stringResource(R.string.abc_action_bar_up_description),
)
}
},
actions = {
AppBarActions(
listOf(
AppBar.Action(
title = stringResource(R.string.action_search),
icon = Icons.Outlined.Search,
onClick = { navigator.navigate(SettingsSearchScreen(), twoPane) },
),
),
)
},
scrollBehavior = scrollBehavior,
)
}
},
containerColor = containerColor,
content = { contentPadding ->
LazyColumn(contentPadding = contentPadding) {
items(
items = items,
key = { it.hashCode() },
) { item ->
var modifier: Modifier = Modifier
var contentColor = LocalContentColor.current
if (twoPane) {
val selected = navigator.items.fastFirstOrNull { it::class == item.screen::class } != null
modifier = Modifier
.padding(horizontal = 8.dp)
.clip(RoundedCornerShape(24.dp))
.then(
if (selected) {
Modifier.background(MaterialTheme.colorScheme.surfaceVariant)
} else {
Modifier
},
)
if (selected) {
contentColor = MaterialTheme.colorScheme.onSurfaceVariant
}
}
CompositionLocalProvider(LocalContentColor provides contentColor) {
TextPreferenceWidget(
modifier = modifier,
title = stringResource(item.titleRes),
subtitle = stringResource(item.subtitleRes),
icon = item.icon,
onPreferenceClick = { navigator.navigate(item.screen, twoPane) },
)
}
}
}
},
onBackPressed = backPress::invoke,
itemsProvider = { getPreferences() },
)
}
private fun Navigator.navigate(screen: Screen, twoPane: Boolean) {
if (twoPane) replaceAll(screen) else push(screen)
}
}
private data class Item(
@StringRes val titleRes: Int,
@StringRes val subtitleRes: Int,
val icon: ImageVector,
val screen: Screen,
)
private val items = listOf(
Item(
titleRes = R.string.pref_category_general,
subtitleRes = R.string.pref_general_summary,
icon = Icons.Outlined.Tune,
screen = SettingsGeneralScreen(),
),
Item(
titleRes = R.string.pref_category_appearance,
subtitleRes = R.string.pref_appearance_summary,
icon = Icons.Outlined.Palette,
screen = SettingsAppearanceScreen(),
),
Item(
titleRes = R.string.pref_category_library,
subtitleRes = R.string.pref_library_summary,
icon = Icons.Outlined.CollectionsBookmark,
screen = SettingsLibraryScreen(),
),
Item(
titleRes = R.string.pref_category_reader,
subtitleRes = R.string.pref_reader_summary,
icon = Icons.Outlined.ChromeReaderMode,
screen = SettingsReaderScreen(),
),
Item(
titleRes = R.string.pref_category_downloads,
subtitleRes = R.string.pref_downloads_summary,
icon = Icons.Outlined.GetApp,
screen = SettingsDownloadScreen(),
),
Item(
titleRes = R.string.pref_category_tracking,
subtitleRes = R.string.pref_tracking_summary,
icon = Icons.Outlined.Sync,
screen = SettingsTrackingScreen(),
),
Item(
titleRes = R.string.browse,
subtitleRes = R.string.pref_browse_summary,
icon = Icons.Outlined.Explore,
screen = SettingsBrowseScreen(),
),
Item(
titleRes = R.string.label_backup,
subtitleRes = R.string.pref_backup_summary,
icon = Icons.Outlined.SettingsBackupRestore,
screen = SettingsBackupScreen(),
),
Item(
titleRes = R.string.pref_category_security,
subtitleRes = R.string.pref_security_summary,
icon = Icons.Outlined.Security,
screen = SettingsSecurityScreen(),
),
Item(
titleRes = R.string.pref_category_advanced,
subtitleRes = R.string.pref_advanced_summary,
icon = Icons.Outlined.Code,
screen = SettingsAdvancedScreen(),
),
)

View File

@ -146,8 +146,7 @@ class SettingsSearchScreen : Screen {
contentPadding = contentPadding,
) { result ->
SearchableSettings.highlightKey = result.highlightKey
navigator.popUntil { it is SettingsMainScreen }
navigator.push(result.route)
navigator.replace(result.route)
}
}
}

View File

@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.HelpOutline
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
@ -22,7 +23,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue
@ -30,6 +30,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
@ -189,7 +190,20 @@ class SettingsTrackingScreen : SearchableSettings {
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(R.string.login_title, stringResource(service.nameRes()))) },
title = {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = stringResource(R.string.login_title, stringResource(service.nameRes())),
modifier = Modifier.weight(1f),
)
IconButton(onClick = onDismissRequest) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(R.string.action_close),
)
}
}
},
text = {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
OutlinedTextField(
@ -232,38 +246,30 @@ class SettingsTrackingScreen : SearchableSettings {
}
},
confirmButton = {
Column {
Button(
modifier = Modifier.fillMaxWidth(),
enabled = !processing,
onClick = {
if (username.text.isEmpty() || password.text.isEmpty()) {
inputError = true
return@Button
}
scope.launchIO {
inputError = false
processing = true
val result = checkLogin(
context = context,
service = service,
username = username.text,
password = password.text,
)
if (result) onDismissRequest()
processing = false
}
},
) {
val id = if (processing) R.string.loading else R.string.login
Text(text = stringResource(id))
}
TextButton(
modifier = Modifier.fillMaxWidth(),
onClick = onDismissRequest,
) {
Text(text = stringResource(android.R.string.cancel))
}
Button(
modifier = Modifier.fillMaxWidth(),
enabled = !processing,
onClick = {
if (username.text.isEmpty() || password.text.isEmpty()) {
inputError = true
return@Button
}
scope.launchIO {
inputError = false
processing = true
val result = checkLogin(
context = context,
service = service,
username = username.text,
password = password.text,
)
if (result) onDismissRequest()
processing = false
}
},
) {
val id = if (processing) R.string.loading else R.string.login
Text(text = stringResource(id))
}
},
)

View File

@ -109,7 +109,7 @@ private fun AppThemesList(
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center,
maxLines = 2,
style = MaterialTheme.typography.bodySmall,
style = MaterialTheme.typography.bodyMedium,
)
}
}

View File

@ -31,6 +31,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import eu.kanade.presentation.more.settings.LocalPreferenceHighlighted
import eu.kanade.presentation.util.secondaryItemAlpha
import kotlinx.coroutines.delay
@ -54,12 +55,12 @@ internal fun BasePreferenceWidget(
modifier = Modifier
.padding(
start = HorizontalPadding,
top = 4.dp,
top = 0.dp,
end = HorizontalPadding,
)
.secondaryItemAlpha(),
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodySmall,
style = MaterialTheme.typography.bodyMedium,
maxLines = 10,
)
}
} else {
@ -106,15 +107,13 @@ private fun BasePreferenceWidgetImpl(
imageVector = icon,
contentDescription = null,
modifier = Modifier
.padding(start = HorizontalPadding, end = 12.dp)
.secondaryItemAlpha(),
tint = MaterialTheme.colorScheme.onSurface,
.padding(start = HorizontalPadding, end = 0.dp),
)
}
Column(
modifier = Modifier
.weight(1f)
.padding(vertical = 14.dp),
.padding(vertical = 16.dp),
) {
if (title.isNotBlank()) {
Row(
@ -125,7 +124,8 @@ private fun BasePreferenceWidgetImpl(
text = title,
overflow = TextOverflow.Ellipsis,
maxLines = 2,
style = MaterialTheme.typography.bodyLarge,
style = MaterialTheme.typography.titleLarge,
fontSize = 20.sp,
)
}
}
@ -173,4 +173,4 @@ internal fun Modifier.highlightBackground(highlighted: Boolean): Modifier = comp
}
internal val TrailingWidgetBuffer = 16.dp
internal val HorizontalPadding = 16.dp
internal val HorizontalPadding = 24.dp

View File

@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
@ -16,6 +17,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@ -23,6 +25,7 @@ import eu.kanade.presentation.components.Divider
import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.presentation.util.isScrolledToEnd
import eu.kanade.presentation.util.isScrolledToStart
import eu.kanade.presentation.util.minimumTouchTargetSize
@Composable
fun <T> ListPreferenceWidget(
@ -86,20 +89,22 @@ private fun DialogRow(
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.selectable(
selected = isSelected,
onClick = { if (!isSelected) onSelected() },
),
)
.fillMaxWidth()
.minimumTouchTargetSize(),
) {
RadioButton(
selected = isSelected,
onClick = { if (!isSelected) onSelected() },
onClick = null,
)
Text(
text = label,
style = MaterialTheme.typography.bodyLarge.merge(),
modifier = Modifier.padding(start = 12.dp),
modifier = Modifier.padding(start = 24.dp),
)
}
}

View File

@ -1,10 +1,11 @@
package eu.kanade.presentation.more.settings.widget
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme
@ -16,10 +17,12 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.util.minimumTouchTargetSize
@Composable
fun MultiSelectListPreferenceWidget(
@ -59,17 +62,22 @@ fun MultiSelectListPreferenceWidget(
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable { onSelectionChanged() },
.clip(RoundedCornerShape(8.dp))
.selectable(
selected = isSelected,
onClick = { onSelectionChanged() },
)
.minimumTouchTargetSize()
.fillMaxWidth(),
) {
Checkbox(
checked = isSelected,
onCheckedChange = { onSelectionChanged() },
onCheckedChange = null,
)
Text(
text = current.value,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(start = 12.dp),
modifier = Modifier.padding(start = 24.dp),
)
}
}

View File

@ -21,7 +21,7 @@ fun PreferenceGroupHeader(title: String) {
Text(
text = title,
color = MaterialTheme.colorScheme.secondary,
modifier = Modifier.padding(horizontal = 16.dp),
modifier = Modifier.padding(horizontal = 24.dp),
style = MaterialTheme.typography.bodyMedium,
)
}

View File

@ -8,18 +8,20 @@ import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
@Composable
fun TextPreferenceWidget(
modifier: Modifier = Modifier,
title: String,
subtitle: String? = null,
icon: ImageVector? = null,
onPreferenceClick: (() -> Unit)? = null,
) {
// TODO: Handle auth requirement here?
BasePreferenceWidget(
modifier = modifier,
title = title,
subtitle = subtitle,
icon = icon,

View File

@ -22,6 +22,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import eu.kanade.presentation.more.settings.LocalPreferenceHighlighted
@Composable
@ -39,7 +40,7 @@ fun TrackingPreferenceWidget(
modifier = modifier
.clickable(enabled = onClick != null, onClick = { onClick?.invoke() })
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
.padding(horizontal = HorizontalPadding, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Box(
@ -60,7 +61,8 @@ fun TrackingPreferenceWidget(
.weight(1f)
.padding(horizontal = 16.dp),
maxLines = 1,
style = MaterialTheme.typography.titleMedium,
style = MaterialTheme.typography.titleLarge,
fontSize = 20.sp,
)
if (checked) {
Icon(

View File

@ -79,7 +79,7 @@ fun <T> TriStateListDialog(
val state = selected[index]
Row(
modifier = Modifier
.clip(RoundedCornerShape(25))
.clip(RoundedCornerShape(8.dp))
.clickable {
selected[index] = when (state) {
State.UNCHECKED -> State.CHECKED

View File

@ -1,18 +1,23 @@
package eu.kanade.tachiyomi.ui.setting
import android.os.Bundle
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.core.os.bundleOf
import cafe.adriel.voyager.core.stack.StackEvent
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.transitions.ScreenTransition
import eu.kanade.presentation.components.TwoPanelBox
import eu.kanade.presentation.more.settings.screen.SettingsBackupScreen
import eu.kanade.presentation.more.settings.screen.SettingsGeneralScreen
import eu.kanade.presentation.more.settings.screen.SettingsMainScreen
import eu.kanade.presentation.util.LocalBackPress
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.presentation.util.calculateWindowWidthSizeClass
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
import soup.compose.material.motion.animation.materialSharedAxisZ
import soup.compose.material.motion.animation.materialSharedAxisX
import soup.compose.material.motion.animation.rememberSlideDistance
class SettingsMainController : BasicFullComposeController {
@ -25,20 +30,52 @@ class SettingsMainController : BasicFullComposeController {
@Composable
override fun ComposeContent() {
Navigator(
screen = if (toBackupScreen) SettingsBackupScreen() else SettingsMainScreen,
content = {
CompositionLocalProvider(
LocalRouter provides router,
LocalBackPress provides this::back,
CompositionLocalProvider(LocalRouter provides router) {
val widthSizeClass = calculateWindowWidthSizeClass()
if (widthSizeClass == WindowWidthSizeClass.Compact) {
Navigator(
screen = if (toBackupScreen) SettingsBackupScreen() else SettingsMainScreen,
content = {
CompositionLocalProvider(LocalBackPress provides this::back) {
val slideDistance = rememberSlideDistance()
ScreenTransition(
navigator = it,
transition = {
materialSharedAxisX(
forward = it.lastEvent != StackEvent.Pop,
slideDistance = slideDistance,
)
},
)
}
},
)
} else {
Navigator(
screen = if (toBackupScreen) SettingsBackupScreen() else SettingsGeneralScreen(),
) {
ScreenTransition(
navigator = it,
transition = { materialSharedAxisZ(forward = it.lastEvent != StackEvent.Pop) },
TwoPanelBox(
startContent = {
CompositionLocalProvider(LocalBackPress provides this@SettingsMainController::back) {
SettingsMainScreen.Content(twoPane = true)
}
},
endContent = {
val slideDistance = rememberSlideDistance()
ScreenTransition(
navigator = it,
transition = {
materialSharedAxisX(
forward = it.lastEvent != StackEvent.Pop,
slideDistance = slideDistance,
)
},
)
},
)
}
},
)
}
}
}
private fun back() {

View File

@ -8,7 +8,7 @@ flowbinding_version = "1.2.0"
shizuku_version = "12.2.0"
sqldelight = "1.5.4"
leakcanary = "2.9.1"
voyager = "1.0.0-rc02"
voyager = "1.0.0-beta16"
[libraries]
android-shortcut-gradle = "com.github.zellius:android-shortcut-gradle-plugin:0.1.2"

View File

@ -159,6 +159,17 @@
<string name="pref_category_advanced">Advanced</string>
<string name="pref_category_about">About</string>
<string name="pref_general_summary">App language, notifications</string>
<string name="pref_appearance_summary">Theme, date &amp; time format</string>
<string name="pref_library_summary">Categories, global update</string>
<string name="pref_reader_summary">Reading mode, display, navigation</string>
<string name="pref_downloads_summary">Automatic download, download ahead</string>
<string name="pref_tracking_summary">One-way progress sync, enhanced sync</string>
<string name="pref_browse_summary">Sources, extensions, global search</string>
<string name="pref_backup_summary">Manual &amp; automatic backups</string>
<string name="pref_security_summary">App lock, secure screen</string>
<string name="pref_advanced_summary">Dump crash logs, battery optimizations</string>
<!-- General section -->
<string name="pref_category_theme">Theme</string>
<string name="pref_theme_mode">Dark mode</string>