Jelajahi Sumber

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
Ivan Iskandar 2 tahun lalu
induk
melakukan
5c5468f9af
21 mengubah file dengan 476 tambahan dan 250 penghapusan
  1. 3 3
      app/build.gradle.kts
  2. 4 7
      app/src/main/java/eu/kanade/presentation/components/Scaffold.kt
  3. 35 0
      app/src/main/java/eu/kanade/presentation/components/TwoPanelBox.kt
  4. 64 72
      app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt
  5. 30 7
      app/src/main/java/eu/kanade/presentation/more/settings/PreferenceScaffold.kt
  6. 3 6
      app/src/main/java/eu/kanade/presentation/more/settings/PreferenceScreen.kt
  7. 2 3
      app/src/main/java/eu/kanade/presentation/more/settings/screen/SearchableSettings.kt
  8. 192 79
      app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsMainScreen.kt
  9. 1 2
      app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSearchScreen.kt
  10. 40 34
      app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt
  11. 1 1
      app/src/main/java/eu/kanade/presentation/more/settings/widget/AppThemePreferenceWidget.kt
  12. 9 9
      app/src/main/java/eu/kanade/presentation/more/settings/widget/BasePreferenceWidget.kt
  13. 9 4
      app/src/main/java/eu/kanade/presentation/more/settings/widget/ListPreferenceWidget.kt
  14. 13 5
      app/src/main/java/eu/kanade/presentation/more/settings/widget/MultiSelectListPreferenceWidget.kt
  15. 1 1
      app/src/main/java/eu/kanade/presentation/more/settings/widget/PreferenceGroupHeader.kt
  16. 3 1
      app/src/main/java/eu/kanade/presentation/more/settings/widget/TextPreferenceWidget.kt
  17. 4 2
      app/src/main/java/eu/kanade/presentation/more/settings/widget/TrackingPreferenceWidget.kt
  18. 1 1
      app/src/main/java/eu/kanade/presentation/more/settings/widget/TriStateListDialog.kt
  19. 49 12
      app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt
  20. 1 1
      gradle/libs.versions.toml
  21. 11 0
      i18n/src/main/res/values/strings.xml

+ 3 - 3
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 {

+ 4 - 7
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,

+ 35 - 0
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,
+        )
+    }
+}

+ 64 - 72
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()),
+                    ) {
+                        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(),
                     ) {
-                        item(
-                            key = MangaScreenItem.CHAPTER_HEADER,
-                            contentType = MangaScreenItem.CHAPTER_HEADER,
+                        LazyColumn(
+                            modifier = Modifier.fillMaxHeight(),
+                            state = chapterListState,
+                            contentPadding = PaddingValues(
+                                top = contentPadding.calculateTopPadding(),
+                                bottom = contentPadding.calculateBottomPadding(),
+                            ),
                         ) {
-                            ChapterHeader(
-                                chapterCount = chapters.size,
-                                onClick = onFilterButtonClicked,
+                            item(
+                                key = MangaScreenItem.CHAPTER_HEADER,
+                                contentType = MangaScreenItem.CHAPTER_HEADER,
+                            ) {
+                                ChapterHeader(
+                                    chapterCount = chapters.size,
+                                    onClick = onFilterButtonClicked,
+                                )
+                            }
+
+                            sharedChapterItems(
+                                chapters = chapters,
+                                onChapterClicked = onChapterClicked,
+                                onDownloadChapter = onDownloadChapter,
+                                onChapterSelected = onChapterSelected,
                             )
                         }
-
-                        sharedChapterItems(
-                            chapters = chapters,
-                            onChapterClicked = onChapterClicked,
-                            onDownloadChapter = onDownloadChapter,
-                            onChapterSelected = onChapterSelected,
-                        )
                     }
-                }
-            }
+                },
+            )
         }
     }
 }

+ 30 - 7
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<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 ->

+ 3 - 6
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))
+                        }
                     }
                 }
 

+ 2 - 3
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() },
         )

+ 192 - 79
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 {
-
+object SettingsMainScreen : Screen {
     @Composable
-    @ReadOnlyComposable
-    @StringRes
-    override fun getTitleRes() = R.string.label_settings
+    override fun Content() {
+        Content(twoPane = false)
+    }
 
     @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()) },
-            ),
-        )
+    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
-    override fun Content() {
+    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(),
+    ),
+)

+ 1 - 2
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)
             }
         }
     }

+ 40 - 34
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))
                 }
             },
         )

+ 1 - 1
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,
                 )
             }
         }

+ 9 - 9
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

+ 9 - 4
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 <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),
         )
     }
 }

+ 13 - 5
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),
                                 )
                             }
                         }

+ 1 - 1
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,
         )
     }

+ 3 - 1
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,

+ 4 - 2
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(

+ 1 - 1
app/src/main/java/eu/kanade/presentation/more/settings/widget/TriStateListDialog.kt

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

+ 49 - 12
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() {

+ 1 - 1
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"

+ 11 - 0
i18n/src/main/res/values/strings.xml

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