Explorar el Código

More settings stuff (#8226)

* title size

* move about screen to settings

keeping shortcut inside more screen

* more

* shrink texts

* scrollable create backup dialog choices

* search back button

* cleanups

* delay changes that require activity recreate

* lessen horizontal padding
Ivan Iskandar hace 2 años
padre
commit
f5bde3726a
Se han modificado 28 ficheros con 552 adiciones y 649 borrados
  1. 30 28
      app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt
  2. 0 152
      app/src/main/java/eu/kanade/presentation/more/about/AboutScreen.kt
  3. 0 39
      app/src/main/java/eu/kanade/presentation/more/about/LicensesScreen.kt
  4. 1 9
      app/src/main/java/eu/kanade/presentation/more/settings/PreferenceScaffold.kt
  5. 0 31
      app/src/main/java/eu/kanade/presentation/more/settings/database/ClearDatabaseState.kt
  6. 0 73
      app/src/main/java/eu/kanade/presentation/more/settings/database/components/ClearDatabaseContent.kt
  7. 0 31
      app/src/main/java/eu/kanade/presentation/more/settings/database/components/ClearDatabaseDialogs.kt
  8. 0 53
      app/src/main/java/eu/kanade/presentation/more/settings/database/components/ClearDatabaseItem.kt
  9. 0 45
      app/src/main/java/eu/kanade/presentation/more/settings/database/components/ClearDatabaseToolbar.kt
  10. 254 0
      app/src/main/java/eu/kanade/presentation/more/settings/screen/AboutScreen.kt
  11. 63 8
      app/src/main/java/eu/kanade/presentation/more/settings/screen/ClearDatabaseScreen.kt
  12. 43 0
      app/src/main/java/eu/kanade/presentation/more/settings/screen/LicensesScreen.kt
  13. 18 0
      app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt
  14. 33 19
      app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBackupScreen.kt
  15. 12 5
      app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsGeneralScreen.kt
  16. 43 8
      app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsMainScreen.kt
  17. 9 6
      app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSearchScreen.kt
  18. 1 1
      app/src/main/java/eu/kanade/presentation/more/settings/widget/AppThemePreferenceWidget.kt
  19. 8 6
      app/src/main/java/eu/kanade/presentation/more/settings/widget/BasePreferenceWidget.kt
  20. 2 2
      app/src/main/java/eu/kanade/presentation/more/settings/widget/InfoWidget.kt
  21. 1 1
      app/src/main/java/eu/kanade/presentation/more/settings/widget/PreferenceGroupHeader.kt
  22. 2 2
      app/src/main/java/eu/kanade/presentation/more/settings/widget/TextPreferenceWidget.kt
  23. 2 3
      app/src/main/java/eu/kanade/presentation/more/settings/widget/TrackingPreferenceWidget.kt
  24. 0 16
      app/src/main/java/eu/kanade/tachiyomi/App.kt
  25. 0 86
      app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutController.kt
  26. 0 15
      app/src/main/java/eu/kanade/tachiyomi/ui/more/LicensesController.kt
  27. 2 2
      app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreController.kt
  28. 28 8
      app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt

+ 30 - 28
app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt

@@ -13,16 +13,16 @@ import androidx.compose.runtime.Composable
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.vector.rememberVectorPainter
+import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.platform.LocalUriHandler
-import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.res.pluralStringResource
 import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.res.vectorResource
 import eu.kanade.presentation.components.AppStateBanners
 import eu.kanade.presentation.components.Divider
-import eu.kanade.presentation.components.PreferenceRow
 import eu.kanade.presentation.components.ScrollbarLazyColumn
-import eu.kanade.presentation.components.SwitchPreference
+import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
+import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.ui.more.DownloadQueueState
 import eu.kanade.tachiyomi.ui.more.MoreController
@@ -57,26 +57,28 @@ fun MoreScreen(
         }
 
         item {
-            SwitchPreference(
-                preference = presenter.downloadedOnly,
+            SwitchPreferenceWidget(
                 title = stringResource(R.string.label_downloaded_only),
                 subtitle = stringResource(R.string.downloaded_only_summary),
-                painter = rememberVectorPainter(Icons.Outlined.CloudOff),
+                icon = Icons.Outlined.CloudOff,
+                checked = presenter.downloadedOnly.value,
+                onCheckedChanged = { presenter.downloadedOnly.value = it },
             )
         }
         item {
-            SwitchPreference(
-                preference = presenter.incognitoMode,
+            SwitchPreferenceWidget(
                 title = stringResource(R.string.pref_incognito_mode),
                 subtitle = stringResource(R.string.pref_incognito_mode_summary),
-                painter = painterResource(R.drawable.ic_glasses_24dp),
+                icon = ImageVector.vectorResource(R.drawable.ic_glasses_24dp),
+                checked = presenter.incognitoMode.value,
+                onCheckedChanged = { presenter.incognitoMode.value = it },
             )
         }
 
         item { Divider() }
 
         item {
-            PreferenceRow(
+            TextPreferenceWidget(
                 title = stringResource(R.string.label_download_queue),
                 subtitle = when (downloadQueueState) {
                     DownloadQueueState.Stopped -> null
@@ -99,46 +101,46 @@ fun MoreScreen(
                         pluralStringResource(id = R.plurals.download_queue_summary, count = pending, pending)
                     }
                 },
-                painter = rememberVectorPainter(Icons.Outlined.GetApp),
-                onClick = onClickDownloadQueue,
+                icon = Icons.Outlined.GetApp,
+                onPreferenceClick = onClickDownloadQueue,
             )
         }
         item {
-            PreferenceRow(
+            TextPreferenceWidget(
                 title = stringResource(R.string.categories),
-                painter = rememberVectorPainter(Icons.Outlined.Label),
-                onClick = onClickCategories,
+                icon = Icons.Outlined.Label,
+                onPreferenceClick = onClickCategories,
             )
         }
         item {
-            PreferenceRow(
+            TextPreferenceWidget(
                 title = stringResource(R.string.label_backup),
-                painter = rememberVectorPainter(Icons.Outlined.SettingsBackupRestore),
-                onClick = onClickBackupAndRestore,
+                icon = Icons.Outlined.SettingsBackupRestore,
+                onPreferenceClick = onClickBackupAndRestore,
             )
         }
 
         item { Divider() }
 
         item {
-            PreferenceRow(
+            TextPreferenceWidget(
                 title = stringResource(R.string.label_settings),
-                painter = rememberVectorPainter(Icons.Outlined.Settings),
-                onClick = onClickSettings,
+                icon = Icons.Outlined.Settings,
+                onPreferenceClick = onClickSettings,
             )
         }
         item {
-            PreferenceRow(
+            TextPreferenceWidget(
                 title = stringResource(R.string.pref_category_about),
-                painter = rememberVectorPainter(Icons.Outlined.Info),
-                onClick = onClickAbout,
+                icon = Icons.Outlined.Info,
+                onPreferenceClick = onClickAbout,
             )
         }
         item {
-            PreferenceRow(
+            TextPreferenceWidget(
                 title = stringResource(R.string.label_help),
-                painter = rememberVectorPainter(Icons.Outlined.HelpOutline),
-                onClick = { uriHandler.openUri(MoreController.URL_HELP) },
+                icon = Icons.Outlined.HelpOutline,
+                onPreferenceClick = { uriHandler.openUri(MoreController.URL_HELP) },
             )
         }
     }

+ 0 - 152
app/src/main/java/eu/kanade/presentation/more/about/AboutScreen.kt

@@ -1,152 +0,0 @@
-package eu.kanade.presentation.more.about
-
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.outlined.Public
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.vector.rememberVectorPainter
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalUriHandler
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.res.stringResource
-import eu.kanade.presentation.components.AppBar
-import eu.kanade.presentation.components.LinkIcon
-import eu.kanade.presentation.components.PreferenceRow
-import eu.kanade.presentation.components.Scaffold
-import eu.kanade.presentation.components.ScrollbarLazyColumn
-import eu.kanade.presentation.more.LogoHeader
-import eu.kanade.presentation.util.plus
-import eu.kanade.tachiyomi.BuildConfig
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.updater.RELEASE_URL
-import eu.kanade.tachiyomi.util.CrashLogUtil
-import eu.kanade.tachiyomi.util.system.copyToClipboard
-
-@Composable
-fun AboutScreen(
-    navigateUp: () -> Unit,
-    checkVersion: () -> Unit,
-    getFormattedBuildTime: () -> String,
-    onClickLicenses: () -> Unit,
-) {
-    val context = LocalContext.current
-    val uriHandler = LocalUriHandler.current
-
-    Scaffold(
-        topBar = { scrollBehavior ->
-            AppBar(
-                title = stringResource(R.string.pref_category_about),
-                navigateUp = navigateUp,
-                scrollBehavior = scrollBehavior,
-            )
-        },
-    ) { contentPadding ->
-        ScrollbarLazyColumn(
-            contentPadding = contentPadding,
-        ) {
-            item {
-                LogoHeader()
-            }
-
-            item {
-                PreferenceRow(
-                    title = stringResource(R.string.version),
-                    subtitle = when {
-                        BuildConfig.DEBUG -> {
-                            "Debug ${BuildConfig.COMMIT_SHA} (${getFormattedBuildTime()})"
-                        }
-                        BuildConfig.PREVIEW -> {
-                            "Preview r${BuildConfig.COMMIT_COUNT} (${BuildConfig.COMMIT_SHA}, ${getFormattedBuildTime()})"
-                        }
-                        else -> {
-                            "Stable ${BuildConfig.VERSION_NAME} (${getFormattedBuildTime()})"
-                        }
-                    },
-                    onClick = {
-                        val deviceInfo = CrashLogUtil(context).getDebugInfo()
-                        context.copyToClipboard("Debug information", deviceInfo)
-                    },
-                )
-            }
-
-            if (BuildConfig.INCLUDE_UPDATER) {
-                item {
-                    PreferenceRow(
-                        title = stringResource(R.string.check_for_updates),
-                        onClick = checkVersion,
-                    )
-                }
-            }
-            if (!BuildConfig.DEBUG) {
-                item {
-                    PreferenceRow(
-                        title = stringResource(R.string.whats_new),
-                        onClick = { uriHandler.openUri(RELEASE_URL) },
-                    )
-                }
-            }
-
-            item {
-                PreferenceRow(
-                    title = stringResource(R.string.help_translate),
-                    onClick = { uriHandler.openUri("https://tachiyomi.org/help/contribution/#translation") },
-                )
-            }
-
-            item {
-                PreferenceRow(
-                    title = stringResource(R.string.licenses),
-                    onClick = onClickLicenses,
-                )
-            }
-
-            item {
-                PreferenceRow(
-                    title = stringResource(R.string.privacy_policy),
-                    onClick = { uriHandler.openUri("https://tachiyomi.org/privacy") },
-                )
-            }
-
-            item {
-                Row(
-                    modifier = Modifier.fillMaxWidth(),
-                    horizontalArrangement = Arrangement.Center,
-                ) {
-                    LinkIcon(
-                        label = stringResource(R.string.website),
-                        painter = rememberVectorPainter(Icons.Outlined.Public),
-                        url = "https://tachiyomi.org",
-                    )
-                    LinkIcon(
-                        label = "Discord",
-                        painter = painterResource(R.drawable.ic_discord_24dp),
-                        url = "https://discord.gg/tachiyomi",
-                    )
-                    LinkIcon(
-                        label = "Twitter",
-                        painter = painterResource(R.drawable.ic_twitter_24dp),
-                        url = "https://twitter.com/tachiyomiorg",
-                    )
-                    LinkIcon(
-                        label = "Facebook",
-                        painter = painterResource(R.drawable.ic_facebook_24dp),
-                        url = "https://facebook.com/tachiyomiorg",
-                    )
-                    LinkIcon(
-                        label = "Reddit",
-                        painter = painterResource(R.drawable.ic_reddit_24dp),
-                        url = "https://www.reddit.com/r/Tachiyomi",
-                    )
-                    LinkIcon(
-                        label = "GitHub",
-                        painter = painterResource(R.drawable.ic_github_24dp),
-                        url = "https://github.com/tachiyomiorg",
-                    )
-                }
-            }
-        }
-    }
-}

+ 0 - 39
app/src/main/java/eu/kanade/presentation/more/about/LicensesScreen.kt

@@ -1,39 +0,0 @@
-package eu.kanade.presentation.more.about
-
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
-import com.mikepenz.aboutlibraries.ui.compose.LibrariesContainer
-import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults
-import eu.kanade.presentation.components.AppBar
-import eu.kanade.presentation.components.Scaffold
-import eu.kanade.tachiyomi.R
-
-@Composable
-fun LicensesScreen(
-    navigateUp: () -> Unit,
-) {
-    Scaffold(
-        topBar = { scrollBehavior ->
-            AppBar(
-                title = stringResource(R.string.licenses),
-                navigateUp = navigateUp,
-                scrollBehavior = scrollBehavior,
-            )
-        },
-    ) { contentPadding ->
-        LibrariesContainer(
-            modifier = Modifier
-                .fillMaxSize(),
-            contentPadding = contentPadding,
-            colors = LibraryDefaults.libraryColors(
-                backgroundColor = MaterialTheme.colorScheme.background,
-                contentColor = MaterialTheme.colorScheme.onBackground,
-                badgeBackgroundColor = MaterialTheme.colorScheme.primary,
-                badgeContentColor = MaterialTheme.colorScheme.onPrimary,
-            ),
-        )
-    }
-}

+ 1 - 9
app/src/main/java/eu/kanade/presentation/more/settings/PreferenceScaffold.kt

@@ -2,7 +2,6 @@ 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
@@ -10,9 +9,7 @@ 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 androidx.compose.ui.unit.dp
 import eu.kanade.presentation.components.Scaffold
 import eu.kanade.tachiyomi.R
 
@@ -26,12 +23,7 @@ fun PreferenceScaffold(
     Scaffold(
         topBar = {
             TopAppBar(
-                title = {
-                    Text(
-                        text = stringResource(id = titleRes),
-                        modifier = Modifier.padding(start = 8.dp),
-                    )
-                },
+                title = { Text(text = stringResource(id = titleRes)) },
                 navigationIcon = {
                     if (onBackPressed != null) {
                         IconButton(onClick = onBackPressed) {

+ 0 - 31
app/src/main/java/eu/kanade/presentation/more/settings/database/ClearDatabaseState.kt

@@ -1,31 +0,0 @@
-package eu.kanade.presentation.more.settings.database
-
-import androidx.compose.runtime.Stable
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import eu.kanade.domain.source.model.SourceWithCount
-
-@Stable
-interface ClearDatabaseState {
-    val items: List<SourceWithCount>
-    val selection: List<Long>
-    val isEmpty: Boolean
-    var dialog: Dialog?
-}
-
-fun ClearDatabaseState(): ClearDatabaseState {
-    return ClearDatabaseStateImpl()
-}
-
-class ClearDatabaseStateImpl : ClearDatabaseState {
-    override var items: List<SourceWithCount> by mutableStateOf(emptyList())
-    override var selection: List<Long> by mutableStateOf(emptyList())
-    override val isEmpty: Boolean by derivedStateOf { items.isEmpty() }
-    override var dialog: Dialog? by mutableStateOf(null)
-}
-
-sealed class Dialog {
-    data class Delete(val sourceIds: List<Long>) : Dialog()
-}

+ 0 - 73
app/src/main/java/eu/kanade/presentation/more/settings/database/components/ClearDatabaseContent.kt

@@ -1,73 +0,0 @@
-package eu.kanade.presentation.more.settings.database.components
-
-import androidx.compose.animation.Crossfade
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.items
-import androidx.compose.material3.Button
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import eu.kanade.domain.source.model.Source
-import eu.kanade.presentation.components.Divider
-import eu.kanade.presentation.components.EmptyScreen
-import eu.kanade.presentation.components.FastScrollLazyColumn
-import eu.kanade.presentation.more.settings.database.ClearDatabaseState
-import eu.kanade.tachiyomi.R
-
-@Composable
-fun ClearDatabaseContent(
-    state: ClearDatabaseState,
-    contentPadding: PaddingValues,
-    onClickSelection: (Source) -> Unit,
-    onClickDelete: () -> Unit,
-) {
-    Crossfade(targetState = state.isEmpty.not()) { _state ->
-        when (_state) {
-            true -> {
-                Column(
-                    modifier = Modifier
-                        .padding(contentPadding)
-                        .fillMaxSize(),
-                ) {
-                    FastScrollLazyColumn(
-                        modifier = Modifier.weight(1f),
-                    ) {
-                        items(state.items) { sourceWithCount ->
-                            ClearDatabaseItem(
-                                source = sourceWithCount.source,
-                                count = sourceWithCount.count,
-                                isSelected = state.selection.contains(sourceWithCount.id),
-                                onClickSelect = { onClickSelection(sourceWithCount.source) },
-                            )
-                        }
-                    }
-
-                    Divider()
-
-                    Button(
-                        modifier = Modifier
-                            .padding(horizontal = 16.dp, vertical = 8.dp)
-                            .fillMaxWidth(),
-                        onClick = onClickDelete,
-                        enabled = state.selection.isNotEmpty(),
-                    ) {
-                        Text(
-                            text = stringResource(R.string.action_delete),
-                            color = MaterialTheme.colorScheme.onPrimary,
-                        )
-                    }
-                }
-            }
-            false -> {
-                EmptyScreen(message = stringResource(R.string.database_clean))
-            }
-        }
-    }
-}

+ 0 - 31
app/src/main/java/eu/kanade/presentation/more/settings/database/components/ClearDatabaseDialogs.kt

@@ -1,31 +0,0 @@
-package eu.kanade.presentation.more.settings.database.components
-
-import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.res.stringResource
-import eu.kanade.tachiyomi.R
-
-@Composable
-fun ClearDatabaseDeleteDialog(
-    onDismissRequest: () -> Unit,
-    onDelete: () -> Unit,
-) {
-    AlertDialog(
-        onDismissRequest = onDismissRequest,
-        confirmButton = {
-            TextButton(onClick = onDelete) {
-                Text(text = stringResource(android.R.string.ok))
-            }
-        },
-        dismissButton = {
-            TextButton(onClick = onDismissRequest) {
-                Text(text = stringResource(android.R.string.cancel))
-            }
-        },
-        text = {
-            Text(text = stringResource(R.string.clear_database_confirmation))
-        },
-    )
-}

+ 0 - 53
app/src/main/java/eu/kanade/presentation/more/settings/database/components/ClearDatabaseItem.kt

@@ -1,53 +0,0 @@
-package eu.kanade.presentation.more.settings.database.components
-
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.Checkbox
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import eu.kanade.domain.source.model.Source
-import eu.kanade.presentation.browse.components.SourceIcon
-import eu.kanade.presentation.util.selectedBackground
-import eu.kanade.tachiyomi.R
-
-@Composable
-fun ClearDatabaseItem(
-    source: Source,
-    count: Long,
-    isSelected: Boolean,
-    onClickSelect: () -> Unit,
-) {
-    Row(
-        modifier = Modifier
-            .selectedBackground(isSelected)
-            .clickable(onClick = onClickSelect)
-            .padding(horizontal = 8.dp)
-            .height(56.dp),
-        verticalAlignment = Alignment.CenterVertically,
-    ) {
-        SourceIcon(source = source)
-        Column(
-            modifier = Modifier
-                .padding(start = 8.dp)
-                .weight(1f),
-        ) {
-            Text(
-                text = source.visualName,
-                style = MaterialTheme.typography.bodyMedium,
-            )
-            Text(text = stringResource(R.string.clear_database_source_item_count, count))
-        }
-        Checkbox(
-            checked = isSelected,
-            onCheckedChange = { onClickSelect() },
-        )
-    }
-}

+ 0 - 45
app/src/main/java/eu/kanade/presentation/more/settings/database/components/ClearDatabaseToolbar.kt

@@ -1,45 +0,0 @@
-package eu.kanade.presentation.more.settings.database.components
-
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.outlined.FlipToBack
-import androidx.compose.material.icons.outlined.SelectAll
-import androidx.compose.material3.TopAppBarScrollBehavior
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.res.stringResource
-import eu.kanade.presentation.components.AppBar
-import eu.kanade.presentation.components.AppBarActions
-import eu.kanade.presentation.more.settings.database.ClearDatabaseState
-import eu.kanade.tachiyomi.R
-
-@Composable
-fun ClearDatabaseToolbar(
-    state: ClearDatabaseState,
-    navigateUp: () -> Unit,
-    onClickSelectAll: () -> Unit,
-    onClickInvertSelection: () -> Unit,
-    scrollBehavior: TopAppBarScrollBehavior,
-) {
-    AppBar(
-        title = stringResource(R.string.pref_clear_database),
-        navigateUp = navigateUp,
-        actions = {
-            if (state.isEmpty.not()) {
-                AppBarActions(
-                    actions = listOf(
-                        AppBar.Action(
-                            title = stringResource(R.string.action_select_all),
-                            icon = Icons.Outlined.SelectAll,
-                            onClick = onClickSelectAll,
-                        ),
-                        AppBar.Action(
-                            title = stringResource(R.string.action_select_all),
-                            icon = Icons.Outlined.FlipToBack,
-                            onClick = onClickInvertSelection,
-                        ),
-                    ),
-                )
-            }
-        },
-        scrollBehavior = scrollBehavior,
-    )
-}

+ 254 - 0
app/src/main/java/eu/kanade/presentation/more/settings/screen/AboutScreen.kt

@@ -0,0 +1,254 @@
+package eu.kanade.presentation.more.settings.screen
+
+import android.content.Context
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Public
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.rememberVectorPainter
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalUriHandler
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import cafe.adriel.voyager.core.screen.Screen
+import cafe.adriel.voyager.navigator.LocalNavigator
+import cafe.adriel.voyager.navigator.currentOrThrow
+import com.bluelinelabs.conductor.Router
+import eu.kanade.domain.ui.UiPreferences
+import eu.kanade.presentation.components.AppBar
+import eu.kanade.presentation.components.LinkIcon
+import eu.kanade.presentation.components.Scaffold
+import eu.kanade.presentation.components.ScrollbarLazyColumn
+import eu.kanade.presentation.more.LogoHeader
+import eu.kanade.presentation.more.about.LicensesScreen
+import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
+import eu.kanade.presentation.util.LocalBackPress
+import eu.kanade.presentation.util.LocalRouter
+import eu.kanade.tachiyomi.BuildConfig
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.updater.AppUpdateChecker
+import eu.kanade.tachiyomi.data.updater.AppUpdateResult
+import eu.kanade.tachiyomi.data.updater.RELEASE_URL
+import eu.kanade.tachiyomi.ui.more.NewUpdateDialogController
+import eu.kanade.tachiyomi.util.CrashLogUtil
+import eu.kanade.tachiyomi.util.lang.toDateTimestampString
+import eu.kanade.tachiyomi.util.lang.withIOContext
+import eu.kanade.tachiyomi.util.lang.withUIContext
+import eu.kanade.tachiyomi.util.system.copyToClipboard
+import eu.kanade.tachiyomi.util.system.logcat
+import eu.kanade.tachiyomi.util.system.toast
+import kotlinx.coroutines.launch
+import logcat.LogPriority
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import java.text.DateFormat
+import java.text.SimpleDateFormat
+import java.util.Locale
+import java.util.TimeZone
+
+class AboutScreen : Screen {
+
+    @Composable
+    override fun Content() {
+        val scope = rememberCoroutineScope()
+        val context = LocalContext.current
+        val uriHandler = LocalUriHandler.current
+        val handleBack = LocalBackPress.current
+        val navigator = LocalNavigator.currentOrThrow
+        val router = LocalRouter.currentOrThrow
+
+        Scaffold(
+            topBar = { scrollBehavior ->
+                AppBar(
+                    title = stringResource(R.string.pref_category_about),
+                    navigateUp = if (handleBack != null) handleBack::invoke else null,
+                    scrollBehavior = scrollBehavior,
+                )
+            },
+        ) { contentPadding ->
+            ScrollbarLazyColumn(
+                contentPadding = contentPadding,
+            ) {
+                item {
+                    LogoHeader()
+                }
+
+                item {
+                    TextPreferenceWidget(
+                        title = stringResource(R.string.version),
+                        subtitle = getVersionName(withBuildDate = true),
+                        onPreferenceClick = {
+                            val deviceInfo = CrashLogUtil(context).getDebugInfo()
+                            context.copyToClipboard("Debug information", deviceInfo)
+                        },
+                    )
+                }
+
+                if (BuildConfig.INCLUDE_UPDATER) {
+                    item {
+                        TextPreferenceWidget(
+                            title = stringResource(R.string.check_for_updates),
+                            onPreferenceClick = {
+                                scope.launch {
+                                    checkVersion(context, router)
+                                }
+                            },
+                        )
+                    }
+                }
+                if (!BuildConfig.DEBUG) {
+                    item {
+                        TextPreferenceWidget(
+                            title = stringResource(R.string.whats_new),
+                            onPreferenceClick = { uriHandler.openUri(RELEASE_URL) },
+                        )
+                    }
+                }
+
+                item {
+                    TextPreferenceWidget(
+                        title = stringResource(R.string.help_translate),
+                        onPreferenceClick = { uriHandler.openUri("https://tachiyomi.org/help/contribution/#translation") },
+                    )
+                }
+
+                item {
+                    TextPreferenceWidget(
+                        title = stringResource(R.string.licenses),
+                        onPreferenceClick = { navigator.push(LicensesScreen()) },
+                    )
+                }
+
+                item {
+                    TextPreferenceWidget(
+                        title = stringResource(R.string.privacy_policy),
+                        onPreferenceClick = { uriHandler.openUri("https://tachiyomi.org/privacy") },
+                    )
+                }
+
+                item {
+                    Row(
+                        modifier = Modifier
+                            .fillMaxWidth()
+                            .padding(vertical = 8.dp),
+                        horizontalArrangement = Arrangement.Center,
+                    ) {
+                        LinkIcon(
+                            label = stringResource(R.string.website),
+                            painter = rememberVectorPainter(Icons.Outlined.Public),
+                            url = "https://tachiyomi.org",
+                        )
+                        LinkIcon(
+                            label = "Discord",
+                            painter = painterResource(R.drawable.ic_discord_24dp),
+                            url = "https://discord.gg/tachiyomi",
+                        )
+                        LinkIcon(
+                            label = "Twitter",
+                            painter = painterResource(R.drawable.ic_twitter_24dp),
+                            url = "https://twitter.com/tachiyomiorg",
+                        )
+                        LinkIcon(
+                            label = "Facebook",
+                            painter = painterResource(R.drawable.ic_facebook_24dp),
+                            url = "https://facebook.com/tachiyomiorg",
+                        )
+                        LinkIcon(
+                            label = "Reddit",
+                            painter = painterResource(R.drawable.ic_reddit_24dp),
+                            url = "https://www.reddit.com/r/Tachiyomi",
+                        )
+                        LinkIcon(
+                            label = "GitHub",
+                            painter = painterResource(R.drawable.ic_github_24dp),
+                            url = "https://github.com/tachiyomiorg",
+                        )
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Checks version and shows a user prompt if an update is available.
+     */
+    private suspend fun checkVersion(context: Context, router: Router) {
+        val updateChecker = AppUpdateChecker()
+        withUIContext {
+            context.toast(R.string.update_check_look_for_updates)
+            try {
+                when (val result = withIOContext { updateChecker.checkForUpdate(context, isUserPrompt = true) }) {
+                    is AppUpdateResult.NewUpdate -> {
+                        NewUpdateDialogController(result).showDialog(router)
+                    }
+                    is AppUpdateResult.NoNewUpdate -> {
+                        context.toast(R.string.update_check_no_new_updates)
+                    }
+                    else -> {}
+                }
+            } catch (e: Exception) {
+                context.toast(e.message)
+                logcat(LogPriority.ERROR, e)
+            }
+        }
+    }
+
+    companion object {
+        fun getVersionName(withBuildDate: Boolean): String {
+            return when {
+                BuildConfig.DEBUG -> {
+                    "Debug ${BuildConfig.COMMIT_SHA}".let {
+                        if (withBuildDate) {
+                            "$it (${getFormattedBuildTime()}"
+                        } else {
+                            it
+                        }
+                    }
+                }
+                BuildConfig.PREVIEW -> {
+                    "Preview r${BuildConfig.COMMIT_COUNT}".let {
+                        if (withBuildDate) {
+                            "$it (${BuildConfig.COMMIT_SHA}, ${getFormattedBuildTime()})"
+                        } else {
+                            "$it (${BuildConfig.COMMIT_SHA})"
+                        }
+                    }
+                }
+                else -> {
+                    "Stable ${BuildConfig.VERSION_NAME}".let {
+                        if (withBuildDate) {
+                            "$it (${getFormattedBuildTime()})"
+                        } else {
+                            it
+                        }
+                    }
+                }
+            }
+        }
+
+        private fun getFormattedBuildTime(): String {
+            return try {
+                val inputDf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'", Locale.US)
+                inputDf.timeZone = TimeZone.getTimeZone("UTC")
+                val buildTime = inputDf.parse(BuildConfig.BUILD_TIME)
+
+                val outputDf = DateFormat.getDateTimeInstance(
+                    DateFormat.MEDIUM,
+                    DateFormat.SHORT,
+                    Locale.getDefault(),
+                )
+                outputDf.timeZone = TimeZone.getDefault()
+
+                buildTime!!.toDateTimestampString(UiPreferences.dateFormat(Injekt.get<UiPreferences>().dateFormat().get()))
+            } catch (e: Exception) {
+                BuildConfig.BUILD_TIME
+            }
+        }
+    }
+}

+ 63 - 8
app/src/main/java/eu/kanade/presentation/more/settings/screen/ClearDatabaseScreen.kt

@@ -1,19 +1,26 @@
 package eu.kanade.presentation.more.settings.screen
 
+import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.lazy.items
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.outlined.FlipToBack
 import androidx.compose.material.icons.outlined.SelectAll
+import androidx.compose.material3.AlertDialog
 import androidx.compose.material3.Button
+import androidx.compose.material3.Checkbox
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.stringResource
@@ -27,6 +34,7 @@ import cafe.adriel.voyager.navigator.currentOrThrow
 import eu.kanade.domain.source.interactor.GetSourcesWithNonLibraryManga
 import eu.kanade.domain.source.model.Source
 import eu.kanade.domain.source.model.SourceWithCount
+import eu.kanade.presentation.browse.components.SourceIcon
 import eu.kanade.presentation.components.AppBar
 import eu.kanade.presentation.components.AppBarActions
 import eu.kanade.presentation.components.Divider
@@ -34,8 +42,7 @@ import eu.kanade.presentation.components.EmptyScreen
 import eu.kanade.presentation.components.FastScrollLazyColumn
 import eu.kanade.presentation.components.LoadingScreen
 import eu.kanade.presentation.components.Scaffold
-import eu.kanade.presentation.more.settings.database.components.ClearDatabaseDeleteDialog
-import eu.kanade.presentation.more.settings.database.components.ClearDatabaseItem
+import eu.kanade.presentation.util.selectedBackground
 import eu.kanade.tachiyomi.Database
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.util.lang.launchIO
@@ -58,13 +65,27 @@ class ClearDatabaseScreen : Screen {
             is ClearDatabaseScreenModel.State.Loading -> LoadingScreen()
             is ClearDatabaseScreenModel.State.Ready -> {
                 if (s.showConfirmation) {
-                    ClearDatabaseDeleteDialog(
+                    AlertDialog(
                         onDismissRequest = model::hideConfirmation,
-                        onDelete = {
-                            model.removeMangaBySourceId()
-                            model.clearSelection()
-                            model.hideConfirmation()
-                            context.toast(R.string.clear_database_completed)
+                        confirmButton = {
+                            TextButton(
+                                onClick = {
+                                    model.removeMangaBySourceId()
+                                    model.clearSelection()
+                                    model.hideConfirmation()
+                                    context.toast(R.string.clear_database_completed)
+                                },
+                            ) {
+                                Text(text = stringResource(android.R.string.ok))
+                            }
+                        },
+                        dismissButton = {
+                            TextButton(onClick = model::hideConfirmation) {
+                                Text(text = stringResource(android.R.string.cancel))
+                            }
+                        },
+                        text = {
+                            Text(text = stringResource(R.string.clear_database_confirmation))
                         },
                     )
                 }
@@ -140,6 +161,40 @@ class ClearDatabaseScreen : Screen {
             }
         }
     }
+
+    @Composable
+    private fun ClearDatabaseItem(
+        source: Source,
+        count: Long,
+        isSelected: Boolean,
+        onClickSelect: () -> Unit,
+    ) {
+        Row(
+            modifier = Modifier
+                .selectedBackground(isSelected)
+                .clickable(onClick = onClickSelect)
+                .padding(horizontal = 8.dp)
+                .height(56.dp),
+            verticalAlignment = Alignment.CenterVertically,
+        ) {
+            SourceIcon(source = source)
+            Column(
+                modifier = Modifier
+                    .padding(start = 8.dp)
+                    .weight(1f),
+            ) {
+                Text(
+                    text = source.visualName,
+                    style = MaterialTheme.typography.bodyMedium,
+                )
+                Text(text = stringResource(R.string.clear_database_source_item_count, count))
+            }
+            Checkbox(
+                checked = isSelected,
+                onCheckedChange = { onClickSelect() },
+            )
+        }
+    }
 }
 
 private class ClearDatabaseScreenModel : StateScreenModel<ClearDatabaseScreenModel.State>(State.Loading) {

+ 43 - 0
app/src/main/java/eu/kanade/presentation/more/settings/screen/LicensesScreen.kt

@@ -0,0 +1,43 @@
+package eu.kanade.presentation.more.about
+
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import cafe.adriel.voyager.core.screen.Screen
+import cafe.adriel.voyager.navigator.LocalNavigator
+import cafe.adriel.voyager.navigator.currentOrThrow
+import com.mikepenz.aboutlibraries.ui.compose.LibrariesContainer
+import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults
+import eu.kanade.presentation.components.AppBar
+import eu.kanade.presentation.components.Scaffold
+import eu.kanade.tachiyomi.R
+
+class LicensesScreen : Screen {
+    @Composable
+    override fun Content() {
+        val navigator = LocalNavigator.currentOrThrow
+        Scaffold(
+            topBar = { scrollBehavior ->
+                AppBar(
+                    title = stringResource(R.string.licenses),
+                    navigateUp = navigator::pop,
+                    scrollBehavior = scrollBehavior,
+                )
+            },
+        ) { contentPadding ->
+            LibrariesContainer(
+                modifier = Modifier
+                    .fillMaxSize(),
+                contentPadding = contentPadding,
+                colors = LibraryDefaults.libraryColors(
+                    backgroundColor = MaterialTheme.colorScheme.background,
+                    contentColor = MaterialTheme.colorScheme.onBackground,
+                    badgeBackgroundColor = MaterialTheme.colorScheme.primary,
+                    badgeContentColor = MaterialTheme.colorScheme.onPrimary,
+                ),
+            )
+        }
+    }
+}

+ 18 - 0
app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt

@@ -4,6 +4,7 @@ import android.app.Activity
 import android.content.Context
 import android.os.Build
 import androidx.annotation.StringRes
+import androidx.appcompat.app.AppCompatDelegate
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.ReadOnlyComposable
@@ -19,6 +20,7 @@ import eu.kanade.presentation.util.collectAsState
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.util.system.isTablet
 import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.debounce
 import kotlinx.coroutines.flow.drop
 import kotlinx.coroutines.flow.merge
 import uy.kohesive.injekt.Injekt
@@ -54,9 +56,25 @@ class SettingsAppearanceScreen : SearchableSettings {
         val appThemePref = uiPreferences.appTheme()
         val amoledPref = uiPreferences.themeDarkAmoled()
 
+        LaunchedEffect(Unit) {
+            themeModePref.changes()
+                .drop(1)
+                .debounce(1000)
+                .collectLatest {
+                    AppCompatDelegate.setDefaultNightMode(
+                        when (it) {
+                            ThemeMode.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
+                            ThemeMode.DARK -> AppCompatDelegate.MODE_NIGHT_YES
+                            ThemeMode.SYSTEM -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
+                        },
+                    )
+                }
+        }
+
         LaunchedEffect(Unit) {
             merge(appThemePref.changes(), amoledPref.changes())
                 .drop(2)
+                .debounce(1000)
                 .collectLatest { (context as? Activity)?.let { ActivityCompat.recreate(it) } }
         }
 

+ 33 - 19
app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBackupScreen.kt

@@ -8,11 +8,12 @@ import androidx.activity.compose.rememberLauncherForActivityResult
 import androidx.activity.result.contract.ActivityResultContracts
 import androidx.annotation.StringRes
 import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.heightIn
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.material3.AlertDialog
 import androidx.compose.material3.Checkbox
 import androidx.compose.material3.MaterialTheme
@@ -39,8 +40,12 @@ import androidx.core.net.toUri
 import com.google.accompanist.permissions.rememberPermissionState
 import com.hippo.unifile.UniFile
 import eu.kanade.domain.backup.service.BackupPreferences
+import eu.kanade.presentation.components.Divider
+import eu.kanade.presentation.components.ScrollbarLazyColumn
 import eu.kanade.presentation.more.settings.Preference
 import eu.kanade.presentation.util.collectAsState
+import eu.kanade.presentation.util.isScrolledToEnd
+import eu.kanade.presentation.util.isScrolledToStart
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.backup.BackupConst
 import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
@@ -148,25 +153,34 @@ class SettingsBackupScreen : SearchableSettings {
             onDismissRequest = onDismissRequest,
             title = { Text(text = stringResource(R.string.backup_choice)) },
             text = {
-                Column {
-                    CreateBackupDialogItem(
-                        isSelected = true,
-                        title = stringResource(R.string.manga),
-                    )
-                    choices.forEach { (k, v) ->
-                        val isSelected = flags.contains(k)
-                        CreateBackupDialogItem(
-                            isSelected = isSelected,
-                            title = stringResource(v),
-                            modifier = Modifier.clickable {
-                                if (isSelected) {
-                                    flags.remove(k)
-                                } else {
-                                    flags.add(k)
-                                }
-                            },
-                        )
+                Box {
+                    val state = rememberLazyListState()
+                    ScrollbarLazyColumn(state = state) {
+                        item {
+                            CreateBackupDialogItem(
+                                isSelected = true,
+                                title = stringResource(R.string.manga),
+                            )
+                        }
+                        choices.forEach { (k, v) ->
+                            item {
+                                val isSelected = flags.contains(k)
+                                CreateBackupDialogItem(
+                                    isSelected = isSelected,
+                                    title = stringResource(v),
+                                    modifier = Modifier.clickable {
+                                        if (isSelected) {
+                                            flags.remove(k)
+                                        } else {
+                                            flags.add(k)
+                                        }
+                                    },
+                                )
+                            }
+                        }
                     }
+                    if (!state.isScrolledToStart()) Divider(modifier = Modifier.align(Alignment.TopCenter))
+                    if (!state.isScrolledToEnd()) Divider(modifier = Modifier.align(Alignment.BottomCenter))
                 }
             },
             dismissButton = {

+ 12 - 5
app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsGeneralScreen.kt

@@ -9,6 +9,7 @@ import androidx.appcompat.app.AppCompatDelegate
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.ReadOnlyComposable
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.stringResource
 import androidx.core.os.LocaleListCompat
@@ -17,6 +18,8 @@ import eu.kanade.domain.library.service.LibraryPreferences
 import eu.kanade.presentation.more.settings.Preference
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.util.system.LocaleHelper
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
 import org.xmlpull.v1.XmlPullParser
 import uy.kohesive.injekt.Injekt
 import uy.kohesive.injekt.api.get
@@ -30,6 +33,7 @@ class SettingsGeneralScreen : SearchableSettings {
 
     @Composable
     override fun getPreferences(): List<Preference> {
+        val scope = rememberCoroutineScope()
         val prefs = remember { Injekt.get<BasePreferences>() }
         val libraryPrefs = remember { Injekt.get<LibraryPreferences>() }
         return mutableListOf<Preference>().apply {
@@ -71,12 +75,15 @@ class SettingsGeneralScreen : SearchableSettings {
                     subtitle = "%s",
                     entries = langs,
                     onValueChanged = { newValue ->
-                        val locale = if (newValue.isEmpty()) {
-                            LocaleListCompat.getEmptyLocaleList()
-                        } else {
-                            LocaleListCompat.forLanguageTags(newValue)
+                        scope.launch {
+                            delay(1000)
+                            val locale = if (newValue.isEmpty()) {
+                                LocaleListCompat.getEmptyLocaleList()
+                            } else {
+                                LocaleListCompat.forLanguageTags(newValue)
+                            }
+                            AppCompatDelegate.setApplicationLocales(locale)
                         }
-                        AppCompatDelegate.setApplicationLocales(locale)
                         true
                     },
                 ),

+ 43 - 8
app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsMainScreen.kt

@@ -4,7 +4,8 @@ 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.lazy.itemsIndexed
+import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.ArrowBack
@@ -13,6 +14,7 @@ import androidx.compose.material.icons.outlined.Code
 import androidx.compose.material.icons.outlined.CollectionsBookmark
 import androidx.compose.material.icons.outlined.Explore
 import androidx.compose.material.icons.outlined.GetApp
+import androidx.compose.material.icons.outlined.Info
 import androidx.compose.material.icons.outlined.Palette
 import androidx.compose.material.icons.outlined.Search
 import androidx.compose.material.icons.outlined.Security
@@ -25,8 +27,11 @@ import androidx.compose.material3.LocalContentColor
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
 import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.rememberTopAppBarState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
@@ -35,7 +40,6 @@ 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
@@ -76,7 +80,9 @@ object SettingsMainScreen : Screen {
         val navigator = LocalNavigator.currentOrThrow
         val backPress = LocalBackPress.currentOrThrow
         val containerColor = if (twoPane) getPalerSurface() else MaterialTheme.colorScheme.surface
+        val topBarState = rememberTopAppBarState()
         Scaffold(
+            topBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(topBarState),
             topBar = { scrollBehavior ->
                 // https://issuetracker.google.com/issues/249688556
                 MaterialTheme(
@@ -114,15 +120,34 @@ object SettingsMainScreen : Screen {
             },
             containerColor = containerColor,
             content = { contentPadding ->
-                LazyColumn(contentPadding = contentPadding) {
-                    items(
+                val state = rememberLazyListState()
+                val indexSelected = if (twoPane) {
+                    items.indexOfFirst { it.screen::class == navigator.items.first()::class }
+                        .also {
+                            LaunchedEffect(Unit) {
+                                state.animateScrollToItem(it)
+                                if (it > 0) {
+                                    // Lift scroll
+                                    topBarState.contentOffset = topBarState.heightOffsetLimit
+                                }
+                            }
+                        }
+                } else {
+                    null
+                }
+
+                LazyColumn(
+                    state = state,
+                    contentPadding = contentPadding,
+                ) {
+                    itemsIndexed(
                         items = items,
-                        key = { it.hashCode() },
-                    ) { item ->
+                        key = { _, item -> item.hashCode() },
+                    ) { index, item ->
+                        val selected = indexSelected == index
                         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))
@@ -141,7 +166,7 @@ object SettingsMainScreen : Screen {
                             TextPreferenceWidget(
                                 modifier = modifier,
                                 title = stringResource(item.titleRes),
-                                subtitle = stringResource(item.subtitleRes),
+                                subtitle = item.formatSubtitle(),
                                 icon = item.icon,
                                 onPreferenceClick = { navigator.navigate(item.screen, twoPane) },
                             )
@@ -160,6 +185,7 @@ object SettingsMainScreen : Screen {
 private data class Item(
     @StringRes val titleRes: Int,
     @StringRes val subtitleRes: Int,
+    val formatSubtitle: @Composable () -> String = { stringResource(subtitleRes) },
     val icon: ImageVector,
     val screen: Screen,
 )
@@ -225,4 +251,13 @@ private val items = listOf(
         icon = Icons.Outlined.Code,
         screen = SettingsAdvancedScreen(),
     ),
+    Item(
+        titleRes = R.string.pref_category_about,
+        subtitleRes = 0,
+        formatSubtitle = {
+            "${stringResource(R.string.app_name)} ${AboutScreen.getVersionName(withBuildDate = false)}"
+        },
+        icon = Icons.Outlined.Info,
+        screen = AboutScreen(),
+    ),
 )

+ 9 - 6
app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSearchScreen.kt

@@ -91,12 +91,15 @@ class SettingsSearchScreen : Screen {
                 Column {
                     TopAppBar(
                         navigationIcon = {
-                            IconButton(onClick = navigator::pop) {
-                                Icon(
-                                    imageVector = Icons.Default.ArrowBack,
-                                    contentDescription = null,
-                                    tint = MaterialTheme.colorScheme.onSurfaceVariant,
-                                )
+                            val canPop = remember { navigator.canPop }
+                            if (canPop) {
+                                IconButton(onClick = navigator::pop) {
+                                    Icon(
+                                        imageVector = Icons.Default.ArrowBack,
+                                        contentDescription = null,
+                                        tint = MaterialTheme.colorScheme.onSurfaceVariant,
+                                    )
+                                }
                             }
                         },
                         title = {

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

@@ -78,7 +78,7 @@ private fun AppThemesList(
         modifier = Modifier
             .animateContentSize()
             .padding(vertical = 8.dp),
-        contentPadding = PaddingValues(horizontal = HorizontalPadding),
+        contentPadding = PaddingValues(horizontal = PrefsHorizontalPadding),
         horizontalArrangement = Arrangement.spacedBy(8.dp),
     ) {
         items(

+ 8 - 6
app/src/main/java/eu/kanade/presentation/more/settings/widget/BasePreferenceWidget.kt

@@ -53,30 +53,30 @@ internal fun BasePreferenceWidget(
         ) {
             if (icon != null) {
                 Box(
-                    modifier = Modifier.padding(start = HorizontalPadding),
+                    modifier = Modifier.padding(start = PrefsHorizontalPadding, end = 8.dp),
                     content = { icon() },
                 )
             }
             Column(
                 modifier = Modifier
                     .weight(1f)
-                    .padding(vertical = 16.dp),
+                    .padding(vertical = PrefsVerticalPadding),
             ) {
                 if (!title.isNullOrBlank()) {
                     Text(
-                        modifier = Modifier.padding(horizontal = HorizontalPadding),
+                        modifier = Modifier.padding(horizontal = PrefsHorizontalPadding),
                         text = title,
                         overflow = TextOverflow.Ellipsis,
                         maxLines = 2,
                         style = MaterialTheme.typography.titleLarge,
-                        fontSize = 20.sp,
+                        fontSize = TitleFontSize,
                     )
                 }
                 subcomponent?.invoke(this)
             }
             if (widget != null) {
                 Box(
-                    modifier = Modifier.padding(end = HorizontalPadding),
+                    modifier = Modifier.padding(end = PrefsHorizontalPadding),
                     content = { widget() },
                 )
             }
@@ -117,4 +117,6 @@ internal fun Modifier.highlightBackground(highlighted: Boolean): Modifier = comp
 }
 
 internal val TrailingWidgetBuffer = 16.dp
-internal val HorizontalPadding = 24.dp
+internal val PrefsHorizontalPadding = 16.dp
+internal val PrefsVerticalPadding = 16.dp
+internal val TitleFontSize = 16.sp

+ 2 - 2
app/src/main/java/eu/kanade/presentation/more/settings/widget/InfoWidget.kt

@@ -23,7 +23,7 @@ import eu.kanade.tachiyomi.R
 internal fun InfoWidget(text: String) {
     Column(
         modifier = Modifier
-            .padding(horizontal = HorizontalPadding, vertical = 16.dp)
+            .padding(horizontal = PrefsHorizontalPadding, vertical = 16.dp)
             .secondaryItemAlpha(),
         verticalArrangement = Arrangement.spacedBy(16.dp),
     ) {
@@ -33,7 +33,7 @@ internal fun InfoWidget(text: String) {
         )
         Text(
             text = text,
-            style = MaterialTheme.typography.bodyMedium,
+            style = MaterialTheme.typography.bodySmall,
         )
     }
 }

+ 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 = 24.dp),
+            modifier = Modifier.padding(horizontal = PrefsHorizontalPadding),
             style = MaterialTheme.typography.bodyMedium,
         )
     }

+ 2 - 2
app/src/main/java/eu/kanade/presentation/more/settings/widget/TextPreferenceWidget.kt

@@ -33,9 +33,9 @@ fun TextPreferenceWidget(
                 Text(
                     text = subtitle,
                     modifier = Modifier
-                        .padding(horizontal = HorizontalPadding)
+                        .padding(horizontal = PrefsHorizontalPadding)
                         .secondaryItemAlpha(),
-                    style = MaterialTheme.typography.bodyMedium,
+                    style = MaterialTheme.typography.bodySmall,
                     maxLines = 10,
                 )
             }

+ 2 - 3
app/src/main/java/eu/kanade/presentation/more/settings/widget/TrackingPreferenceWidget.kt

@@ -22,7 +22,6 @@ 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
@@ -40,7 +39,7 @@ fun TrackingPreferenceWidget(
             modifier = modifier
                 .clickable(enabled = onClick != null, onClick = { onClick?.invoke() })
                 .fillMaxWidth()
-                .padding(horizontal = HorizontalPadding, vertical = 8.dp),
+                .padding(horizontal = PrefsHorizontalPadding, vertical = 8.dp),
             verticalAlignment = Alignment.CenterVertically,
         ) {
             Box(
@@ -62,7 +61,7 @@ fun TrackingPreferenceWidget(
                     .padding(horizontal = 16.dp),
                 maxLines = 1,
                 style = MaterialTheme.typography.titleLarge,
-                fontSize = 20.sp,
+                fontSize = TitleFontSize,
             )
             if (checked) {
                 Icon(

+ 0 - 16
app/src/main/java/eu/kanade/tachiyomi/App.kt

@@ -11,7 +11,6 @@ import android.content.IntentFilter
 import android.os.Build
 import android.os.Looper
 import android.webkit.WebView
-import androidx.appcompat.app.AppCompatDelegate
 import androidx.core.app.NotificationManagerCompat
 import androidx.core.content.getSystemService
 import androidx.glance.appwidget.GlanceAppWidgetManager
@@ -28,8 +27,6 @@ import coil.util.DebugLogger
 import eu.kanade.data.DatabaseHandler
 import eu.kanade.domain.DomainModule
 import eu.kanade.domain.base.BasePreferences
-import eu.kanade.domain.ui.UiPreferences
-import eu.kanade.domain.ui.model.ThemeMode
 import eu.kanade.tachiyomi.crash.CrashActivity
 import eu.kanade.tachiyomi.crash.GlobalExceptionHandler
 import eu.kanade.tachiyomi.data.coil.DomainMangaKeyer
@@ -42,7 +39,6 @@ import eu.kanade.tachiyomi.glance.UpdatesGridGlanceWidget
 import eu.kanade.tachiyomi.network.NetworkHelper
 import eu.kanade.tachiyomi.network.NetworkPreferences
 import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate
-import eu.kanade.tachiyomi.util.preference.asHotFlow
 import eu.kanade.tachiyomi.util.system.WebViewUtil
 import eu.kanade.tachiyomi.util.system.animatorDurationScale
 import eu.kanade.tachiyomi.util.system.isDevFlavor
@@ -67,7 +63,6 @@ import java.security.Security
 class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
 
     private val basePreferences: BasePreferences by injectLazy()
-    private val uiPreferences: UiPreferences by injectLazy()
     private val networkPreferences: NetworkPreferences by injectLazy()
 
     private val disableIncognitoReceiver = DisableIncognitoReceiver()
@@ -126,17 +121,6 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
             }
             .launchIn(ProcessLifecycleOwner.get().lifecycleScope)
 
-        uiPreferences.themeMode()
-            .asHotFlow {
-                AppCompatDelegate.setDefaultNightMode(
-                    when (it) {
-                        ThemeMode.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
-                        ThemeMode.DARK -> AppCompatDelegate.MODE_NIGHT_YES
-                        ThemeMode.SYSTEM -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
-                    },
-                )
-            }.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
-
         // Updates widget update
         Injekt.get<DatabaseHandler>()
             .subscribeToList { updatesViewQueries.updates(after = UpdatesGridGlanceWidget.DateLimit.timeInMillis) }

+ 0 - 86
app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutController.kt

@@ -1,86 +0,0 @@
-package eu.kanade.tachiyomi.ui.more
-
-import androidx.compose.runtime.Composable
-import eu.kanade.domain.ui.UiPreferences
-import eu.kanade.presentation.more.about.AboutScreen
-import eu.kanade.tachiyomi.BuildConfig
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.updater.AppUpdateChecker
-import eu.kanade.tachiyomi.data.updater.AppUpdateResult
-import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
-import eu.kanade.tachiyomi.ui.base.controller.pushController
-import eu.kanade.tachiyomi.util.lang.launchIO
-import eu.kanade.tachiyomi.util.lang.toDateTimestampString
-import eu.kanade.tachiyomi.util.lang.withUIContext
-import eu.kanade.tachiyomi.util.system.logcat
-import eu.kanade.tachiyomi.util.system.toast
-import logcat.LogPriority
-import uy.kohesive.injekt.injectLazy
-import java.text.DateFormat
-import java.text.SimpleDateFormat
-import java.util.Locale
-import java.util.TimeZone
-
-class AboutController : BasicFullComposeController() {
-
-    private val preferences: UiPreferences by injectLazy()
-    private val updateChecker by lazy { AppUpdateChecker() }
-
-    @Composable
-    override fun ComposeContent() {
-        AboutScreen(
-            navigateUp = router::popCurrentController,
-            checkVersion = this::checkVersion,
-            getFormattedBuildTime = this::getFormattedBuildTime,
-            onClickLicenses = { router.pushController(LicensesController()) },
-        )
-    }
-
-    /**
-     * Checks version and shows a user prompt if an update is available.
-     */
-    private fun checkVersion() {
-        if (activity == null) return
-
-        activity!!.toast(R.string.update_check_look_for_updates)
-
-        viewScope.launchIO {
-            try {
-                val result = updateChecker.checkForUpdate(activity!!, isUserPrompt = true)
-                withUIContext {
-                    when (result) {
-                        is AppUpdateResult.NewUpdate -> {
-                            NewUpdateDialogController(result).showDialog(router)
-                        }
-                        is AppUpdateResult.NoNewUpdate -> {
-                            activity?.toast(R.string.update_check_no_new_updates)
-                        }
-                        else -> {}
-                    }
-                }
-            } catch (e: Exception) {
-                withUIContext { activity?.toast(e.message) }
-                logcat(LogPriority.ERROR, e)
-            }
-        }
-    }
-
-    private fun getFormattedBuildTime(): String {
-        return try {
-            val inputDf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'", Locale.US)
-            inputDf.timeZone = TimeZone.getTimeZone("UTC")
-            val buildTime = inputDf.parse(BuildConfig.BUILD_TIME)
-
-            val outputDf = DateFormat.getDateTimeInstance(
-                DateFormat.MEDIUM,
-                DateFormat.SHORT,
-                Locale.getDefault(),
-            )
-            outputDf.timeZone = TimeZone.getDefault()
-
-            buildTime!!.toDateTimestampString(UiPreferences.dateFormat(preferences.dateFormat().get()))
-        } catch (e: Exception) {
-            BuildConfig.BUILD_TIME
-        }
-    }
-}

+ 0 - 15
app/src/main/java/eu/kanade/tachiyomi/ui/more/LicensesController.kt

@@ -1,15 +0,0 @@
-package eu.kanade.tachiyomi.ui.more
-
-import androidx.compose.runtime.Composable
-import eu.kanade.presentation.more.about.LicensesScreen
-import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
-
-class LicensesController : BasicFullComposeController() {
-
-    @Composable
-    override fun ComposeContent() {
-        LicensesScreen(
-            navigateUp = router::popCurrentController,
-        )
-    }
-}

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreController.kt

@@ -21,9 +21,9 @@ class MoreController :
             presenter = presenter,
             onClickDownloadQueue = { router.pushController(DownloadController()) },
             onClickCategories = { router.pushController(CategoryController()) },
-            onClickBackupAndRestore = { router.pushController(SettingsMainController(toBackupScreen = true)) },
+            onClickBackupAndRestore = { router.pushController(SettingsMainController.toBackupScreen()) },
             onClickSettings = { router.pushController(SettingsMainController()) },
-            onClickAbout = { router.pushController(AboutController()) },
+            onClickAbout = { router.pushController(SettingsMainController.toAboutScreen()) },
         )
     }
 

+ 28 - 8
app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt

@@ -9,6 +9,7 @@ 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.AboutScreen
 import eu.kanade.presentation.more.settings.screen.SettingsBackupScreen
 import eu.kanade.presentation.more.settings.screen.SettingsGeneralScreen
 import eu.kanade.presentation.more.settings.screen.SettingsMainScreen
@@ -19,14 +20,10 @@ import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
 import soup.compose.material.motion.animation.materialSharedAxisX
 import soup.compose.material.motion.animation.rememberSlideDistance
 
-class SettingsMainController : BasicFullComposeController {
-
-    @Suppress("unused")
-    constructor(bundle: Bundle) : this(bundle.getBoolean(TO_BACKUP_SCREEN))
-
-    constructor(toBackupScreen: Boolean = false) : super(bundleOf(TO_BACKUP_SCREEN to toBackupScreen))
+class SettingsMainController(bundle: Bundle = bundleOf()) : BasicFullComposeController(bundle) {
 
     private val toBackupScreen = args.getBoolean(TO_BACKUP_SCREEN)
+    private val toAboutScreen = args.getBoolean(TO_ABOUT_SCREEN)
 
     @Composable
     override fun ComposeContent() {
@@ -34,7 +31,13 @@ class SettingsMainController : BasicFullComposeController {
             val widthSizeClass = calculateWindowWidthSizeClass()
             if (widthSizeClass == WindowWidthSizeClass.Compact) {
                 Navigator(
-                    screen = if (toBackupScreen) SettingsBackupScreen() else SettingsMainScreen,
+                    screen = if (toBackupScreen) {
+                        SettingsBackupScreen()
+                    } else if (toAboutScreen) {
+                        AboutScreen()
+                    } else {
+                        SettingsMainScreen
+                    },
                     content = {
                         CompositionLocalProvider(LocalBackPress provides this::back) {
                             val slideDistance = rememberSlideDistance()
@@ -52,7 +55,13 @@ class SettingsMainController : BasicFullComposeController {
                 )
             } else {
                 Navigator(
-                    screen = if (toBackupScreen) SettingsBackupScreen() else SettingsGeneralScreen(),
+                    screen = if (toBackupScreen) {
+                        SettingsBackupScreen()
+                    } else if (toAboutScreen) {
+                        AboutScreen()
+                    } else {
+                        SettingsGeneralScreen()
+                    },
                 ) {
                     TwoPanelBox(
                         startContent = {
@@ -81,6 +90,17 @@ class SettingsMainController : BasicFullComposeController {
     private fun back() {
         activity?.onBackPressed()
     }
+
+    companion object {
+        fun toBackupScreen(): SettingsMainController {
+            return SettingsMainController(bundleOf(TO_BACKUP_SCREEN to true))
+        }
+
+        fun toAboutScreen(): SettingsMainController {
+            return SettingsMainController(bundleOf(TO_ABOUT_SCREEN to true))
+        }
+    }
 }
 
 private const val TO_BACKUP_SCREEN = "to_backup_screen"
+private const val TO_ABOUT_SCREEN = "to_about_screen"