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 { compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_11
} }
kotlinOptions { kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString() jvmTarget = JavaVersion.VERSION_11.toString()
} }
sqldelight { sqldelight {

View File

@ -56,6 +56,7 @@ import androidx.compose.ui.unit.dp
* @sample androidx.compose.material3.samples.ScaffoldWithSimpleSnackbar * @sample androidx.compose.material3.samples.ScaffoldWithSimpleSnackbar
* *
* Tachiyomi changes: * Tachiyomi changes:
* * Pass scroll behavior to top bar by default
* * Remove height constraint for expanded app bar * * Remove height constraint for expanded app bar
* * Also take account of fab height when providing inner padding * * Also take account of fab height when providing inner padding
* *
@ -80,6 +81,7 @@ import androidx.compose.ui.unit.dp
@Composable @Composable
fun Scaffold( fun Scaffold(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
topBarScrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()),
topBar: @Composable (TopAppBarScrollBehavior) -> Unit = {}, topBar: @Composable (TopAppBarScrollBehavior) -> Unit = {},
bottomBar: @Composable () -> Unit = {}, bottomBar: @Composable () -> Unit = {},
snackbarHost: @Composable () -> Unit = {}, snackbarHost: @Composable () -> Unit = {},
@ -89,21 +91,16 @@ fun Scaffold(
contentColor: Color = contentColorFor(containerColor), contentColor: Color = contentColorFor(containerColor),
content: @Composable (PaddingValues) -> Unit, content: @Composable (PaddingValues) -> Unit,
) { ) {
/**
* Tachiyomi: Pass scroll behavior to topBar
*/
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
androidx.compose.material3.Surface( androidx.compose.material3.Surface(
modifier = Modifier modifier = Modifier
.nestedScroll(scrollBehavior.nestedScrollConnection) .nestedScroll(topBarScrollBehavior.nestedScrollConnection)
.then(modifier), .then(modifier),
color = containerColor, color = containerColor,
contentColor = contentColor, contentColor = contentColor,
) { ) {
ScaffoldLayout( ScaffoldLayout(
fabPosition = floatingActionButtonPosition, fabPosition = floatingActionButtonPosition,
topBar = { topBar(scrollBehavior) }, topBar = { topBar(topBarScrollBehavior) },
bottomBar = bottomBar, bottomBar = bottomBar,
content = content, content = content,
snackbar = snackbarHost, 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.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
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.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets 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.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState 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.LocalHapticFeedback
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.kanade.domain.chapter.model.Chapter import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.presentation.components.ChapterDownloadAction import eu.kanade.presentation.components.ChapterDownloadAction
import eu.kanade.presentation.components.ExtendedFloatingActionButton 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.MangaBottomActionMenu
import eu.kanade.presentation.components.Scaffold import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.components.SwipeRefresh import eu.kanade.presentation.components.SwipeRefresh
import eu.kanade.presentation.components.TwoPanelBox
import eu.kanade.presentation.components.VerticalFastScroller import eu.kanade.presentation.components.VerticalFastScroller
import eu.kanade.presentation.manga.components.ChapterHeader import eu.kanade.presentation.manga.components.ChapterHeader
import eu.kanade.presentation.manga.components.ExpandableMangaDescription import eu.kanade.presentation.manga.components.ExpandableMangaDescription
@ -501,14 +498,10 @@ fun MangaScreenLargeImpl(
} }
}, },
) { contentPadding -> ) { contentPadding ->
BoxWithConstraints(modifier = Modifier.fillMaxSize()) { TwoPanelBox(
val firstWidth = (maxWidth / 2).coerceAtMost(450.dp) startContent = {
val secondWidth = maxWidth - firstWidth
Column( Column(
modifier = Modifier modifier = Modifier
.align(Alignment.TopStart)
.width(firstWidth)
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
) { ) {
MangaInfoBox( MangaInfoBox(
@ -539,12 +532,10 @@ fun MangaScreenLargeImpl(
onTagClicked = onTagClicked, onTagClicked = onTagClicked,
) )
} }
},
endContent = {
VerticalFastScroller( VerticalFastScroller(
listState = chapterListState, listState = chapterListState,
modifier = Modifier
.align(Alignment.TopEnd)
.width(secondWidth),
topContentPadding = contentPadding.calculateTopPadding(), topContentPadding = contentPadding.calculateTopPadding(),
) { ) {
LazyColumn( LazyColumn(
@ -573,7 +564,8 @@ fun MangaScreenLargeImpl(
) )
} }
} }
} },
)
} }
} }
} }

View File

@ -2,25 +2,48 @@ package eu.kanade.presentation.more.settings
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.foundation.layout.RowScope 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.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource 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.presentation.components.Scaffold
import eu.kanade.tachiyomi.R
@Composable @Composable
fun PreferenceScaffold( fun PreferenceScaffold(
@StringRes titleRes: Int, @StringRes titleRes: Int,
actions: @Composable RowScope.() -> Unit = {}, actions: @Composable RowScope.() -> Unit = {},
onBackPressed: () -> Unit = {}, onBackPressed: (() -> Unit)? = null,
itemsProvider: @Composable () -> List<Preference>, itemsProvider: @Composable () -> List<Preference>,
) { ) {
Scaffold( Scaffold(
topBar = { scrollBehavior -> topBar = {
AppBar( TopAppBar(
title = stringResource(titleRes), title = {
navigateUp = onBackPressed, 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, actions = actions,
scrollBehavior = scrollBehavior, scrollBehavior = it,
) )
}, },
content = { contentPadding -> 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.PaddingValues
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -12,7 +11,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed import androidx.compose.ui.util.fastForEachIndexed
import eu.kanade.presentation.components.Divider
import eu.kanade.presentation.components.ScrollbarLazyColumn import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.presentation.more.settings.screen.SearchableSettings import eu.kanade.presentation.more.settings.screen.SearchableSettings
import eu.kanade.presentation.more.settings.widget.PreferenceGroupHeader import eu.kanade.presentation.more.settings.widget.PreferenceGroupHeader
@ -55,9 +53,6 @@ fun PreferenceScreen(
item { item {
Column { Column {
if (i != 0) {
Divider(modifier = Modifier.padding(bottom = 8.dp))
}
PreferenceGroupHeader(title = preference.title) PreferenceGroupHeader(title = preference.title)
} }
} }
@ -68,9 +63,11 @@ fun PreferenceScreen(
) )
} }
item { item {
if (i < items.lastIndex) {
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
} }
} }
}
// Create Preference Item // Create Preference Item
is Preference.PreferenceItem<*> -> item { is Preference.PreferenceItem<*> -> item {

View File

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

View File

@ -1,7 +1,13 @@
package eu.kanade.presentation.more.settings.screen package eu.kanade.presentation.more.settings.screen
import androidx.annotation.StringRes 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.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.outlined.ChromeReaderMode import androidx.compose.material.icons.outlined.ChromeReaderMode
import androidx.compose.material.icons.outlined.Code import androidx.compose.material.icons.outlined.Code
import androidx.compose.material.icons.outlined.CollectionsBookmark 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.SettingsBackupRestore
import androidx.compose.material.icons.outlined.Sync import androidx.compose.material.icons.outlined.Sync
import androidx.compose.material.icons.outlined.Tune 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.Composable
import androidx.compose.runtime.NonRestartableComposable import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.ReadOnlyComposable 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.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.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.more.settings.Preference import eu.kanade.presentation.components.LazyColumn
import eu.kanade.presentation.more.settings.PreferenceScaffold import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
import eu.kanade.presentation.util.LocalBackPress import eu.kanade.presentation.util.LocalBackPress
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
object SettingsMainScreen : SearchableSettings { object SettingsMainScreen : Screen {
@Composable @Composable
@ReadOnlyComposable override fun Content() {
@StringRes Content(twoPane = false)
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()) },
),
)
} }
@Composable @Composable
override fun Content() { 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 navigator = LocalNavigator.currentOrThrow
val backPress = LocalBackPress.currentOrThrow val backPress = LocalBackPress.currentOrThrow
PreferenceScaffold( val containerColor = if (twoPane) getPalerSurface() else MaterialTheme.colorScheme.surface
titleRes = getTitleRes(), 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 = { actions = {
AppBarActions( AppBarActions(
listOf( listOf(
AppBar.Action( AppBar.Action(
title = stringResource(R.string.action_search), title = stringResource(R.string.action_search),
icon = Icons.Outlined.Search, icon = Icons.Outlined.Search,
onClick = { navigator.push(SettingsSearchScreen()) }, onClick = { navigator.navigate(SettingsSearchScreen(), twoPane) },
), ),
), ),
) )
}, },
onBackPressed = backPress::invoke, scrollBehavior = scrollBehavior,
itemsProvider = { getPreferences() }, )
}
},
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) },
) )
} }
} }
}
},
)
}
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, contentPadding = contentPadding,
) { result -> ) { result ->
SearchableSettings.highlightKey = result.highlightKey SearchableSettings.highlightKey = result.highlightKey
navigator.popUntil { it is SettingsMainScreen } navigator.replace(result.route)
navigator.push(result.route)
} }
} }
} }

View File

@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons 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.HelpOutline
import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff 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.OutlinedButton
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -30,6 +30,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
@ -189,7 +190,20 @@ class SettingsTrackingScreen : SearchableSettings {
AlertDialog( AlertDialog(
onDismissRequest = onDismissRequest, 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 = { text = {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
OutlinedTextField( OutlinedTextField(
@ -232,7 +246,6 @@ class SettingsTrackingScreen : SearchableSettings {
} }
}, },
confirmButton = { confirmButton = {
Column {
Button( Button(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
enabled = !processing, enabled = !processing,
@ -258,13 +271,6 @@ class SettingsTrackingScreen : SearchableSettings {
val id = if (processing) R.string.loading else R.string.login val id = if (processing) R.string.loading else R.string.login
Text(text = stringResource(id)) Text(text = stringResource(id))
} }
TextButton(
modifier = Modifier.fillMaxWidth(),
onClick = onDismissRequest,
) {
Text(text = stringResource(android.R.string.cancel))
}
}
}, },
) )
} }

View File

@ -109,7 +109,7 @@ private fun AppThemesList(
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
maxLines = 2, 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.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import eu.kanade.presentation.more.settings.LocalPreferenceHighlighted import eu.kanade.presentation.more.settings.LocalPreferenceHighlighted
import eu.kanade.presentation.util.secondaryItemAlpha import eu.kanade.presentation.util.secondaryItemAlpha
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -54,12 +55,12 @@ internal fun BasePreferenceWidget(
modifier = Modifier modifier = Modifier
.padding( .padding(
start = HorizontalPadding, start = HorizontalPadding,
top = 4.dp, top = 0.dp,
end = HorizontalPadding, end = HorizontalPadding,
) )
.secondaryItemAlpha(), .secondaryItemAlpha(),
color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.bodyMedium,
style = MaterialTheme.typography.bodySmall, maxLines = 10,
) )
} }
} else { } else {
@ -106,15 +107,13 @@ private fun BasePreferenceWidgetImpl(
imageVector = icon, imageVector = icon,
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
.padding(start = HorizontalPadding, end = 12.dp) .padding(start = HorizontalPadding, end = 0.dp),
.secondaryItemAlpha(),
tint = MaterialTheme.colorScheme.onSurface,
) )
} }
Column( Column(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.padding(vertical = 14.dp), .padding(vertical = 16.dp),
) { ) {
if (title.isNotBlank()) { if (title.isNotBlank()) {
Row( Row(
@ -125,7 +124,8 @@ private fun BasePreferenceWidgetImpl(
text = title, text = title,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
maxLines = 2, 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 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.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton import androidx.compose.material3.RadioButton
@ -16,6 +17,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
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.clip
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp 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.components.ScrollbarLazyColumn
import eu.kanade.presentation.util.isScrolledToEnd import eu.kanade.presentation.util.isScrolledToEnd
import eu.kanade.presentation.util.isScrolledToStart import eu.kanade.presentation.util.isScrolledToStart
import eu.kanade.presentation.util.minimumTouchTargetSize
@Composable @Composable
fun <T> ListPreferenceWidget( fun <T> ListPreferenceWidget(
@ -86,20 +89,22 @@ private fun DialogRow(
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .clip(RoundedCornerShape(8.dp))
.selectable( .selectable(
selected = isSelected, selected = isSelected,
onClick = { if (!isSelected) onSelected() }, onClick = { if (!isSelected) onSelected() },
), )
.fillMaxWidth()
.minimumTouchTargetSize(),
) { ) {
RadioButton( RadioButton(
selected = isSelected, selected = isSelected,
onClick = { if (!isSelected) onSelected() }, onClick = null,
) )
Text( Text(
text = label, text = label,
style = MaterialTheme.typography.bodyLarge.merge(), 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 package eu.kanade.presentation.more.settings.widget
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn 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.AlertDialog
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -16,10 +17,12 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.toMutableStateList import androidx.compose.runtime.toMutableStateList
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.clip
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import eu.kanade.presentation.more.settings.Preference import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.util.minimumTouchTargetSize
@Composable @Composable
fun MultiSelectListPreferenceWidget( fun MultiSelectListPreferenceWidget(
@ -59,17 +62,22 @@ fun MultiSelectListPreferenceWidget(
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .clip(RoundedCornerShape(8.dp))
.clickable { onSelectionChanged() }, .selectable(
selected = isSelected,
onClick = { onSelectionChanged() },
)
.minimumTouchTargetSize()
.fillMaxWidth(),
) { ) {
Checkbox( Checkbox(
checked = isSelected, checked = isSelected,
onCheckedChange = { onSelectionChanged() }, onCheckedChange = null,
) )
Text( Text(
text = current.value, text = current.value,
style = MaterialTheme.typography.bodyMedium, 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(
text = title, text = title,
color = MaterialTheme.colorScheme.secondary, color = MaterialTheme.colorScheme.secondary,
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.padding(horizontal = 24.dp),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
) )
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@ flowbinding_version = "1.2.0"
shizuku_version = "12.2.0" shizuku_version = "12.2.0"
sqldelight = "1.5.4" sqldelight = "1.5.4"
leakcanary = "2.9.1" leakcanary = "2.9.1"
voyager = "1.0.0-rc02" voyager = "1.0.0-beta16"
[libraries] [libraries]
android-shortcut-gradle = "com.github.zellius:android-shortcut-gradle-plugin:0.1.2" 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_advanced">Advanced</string>
<string name="pref_category_about">About</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 --> <!-- General section -->
<string name="pref_category_theme">Theme</string> <string name="pref_category_theme">Theme</string>
<string name="pref_theme_mode">Dark mode</string> <string name="pref_theme_mode">Dark mode</string>