From 5c5468f9af74be984d0d9cb79da804370ee0e775 Mon Sep 17 00:00:00 2001 From: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com> Date: Sun, 16 Oct 2022 23:15:01 +0700 Subject: [PATCH] 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 570fec6ea622a7deae44668f4d9c3317699de2aa. https://github.com/adrielcafe/voyager/issues/62 --- app/build.gradle.kts | 6 +- .../presentation/components/Scaffold.kt | 11 +- .../presentation/components/TwoPanelBox.kt | 35 +++ .../kanade/presentation/manga/MangaScreen.kt | 140 +++++---- .../more/settings/PreferenceScaffold.kt | 37 ++- .../more/settings/PreferenceScreen.kt | 9 +- .../settings/screen/SearchableSettings.kt | 5 +- .../settings/screen/SettingsMainScreen.kt | 279 ++++++++++++------ .../settings/screen/SettingsSearchScreen.kt | 3 +- .../settings/screen/SettingsTrackingScreen.kt | 74 ++--- .../widget/AppThemePreferenceWidget.kt | 2 +- .../settings/widget/BasePreferenceWidget.kt | 18 +- .../settings/widget/ListPreferenceWidget.kt | 13 +- .../widget/MultiSelectListPreferenceWidget.kt | 18 +- .../settings/widget/PreferenceGroupHeader.kt | 2 +- .../settings/widget/TextPreferenceWidget.kt | 4 +- .../widget/TrackingPreferenceWidget.kt | 6 +- .../settings/widget/TriStateListDialog.kt | 2 +- .../ui/setting/SettingsMainController.kt | 61 +++- gradle/libs.versions.toml | 2 +- i18n/src/main/res/values/strings.xml | 11 + 21 files changed, 482 insertions(+), 256 deletions(-) create mode 100644 app/src/main/java/eu/kanade/presentation/components/TwoPanelBox.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 683323a060..3f45e2c22e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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 { diff --git a/app/src/main/java/eu/kanade/presentation/components/Scaffold.kt b/app/src/main/java/eu/kanade/presentation/components/Scaffold.kt index 800405c3b3..156f87f002 100644 --- a/app/src/main/java/eu/kanade/presentation/components/Scaffold.kt +++ b/app/src/main/java/eu/kanade/presentation/components/Scaffold.kt @@ -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, diff --git a/app/src/main/java/eu/kanade/presentation/components/TwoPanelBox.kt b/app/src/main/java/eu/kanade/presentation/components/TwoPanelBox.kt new file mode 100644 index 0000000000..37d5a1f830 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/components/TwoPanelBox.kt @@ -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, + ) + } +} diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt index f5c5072b03..424965a585 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt @@ -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, + ) + } + } + }, + ) } } } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceScaffold.kt b/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceScaffold.kt index 079c169f3d..10fa1984f8 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceScaffold.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceScaffold.kt @@ -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, ) { 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 -> diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceScreen.kt index 629c062bf5..0cce2ed918 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceScreen.kt @@ -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)) + } } } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SearchableSettings.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SearchableSettings.kt index 750a13b89e..3ea1bdc64f 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SearchableSettings.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SearchableSettings.kt @@ -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() }, ) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsMainScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsMainScreen.kt index 2bfcc2432d..2817fdbdf5 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsMainScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsMainScreen.kt @@ -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 { - 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(), + ), +) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSearchScreen.kt index fe03e49b71..72bd4084f8 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSearchScreen.kt @@ -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) } } } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt index 85a7527c53..8eeb4054b5 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt @@ -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)) } }, ) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/widget/AppThemePreferenceWidget.kt b/app/src/main/java/eu/kanade/presentation/more/settings/widget/AppThemePreferenceWidget.kt index fecc94ae86..9c20d670b5 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/widget/AppThemePreferenceWidget.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/widget/AppThemePreferenceWidget.kt @@ -109,7 +109,7 @@ private fun AppThemesList( color = MaterialTheme.colorScheme.onSurface, textAlign = TextAlign.Center, maxLines = 2, - style = MaterialTheme.typography.bodySmall, + style = MaterialTheme.typography.bodyMedium, ) } } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/widget/BasePreferenceWidget.kt b/app/src/main/java/eu/kanade/presentation/more/settings/widget/BasePreferenceWidget.kt index b27246d6d7..3685813d71 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/widget/BasePreferenceWidget.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/widget/BasePreferenceWidget.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/widget/ListPreferenceWidget.kt b/app/src/main/java/eu/kanade/presentation/more/settings/widget/ListPreferenceWidget.kt index 8c1d767709..261de06f3b 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/widget/ListPreferenceWidget.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/widget/ListPreferenceWidget.kt @@ -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 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), ) } } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/widget/MultiSelectListPreferenceWidget.kt b/app/src/main/java/eu/kanade/presentation/more/settings/widget/MultiSelectListPreferenceWidget.kt index 5131ec1b20..12074093f3 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/widget/MultiSelectListPreferenceWidget.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/widget/MultiSelectListPreferenceWidget.kt @@ -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), ) } } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/widget/PreferenceGroupHeader.kt b/app/src/main/java/eu/kanade/presentation/more/settings/widget/PreferenceGroupHeader.kt index f825df342a..faa57ca36f 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/widget/PreferenceGroupHeader.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/widget/PreferenceGroupHeader.kt @@ -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, ) } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/widget/TextPreferenceWidget.kt b/app/src/main/java/eu/kanade/presentation/more/settings/widget/TextPreferenceWidget.kt index b670df2260..30f749a2f8 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/widget/TextPreferenceWidget.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/widget/TextPreferenceWidget.kt @@ -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, diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/widget/TrackingPreferenceWidget.kt b/app/src/main/java/eu/kanade/presentation/more/settings/widget/TrackingPreferenceWidget.kt index 2af0497bf9..582453e1a9 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/widget/TrackingPreferenceWidget.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/widget/TrackingPreferenceWidget.kt @@ -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( diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/widget/TriStateListDialog.kt b/app/src/main/java/eu/kanade/presentation/more/settings/widget/TriStateListDialog.kt index 6722e337aa..e9158cbfab 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/widget/TriStateListDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/widget/TriStateListDialog.kt @@ -79,7 +79,7 @@ fun TriStateListDialog( val state = selected[index] Row( modifier = Modifier - .clip(RoundedCornerShape(25)) + .clip(RoundedCornerShape(8.dp)) .clickable { selected[index] = when (state) { State.UNCHECKED -> State.CHECKED diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt index 14d2f97eb4..53580fc1e4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt @@ -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() { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 60ed028bcf..c97f3a1e6f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" diff --git a/i18n/src/main/res/values/strings.xml b/i18n/src/main/res/values/strings.xml index 199e7b843d..d4dd013b05 100644 --- a/i18n/src/main/res/values/strings.xml +++ b/i18n/src/main/res/values/strings.xml @@ -159,6 +159,17 @@ Advanced About + App language, notifications + Theme, date & time format + Categories, global update + Reading mode, display, navigation + Automatic download, download ahead + One-way progress sync, enhanced sync + Sources, extensions, global search + Manual & automatic backups + App lock, secure screen + Dump crash logs, battery optimizations + Theme Dark mode