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
This commit is contained in:
Ivan Iskandar 2022-10-18 20:35:10 +07:00 committed by GitHub
parent ea092fa175
commit f5bde3726a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 552 additions and 649 deletions

View File

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

View File

@ -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",
)
}
}
}
}
}

View File

@ -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,
),
)
}
}

View File

@ -2,7 +2,6 @@ package eu.kanade.presentation.more.settings
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -10,9 +9,7 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.Scaffold import eu.kanade.presentation.components.Scaffold
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@ -26,12 +23,7 @@ fun PreferenceScaffold(
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
title = { title = { Text(text = stringResource(id = titleRes)) },
Text(
text = stringResource(id = titleRes),
modifier = Modifier.padding(start = 8.dp),
)
},
navigationIcon = { navigationIcon = {
if (onBackPressed != null) { if (onBackPressed != null) {
IconButton(onClick = onBackPressed) { IconButton(onClick = onBackPressed) {

View File

@ -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()
}

View File

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

View File

@ -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))
},
)
}

View File

@ -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() },
)
}
}

View File

@ -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,
)
}

View File

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

View File

@ -1,19 +1,26 @@
package eu.kanade.presentation.more.settings.screen package eu.kanade.presentation.more.settings.screen
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.FlipToBack import androidx.compose.material.icons.outlined.FlipToBack
import androidx.compose.material.icons.outlined.SelectAll import androidx.compose.material.icons.outlined.SelectAll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource 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.interactor.GetSourcesWithNonLibraryManga
import eu.kanade.domain.source.model.Source import eu.kanade.domain.source.model.Source
import eu.kanade.domain.source.model.SourceWithCount 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.AppBar
import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.Divider 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.FastScrollLazyColumn
import eu.kanade.presentation.components.LoadingScreen import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.Scaffold import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.more.settings.database.components.ClearDatabaseDeleteDialog import eu.kanade.presentation.util.selectedBackground
import eu.kanade.presentation.more.settings.database.components.ClearDatabaseItem
import eu.kanade.tachiyomi.Database import eu.kanade.tachiyomi.Database
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
@ -58,13 +65,27 @@ class ClearDatabaseScreen : Screen {
is ClearDatabaseScreenModel.State.Loading -> LoadingScreen() is ClearDatabaseScreenModel.State.Loading -> LoadingScreen()
is ClearDatabaseScreenModel.State.Ready -> { is ClearDatabaseScreenModel.State.Ready -> {
if (s.showConfirmation) { if (s.showConfirmation) {
ClearDatabaseDeleteDialog( AlertDialog(
onDismissRequest = model::hideConfirmation, onDismissRequest = model::hideConfirmation,
onDelete = { confirmButton = {
model.removeMangaBySourceId() TextButton(
model.clearSelection() onClick = {
model.hideConfirmation() model.removeMangaBySourceId()
context.toast(R.string.clear_database_completed) 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) { private class ClearDatabaseScreenModel : StateScreenModel<ClearDatabaseScreenModel.State>(State.Loading) {

View File

@ -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,
),
)
}
}
}

View File

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

View File

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

View File

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

View File

@ -4,7 +4,8 @@ import androidx.annotation.StringRes
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.padding 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.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.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.CollectionsBookmark
import androidx.compose.material.icons.outlined.Explore import androidx.compose.material.icons.outlined.Explore
import androidx.compose.material.icons.outlined.GetApp 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.Palette
import androidx.compose.material.icons.outlined.Search import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.Security 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.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip 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.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastFirstOrNull
import androidx.core.graphics.ColorUtils import androidx.core.graphics.ColorUtils
import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
@ -76,7 +80,9 @@ object SettingsMainScreen : Screen {
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val backPress = LocalBackPress.currentOrThrow val backPress = LocalBackPress.currentOrThrow
val containerColor = if (twoPane) getPalerSurface() else MaterialTheme.colorScheme.surface val containerColor = if (twoPane) getPalerSurface() else MaterialTheme.colorScheme.surface
val topBarState = rememberTopAppBarState()
Scaffold( Scaffold(
topBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(topBarState),
topBar = { scrollBehavior -> topBar = { scrollBehavior ->
// https://issuetracker.google.com/issues/249688556 // https://issuetracker.google.com/issues/249688556
MaterialTheme( MaterialTheme(
@ -114,15 +120,34 @@ object SettingsMainScreen : Screen {
}, },
containerColor = containerColor, containerColor = containerColor,
content = { contentPadding -> content = { contentPadding ->
LazyColumn(contentPadding = contentPadding) { val state = rememberLazyListState()
items( 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, items = items,
key = { it.hashCode() }, key = { _, item -> item.hashCode() },
) { item -> ) { index, item ->
val selected = indexSelected == index
var modifier: Modifier = Modifier var modifier: Modifier = Modifier
var contentColor = LocalContentColor.current var contentColor = LocalContentColor.current
if (twoPane) { if (twoPane) {
val selected = navigator.items.fastFirstOrNull { it::class == item.screen::class } != null
modifier = Modifier modifier = Modifier
.padding(horizontal = 8.dp) .padding(horizontal = 8.dp)
.clip(RoundedCornerShape(24.dp)) .clip(RoundedCornerShape(24.dp))
@ -141,7 +166,7 @@ object SettingsMainScreen : Screen {
TextPreferenceWidget( TextPreferenceWidget(
modifier = modifier, modifier = modifier,
title = stringResource(item.titleRes), title = stringResource(item.titleRes),
subtitle = stringResource(item.subtitleRes), subtitle = item.formatSubtitle(),
icon = item.icon, icon = item.icon,
onPreferenceClick = { navigator.navigate(item.screen, twoPane) }, onPreferenceClick = { navigator.navigate(item.screen, twoPane) },
) )
@ -160,6 +185,7 @@ object SettingsMainScreen : Screen {
private data class Item( private data class Item(
@StringRes val titleRes: Int, @StringRes val titleRes: Int,
@StringRes val subtitleRes: Int, @StringRes val subtitleRes: Int,
val formatSubtitle: @Composable () -> String = { stringResource(subtitleRes) },
val icon: ImageVector, val icon: ImageVector,
val screen: Screen, val screen: Screen,
) )
@ -225,4 +251,13 @@ private val items = listOf(
icon = Icons.Outlined.Code, icon = Icons.Outlined.Code,
screen = SettingsAdvancedScreen(), 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(),
),
) )

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,
)
}
}

View File

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

View File

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