Full Compose settings (#8201)
* Uses Voyager for navigation. * Replaces every screen inside settings except category editor screen since it's called from several places.
This commit is contained in:
parent
3fdcd636d7
commit
890f1a3c7b
42 changed files with 4904 additions and 80 deletions
|
@ -141,12 +141,12 @@ android {
|
|||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||
}
|
||||
|
||||
sqldelight {
|
||||
|
@ -178,6 +178,7 @@ dependencies {
|
|||
implementation(compose.accompanist.flowlayout)
|
||||
implementation(compose.accompanist.pager.core)
|
||||
implementation(compose.accompanist.pager.indicators)
|
||||
implementation(compose.accompanist.permissions)
|
||||
|
||||
implementation(androidx.paging.runtime)
|
||||
implementation(androidx.paging.compose)
|
||||
|
@ -264,6 +265,9 @@ dependencies {
|
|||
implementation(libs.markwon)
|
||||
implementation(libs.aboutLibraries.compose)
|
||||
implementation(libs.cascade)
|
||||
implementation(libs.numberpicker)
|
||||
implementation(libs.bundles.voyager)
|
||||
implementation(libs.materialmotion.core)
|
||||
|
||||
// Conductor
|
||||
implementation(libs.bundles.conductor)
|
||||
|
@ -315,10 +319,12 @@ tasks {
|
|||
kotlinOptions.freeCompilerArgs += listOf(
|
||||
"-opt-in=coil.annotation.ExperimentalCoilApi",
|
||||
"-opt-in=com.google.accompanist.pager.ExperimentalPagerApi",
|
||||
"-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
|
||||
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
|
||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
||||
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
|
||||
"-opt-in=androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi",
|
||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
|
|
|
@ -23,4 +23,6 @@ class BasePreferences(
|
|||
"extension_installer",
|
||||
if (DeviceUtil.isMiui) PreferenceValues.ExtensionInstaller.LEGACY else PreferenceValues.ExtensionInstaller.PACKAGEINSTALLER,
|
||||
)
|
||||
|
||||
fun acraEnabled() = preferenceStore.getBoolean("acra.enable", true)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
package eu.kanade.presentation.more.settings
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.structuralEqualityPolicy
|
||||
import eu.kanade.domain.track.service.TrackPreferences
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.presentation.more.settings.widget.AppThemePreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.EditTextPreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.ListPreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.MultiSelectListPreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.TrackingPreferenceWidget
|
||||
import eu.kanade.presentation.util.collectAsState
|
||||
import eu.kanade.tachiyomi.core.preference.PreferenceStore
|
||||
import kotlinx.coroutines.launch
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
val LocalPreferenceHighlighted = compositionLocalOf(structuralEqualityPolicy()) { false }
|
||||
|
||||
@Composable
|
||||
fun StatusWrapper(
|
||||
item: Preference.PreferenceItem<*>,
|
||||
highlightKey: String?,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val enabled = item.enabled
|
||||
val highlighted = item.title == highlightKey
|
||||
AnimatedVisibility(
|
||||
visible = enabled,
|
||||
enter = expandVertically() + fadeIn(),
|
||||
exit = shrinkVertically() + fadeOut(),
|
||||
content = {
|
||||
CompositionLocalProvider(
|
||||
LocalPreferenceHighlighted provides highlighted,
|
||||
content = content,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun PreferenceItem(
|
||||
item: Preference.PreferenceItem<*>,
|
||||
highlightKey: String?,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
StatusWrapper(
|
||||
item = item,
|
||||
highlightKey = highlightKey,
|
||||
) {
|
||||
when (item) {
|
||||
is Preference.PreferenceItem.SwitchPreference -> {
|
||||
val value by item.pref.collectAsState()
|
||||
SwitchPreferenceWidget(
|
||||
title = item.title,
|
||||
subtitle = item.subtitle,
|
||||
icon = item.icon,
|
||||
checked = value,
|
||||
onCheckedChanged = { newValue ->
|
||||
scope.launch {
|
||||
if (item.onValueChanged(newValue)) {
|
||||
item.pref.set(newValue)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
is Preference.PreferenceItem.ListPreference<*> -> {
|
||||
val value by item.pref.collectAsState()
|
||||
ListPreferenceWidget(
|
||||
value = value,
|
||||
title = item.title,
|
||||
subtitle = item.subtitle,
|
||||
icon = item.icon,
|
||||
entries = item.entries,
|
||||
onValueChange = { newValue ->
|
||||
scope.launch {
|
||||
if (item.internalOnValueChanged(newValue!!)) {
|
||||
item.internalSet(newValue)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
is Preference.PreferenceItem.BasicListPreference -> {
|
||||
ListPreferenceWidget(
|
||||
value = item.value,
|
||||
title = item.title,
|
||||
subtitle = item.subtitle,
|
||||
icon = item.icon,
|
||||
entries = item.entries,
|
||||
onValueChange = { scope.launch { item.onValueChanged(it) } },
|
||||
)
|
||||
}
|
||||
is Preference.PreferenceItem.MultiSelectListPreference -> {
|
||||
val values by item.pref.collectAsState()
|
||||
MultiSelectListPreferenceWidget(
|
||||
preference = item,
|
||||
values = values,
|
||||
onValuesChange = { newValues ->
|
||||
scope.launch {
|
||||
if (item.onValueChanged(newValues)) {
|
||||
item.pref.set(newValues.toMutableSet())
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
is Preference.PreferenceItem.TextPreference -> {
|
||||
TextPreferenceWidget(
|
||||
title = item.title,
|
||||
subtitle = item.subtitle,
|
||||
icon = item.icon,
|
||||
onPreferenceClick = item.onClick,
|
||||
)
|
||||
}
|
||||
is Preference.PreferenceItem.EditTextPreference -> {
|
||||
val values by item.pref.collectAsState()
|
||||
EditTextPreferenceWidget(
|
||||
title = item.title,
|
||||
subtitle = item.subtitle,
|
||||
icon = item.icon,
|
||||
value = values,
|
||||
onConfirm = {
|
||||
val accepted = item.onValueChanged(it)
|
||||
if (accepted) item.pref.set(it)
|
||||
accepted
|
||||
},
|
||||
)
|
||||
}
|
||||
is Preference.PreferenceItem.AppThemePreference -> {
|
||||
val value by item.pref.collectAsState()
|
||||
val amoled by Injekt.get<UiPreferences>().themeDarkAmoled().collectAsState()
|
||||
AppThemePreferenceWidget(
|
||||
title = item.title,
|
||||
value = value,
|
||||
amoled = amoled,
|
||||
onItemClick = { scope.launch { item.pref.set(it) } },
|
||||
)
|
||||
}
|
||||
is Preference.PreferenceItem.TrackingPreference -> {
|
||||
val uName by Injekt.get<PreferenceStore>()
|
||||
.getString(TrackPreferences.trackUsername(item.service.id))
|
||||
.collectAsState()
|
||||
item.service.run {
|
||||
TrackingPreferenceWidget(
|
||||
title = item.title,
|
||||
logoRes = getLogo(),
|
||||
logoColor = getLogoColor(),
|
||||
checked = uName.isNotEmpty(),
|
||||
onClick = { if (isLogged) item.logout() else item.login() },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
package eu.kanade.presentation.more.settings
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import eu.kanade.domain.ui.model.AppTheme
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.core.preference.Preference as PreferenceData
|
||||
|
||||
sealed class Preference {
|
||||
abstract val title: String
|
||||
abstract val enabled: Boolean
|
||||
|
||||
sealed class PreferenceItem<T> : Preference() {
|
||||
abstract val subtitle: String?
|
||||
abstract val icon: ImageVector?
|
||||
abstract val onValueChanged: suspend (newValue: T) -> Boolean
|
||||
|
||||
/**
|
||||
* A basic [PreferenceItem] that only displays texts.
|
||||
*/
|
||||
data class TextPreference(
|
||||
override val title: String,
|
||||
override val subtitle: String? = null,
|
||||
override val icon: ImageVector? = null,
|
||||
override val enabled: Boolean = true,
|
||||
override val onValueChanged: suspend (newValue: String) -> Boolean = { true },
|
||||
|
||||
val onClick: (() -> Unit)? = null,
|
||||
) : PreferenceItem<String>()
|
||||
|
||||
/**
|
||||
* A [PreferenceItem] that provides a two-state toggleable option.
|
||||
*/
|
||||
data class SwitchPreference(
|
||||
val pref: PreferenceData<Boolean>,
|
||||
override val title: String,
|
||||
override val subtitle: String? = null,
|
||||
override val icon: ImageVector? = null,
|
||||
override val enabled: Boolean = true,
|
||||
override val onValueChanged: suspend (newValue: Boolean) -> Boolean = { true },
|
||||
) : PreferenceItem<Boolean>()
|
||||
|
||||
/**
|
||||
* A [PreferenceItem] that displays a list of entries as a dialog.
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
data class ListPreference<T>(
|
||||
val pref: PreferenceData<T>,
|
||||
override val title: String,
|
||||
override val subtitle: String? = "%s",
|
||||
override val icon: ImageVector? = null,
|
||||
override val enabled: Boolean = true,
|
||||
override val onValueChanged: suspend (newValue: T) -> Boolean = { true },
|
||||
|
||||
val entries: Map<T, String>,
|
||||
) : PreferenceItem<T>() {
|
||||
internal fun internalSet(newValue: Any) = pref.set(newValue as T)
|
||||
internal suspend fun internalOnValueChanged(newValue: Any) = onValueChanged(newValue as T)
|
||||
}
|
||||
|
||||
/**
|
||||
* [ListPreference] but with no connection to a [PreferenceData]
|
||||
*/
|
||||
data class BasicListPreference(
|
||||
val value: String,
|
||||
override val title: String,
|
||||
override val subtitle: String? = "%s",
|
||||
override val icon: ImageVector? = null,
|
||||
override val enabled: Boolean = true,
|
||||
override val onValueChanged: suspend (newValue: String) -> Boolean = { true },
|
||||
|
||||
val entries: Map<String, String>,
|
||||
) : PreferenceItem<String>()
|
||||
|
||||
/**
|
||||
* A [PreferenceItem] that displays a list of entries as a dialog.
|
||||
* Multiple entries can be selected at the same time.
|
||||
*/
|
||||
data class MultiSelectListPreference(
|
||||
val pref: PreferenceData<Set<String>>,
|
||||
override val title: String,
|
||||
override val subtitle: String? = null,
|
||||
override val icon: ImageVector? = null,
|
||||
override val enabled: Boolean = true,
|
||||
override val onValueChanged: suspend (newValue: Set<String>) -> Boolean = { true },
|
||||
|
||||
val entries: Map<String, String>,
|
||||
) : PreferenceItem<Set<String>>()
|
||||
|
||||
/**
|
||||
* A [PreferenceItem] that shows a EditText in the dialog.
|
||||
*/
|
||||
data class EditTextPreference(
|
||||
val pref: PreferenceData<String>,
|
||||
override val title: String,
|
||||
override val subtitle: String? = "%s",
|
||||
override val icon: ImageVector? = null,
|
||||
override val enabled: Boolean = true,
|
||||
override val onValueChanged: suspend (newValue: String) -> Boolean = { true },
|
||||
) : PreferenceItem<String>()
|
||||
|
||||
/**
|
||||
* A [PreferenceItem] that shows previews of [AppTheme] selection.
|
||||
*/
|
||||
data class AppThemePreference(
|
||||
val pref: PreferenceData<AppTheme>,
|
||||
override val title: String,
|
||||
) : PreferenceItem<AppTheme>() {
|
||||
override val enabled: Boolean = true
|
||||
override val subtitle: String? = null
|
||||
override val icon: ImageVector? = null
|
||||
override val onValueChanged: suspend (newValue: AppTheme) -> Boolean = { true }
|
||||
}
|
||||
|
||||
/**
|
||||
* A [PreferenceItem] for individual tracking service.
|
||||
*/
|
||||
data class TrackingPreference(
|
||||
val service: TrackService,
|
||||
override val title: String,
|
||||
val login: () -> Unit,
|
||||
val logout: () -> Unit,
|
||||
) : PreferenceItem<String>() {
|
||||
override val enabled: Boolean = true
|
||||
override val subtitle: String? = null
|
||||
override val icon: ImageVector? = null
|
||||
override val onValueChanged: suspend (newValue: String) -> Boolean = { true }
|
||||
}
|
||||
}
|
||||
|
||||
data class PreferenceGroup(
|
||||
override val title: String,
|
||||
override val enabled: Boolean = true,
|
||||
|
||||
val preferenceItems: List<PreferenceItem<out Any>>,
|
||||
) : Preference()
|
||||
|
||||
companion object {
|
||||
fun infoPreference(info: String) = PreferenceItem.TextPreference(
|
||||
title = "",
|
||||
subtitle = info,
|
||||
icon = Icons.Outlined.Info,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package eu.kanade.presentation.more.settings
|
||||
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.runtime.Composable
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.Scaffold
|
||||
|
||||
@Composable
|
||||
fun PreferenceScaffold(
|
||||
title: String,
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
onBackPressed: () -> Unit = {},
|
||||
itemsProvider: @Composable () -> List<Preference>,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
AppBar(
|
||||
title = title,
|
||||
navigateUp = onBackPressed,
|
||||
actions = actions,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
content = { contentPadding ->
|
||||
PreferenceScreen(
|
||||
items = itemsProvider(),
|
||||
contentPadding = contentPadding,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
package eu.kanade.presentation.more.settings
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import eu.kanade.presentation.components.Divider
|
||||
import eu.kanade.presentation.components.ScrollbarLazyColumn
|
||||
import eu.kanade.presentation.more.settings.screen.SearchableSettings
|
||||
import eu.kanade.presentation.more.settings.widget.PreferenceGroupHeader
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
/**
|
||||
* Preference Screen composable which contains a list of [Preference] items
|
||||
* @param items [Preference] items which should be displayed on the preference screen. An item can be a single [PreferenceItem] or a group ([Preference.PreferenceGroup])
|
||||
* @param modifier [Modifier] to be applied to the preferenceScreen layout
|
||||
*/
|
||||
@Composable
|
||||
fun PreferenceScreen(
|
||||
items: List<Preference>,
|
||||
modifier: Modifier = Modifier,
|
||||
contentPadding: PaddingValues = PaddingValues(0.dp),
|
||||
) {
|
||||
val state = rememberLazyListState()
|
||||
val highlightKey = SearchableSettings.highlightKey
|
||||
if (highlightKey != null) {
|
||||
LaunchedEffect(Unit) {
|
||||
val i = items.findHighlightedIndex(highlightKey)
|
||||
if (i >= 0) {
|
||||
delay(500)
|
||||
state.animateScrollToItem(i)
|
||||
}
|
||||
SearchableSettings.highlightKey = null
|
||||
}
|
||||
}
|
||||
|
||||
ScrollbarLazyColumn(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
items.fastForEachIndexed { i, preference ->
|
||||
when (preference) {
|
||||
// Create Preference Group
|
||||
is Preference.PreferenceGroup -> {
|
||||
if (!preference.enabled) return@fastForEachIndexed
|
||||
|
||||
item {
|
||||
Column {
|
||||
if (i != 0) {
|
||||
Divider(modifier = Modifier.padding(bottom = 8.dp))
|
||||
}
|
||||
PreferenceGroupHeader(title = preference.title)
|
||||
}
|
||||
}
|
||||
items(preference.preferenceItems) { item ->
|
||||
PreferenceItem(
|
||||
item = item,
|
||||
highlightKey = highlightKey,
|
||||
)
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
}
|
||||
|
||||
// Create Preference Item
|
||||
is Preference.PreferenceItem<*> -> item {
|
||||
PreferenceItem(
|
||||
item = preference,
|
||||
highlightKey = highlightKey,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<Preference>.findHighlightedIndex(highlightKey: String): Int {
|
||||
return flatMap {
|
||||
if (it is Preference.PreferenceGroup) {
|
||||
mutableListOf<String?>()
|
||||
.apply {
|
||||
add(null) // Header
|
||||
addAll(it.preferenceItems.map { groupItem -> groupItem.title })
|
||||
add(null) // Spacer
|
||||
}
|
||||
} else {
|
||||
listOf(it.title)
|
||||
}
|
||||
}.indexOfFirst { it == highlightKey }
|
||||
}
|
|
@ -0,0 +1,218 @@
|
|||
package eu.kanade.presentation.more.settings.screen
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.FlipToBack
|
||||
import androidx.compose.material.icons.outlined.SelectAll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.coroutineScope
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.core.screen.Screen
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
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.components.AppBar
|
||||
import eu.kanade.presentation.components.AppBarActions
|
||||
import eu.kanade.presentation.components.Divider
|
||||
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.tachiyomi.Database
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.update
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class ClearDatabaseScreen : Screen {
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val context = LocalContext.current
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val model = rememberScreenModel { ClearDatabaseScreenModel() }
|
||||
val state by model.state.collectAsState()
|
||||
|
||||
when (val s = state) {
|
||||
is ClearDatabaseScreenModel.State.Loading -> LoadingScreen()
|
||||
is ClearDatabaseScreenModel.State.Ready -> {
|
||||
if (s.showConfirmation) {
|
||||
ClearDatabaseDeleteDialog(
|
||||
onDismissRequest = model::hideConfirmation,
|
||||
onDelete = {
|
||||
model.removeMangaBySourceId()
|
||||
model.clearSelection()
|
||||
model.hideConfirmation()
|
||||
context.toast(R.string.clear_database_completed)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
AppBar(
|
||||
title = stringResource(R.string.pref_clear_database),
|
||||
navigateUp = navigator::pop,
|
||||
actions = {
|
||||
if (s.items.isNotEmpty()) {
|
||||
AppBarActions(
|
||||
actions = listOf(
|
||||
AppBar.Action(
|
||||
title = stringResource(R.string.action_select_all),
|
||||
icon = Icons.Outlined.SelectAll,
|
||||
onClick = model::selectAll,
|
||||
),
|
||||
AppBar.Action(
|
||||
title = stringResource(R.string.action_select_all),
|
||||
icon = Icons.Outlined.FlipToBack,
|
||||
onClick = model::invertSelection,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
if (s.items.isEmpty()) {
|
||||
EmptyScreen(
|
||||
message = stringResource(R.string.database_clean),
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
)
|
||||
} else {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(contentPadding)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
FastScrollLazyColumn(
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
items(s.items) { sourceWithCount ->
|
||||
ClearDatabaseItem(
|
||||
source = sourceWithCount.source,
|
||||
count = sourceWithCount.count,
|
||||
isSelected = s.selection.contains(sourceWithCount.id),
|
||||
onClickSelect = { model.toggleSelection(sourceWithCount.source) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
Button(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
.fillMaxWidth(),
|
||||
onClick = model::showConfirmation,
|
||||
enabled = s.selection.isNotEmpty(),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.action_delete),
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ClearDatabaseScreenModel : StateScreenModel<ClearDatabaseScreenModel.State>(State.Loading) {
|
||||
private val getSourcesWithNonLibraryManga: GetSourcesWithNonLibraryManga = Injekt.get()
|
||||
private val database: Database = Injekt.get()
|
||||
|
||||
init {
|
||||
coroutineScope.launchIO {
|
||||
getSourcesWithNonLibraryManga.subscribe()
|
||||
.collectLatest { list ->
|
||||
mutableState.update { old ->
|
||||
val items = list.sortedBy { it.name }
|
||||
when (old) {
|
||||
State.Loading -> State.Ready(items)
|
||||
is State.Ready -> old.copy(items = items)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeMangaBySourceId() {
|
||||
val state = state.value as? State.Ready ?: return
|
||||
database.mangasQueries.deleteMangasNotInLibraryBySourceIds(state.selection)
|
||||
database.historyQueries.removeResettedHistory()
|
||||
}
|
||||
|
||||
fun toggleSelection(source: Source) = mutableState.update { state ->
|
||||
if (state !is State.Ready) return@update state
|
||||
val mutableList = state.selection.toMutableList()
|
||||
if (mutableList.contains(source.id)) {
|
||||
mutableList.remove(source.id)
|
||||
} else {
|
||||
mutableList.add(source.id)
|
||||
}
|
||||
state.copy(selection = mutableList)
|
||||
}
|
||||
|
||||
fun clearSelection() = mutableState.update { state ->
|
||||
if (state !is State.Ready) return@update state
|
||||
state.copy(selection = emptyList())
|
||||
}
|
||||
|
||||
fun selectAll() = mutableState.update { state ->
|
||||
if (state !is State.Ready) return@update state
|
||||
state.copy(selection = state.items.map { it.id })
|
||||
}
|
||||
|
||||
fun invertSelection() = mutableState.update { state ->
|
||||
if (state !is State.Ready) return@update state
|
||||
state.copy(
|
||||
selection = state.items
|
||||
.map { it.id }
|
||||
.filterNot { it in state.selection },
|
||||
)
|
||||
}
|
||||
|
||||
fun showConfirmation() = mutableState.update { state ->
|
||||
if (state !is State.Ready) return@update state
|
||||
state.copy(showConfirmation = true)
|
||||
}
|
||||
|
||||
fun hideConfirmation() = mutableState.update { state ->
|
||||
if (state !is State.Ready) return@update state
|
||||
state.copy(showConfirmation = false)
|
||||
}
|
||||
|
||||
sealed class State {
|
||||
object Loading : State()
|
||||
data class Ready(
|
||||
val items: List<SourceWithCount>,
|
||||
val selection: List<Long> = emptyList(),
|
||||
val showConfirmation: Boolean = false,
|
||||
) : State()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package eu.kanade.presentation.more.settings.screen
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.presentation.category.visualName
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
/**
|
||||
* Returns a string of categories name for settings subtitle
|
||||
*/
|
||||
|
||||
@ReadOnlyComposable
|
||||
@Composable
|
||||
fun getCategoriesLabel(
|
||||
allCategories: List<Category>,
|
||||
included: Set<String>,
|
||||
excluded: Set<String>,
|
||||
): String {
|
||||
val context = LocalContext.current
|
||||
|
||||
val includedCategories = included
|
||||
.mapNotNull { id -> allCategories.find { it.id == id.toLong() } }
|
||||
.sortedBy { it.order }
|
||||
val excludedCategories = excluded
|
||||
.mapNotNull { id -> allCategories.find { it.id == id.toLong() } }
|
||||
.sortedBy { it.order }
|
||||
val allExcluded = excludedCategories.size == allCategories.size
|
||||
|
||||
val includedItemsText = when {
|
||||
// Some selected, but not all
|
||||
includedCategories.isNotEmpty() && includedCategories.size != allCategories.size -> includedCategories.joinToString { it.visualName(context) }
|
||||
// All explicitly selected
|
||||
includedCategories.size == allCategories.size -> stringResource(R.string.all)
|
||||
allExcluded -> stringResource(R.string.none)
|
||||
else -> stringResource(R.string.all)
|
||||
}
|
||||
val excludedItemsText = when {
|
||||
excludedCategories.isEmpty() -> stringResource(R.string.none)
|
||||
allExcluded -> stringResource(R.string.all)
|
||||
else -> excludedCategories.joinToString { it.visualName(context) }
|
||||
}
|
||||
return stringResource(id = R.string.include, includedItemsText) + "\n" +
|
||||
stringResource(id = R.string.exclude, excludedItemsText)
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package eu.kanade.presentation.more.settings.screen
|
||||
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import cafe.adriel.voyager.core.screen.Screen
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.presentation.more.settings.Preference
|
||||
import eu.kanade.presentation.more.settings.PreferenceScaffold
|
||||
import eu.kanade.presentation.util.LocalBackPress
|
||||
|
||||
interface SearchableSettings : Screen {
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
fun getTitle(): String
|
||||
|
||||
@Composable
|
||||
fun getPreferences(): List<Preference>
|
||||
|
||||
@Composable
|
||||
fun RowScope.AppBarAction() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val handleBack = LocalBackPress.currentOrThrow
|
||||
PreferenceScaffold(
|
||||
title = getTitle(),
|
||||
onBackPressed = handleBack::invoke,
|
||||
actions = { AppBarAction() },
|
||||
itemsProvider = { getPreferences() },
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
// HACK: for the background blipping thingy.
|
||||
// The title of the target PreferenceItem
|
||||
// Set before showing the destination screen and reset after
|
||||
// See BasePreferenceWidget.highlightBackground
|
||||
var highlightKey: String? = null
|
||||
}
|
||||
}
|
|
@ -0,0 +1,398 @@
|
|||
package eu.kanade.presentation.more.settings.screen
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.provider.Settings
|
||||
import android.webkit.WebStorage
|
||||
import android.webkit.WebView
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.core.net.toUri
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.domain.library.service.LibraryPreferences
|
||||
import eu.kanade.domain.manga.repository.MangaRepository
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.domain.ui.model.TabletUiMode
|
||||
import eu.kanade.presentation.more.settings.Preference
|
||||
import eu.kanade.presentation.util.collectAsState
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.NetworkPreferences
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_360
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_ADGUARD
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_ALIDNS
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_CONTROLD
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_DNSPOD
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_GOOGLE
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_MULLVAD
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_NJALLA
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_QUAD101
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_QUAD9
|
||||
import eu.kanade.tachiyomi.util.CrashLogUtil
|
||||
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||
import eu.kanade.tachiyomi.util.system.isPackageInstalled
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||
import eu.kanade.tachiyomi.util.system.powerManager
|
||||
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import logcat.LogPriority
|
||||
import rikka.sui.Sui
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
|
||||
class SettingsAdvancedScreen : SearchableSettings {
|
||||
@ReadOnlyComposable
|
||||
@Composable
|
||||
override fun getTitle(): String = stringResource(id = R.string.pref_category_advanced)
|
||||
|
||||
@Composable
|
||||
override fun getPreferences(): List<Preference> {
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val basePreferences = remember { Injekt.get<BasePreferences>() }
|
||||
val networkPreferences = remember { Injekt.get<NetworkPreferences>() }
|
||||
|
||||
return listOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = basePreferences.acraEnabled(),
|
||||
title = stringResource(id = R.string.pref_enable_acra),
|
||||
subtitle = stringResource(id = R.string.pref_acra_summary),
|
||||
enabled = !isDevFlavor,
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(id = R.string.pref_dump_crash_logs),
|
||||
subtitle = stringResource(id = R.string.pref_dump_crash_logs_summary),
|
||||
onClick = {
|
||||
scope.launchNonCancellable {
|
||||
CrashLogUtil(context).dumpLogs()
|
||||
}
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = networkPreferences.verboseLogging(),
|
||||
title = stringResource(id = R.string.pref_verbose_logging),
|
||||
subtitle = stringResource(id = R.string.pref_verbose_logging_summary),
|
||||
onValueChanged = {
|
||||
context.toast(R.string.requires_app_restart)
|
||||
true
|
||||
},
|
||||
),
|
||||
getBackgroundActivityGroup(),
|
||||
getDataGroup(),
|
||||
getNetworkGroup(networkPreferences = networkPreferences),
|
||||
getLibraryGroup(),
|
||||
getExtensionsGroup(basePreferences = basePreferences),
|
||||
getDisplayGroup(),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getBackgroundActivityGroup(): Preference.PreferenceGroup {
|
||||
val context = LocalContext.current
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(id = R.string.label_background_activity),
|
||||
preferenceItems = listOf(
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(id = R.string.pref_disable_battery_optimization),
|
||||
subtitle = stringResource(id = R.string.pref_disable_battery_optimization_summary),
|
||||
onClick = {
|
||||
val packageName: String = context.packageName
|
||||
if (!context.powerManager.isIgnoringBatteryOptimizations(packageName)) {
|
||||
try {
|
||||
@SuppressLint("BatteryLife")
|
||||
val intent = Intent().apply {
|
||||
action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
|
||||
data = "package:$packageName".toUri()
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
context.toast(R.string.battery_optimization_setting_activity_not_found)
|
||||
}
|
||||
} else {
|
||||
context.toast(R.string.battery_optimization_disabled)
|
||||
}
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = "Don't kill my app!",
|
||||
subtitle = stringResource(id = R.string.about_dont_kill_my_app),
|
||||
onClick = { context.openInBrowser("https://dontkillmyapp.com/") },
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getDataGroup(): Preference.PreferenceGroup {
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val libraryPreferences = remember { Injekt.get<LibraryPreferences>() }
|
||||
|
||||
val chapterCache = remember { Injekt.get<ChapterCache>() }
|
||||
var readableSizeSema by remember { mutableStateOf(0) }
|
||||
val readableSize = remember(readableSizeSema) { chapterCache.readableSize }
|
||||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(id = R.string.label_data),
|
||||
preferenceItems = listOf(
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(id = R.string.pref_clear_chapter_cache),
|
||||
subtitle = stringResource(id = R.string.used_cache, readableSize),
|
||||
onClick = {
|
||||
scope.launchNonCancellable {
|
||||
try {
|
||||
val deletedFiles = chapterCache.clear()
|
||||
withUIContext {
|
||||
context.toast(context.getString(R.string.cache_deleted, deletedFiles))
|
||||
readableSizeSema++
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
withUIContext { context.toast(R.string.cache_delete_error) }
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = libraryPreferences.autoClearChapterCache(),
|
||||
title = stringResource(id = R.string.pref_auto_clear_chapter_cache),
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(id = R.string.pref_clear_database),
|
||||
subtitle = stringResource(id = R.string.pref_clear_database_summary),
|
||||
onClick = { navigator.push(ClearDatabaseScreen()) },
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getNetworkGroup(
|
||||
networkPreferences: NetworkPreferences,
|
||||
): Preference.PreferenceGroup {
|
||||
val context = LocalContext.current
|
||||
val networkHelper = remember { Injekt.get<NetworkHelper>() }
|
||||
|
||||
val userAgentPref = networkPreferences.defaultUserAgent()
|
||||
val userAgent by userAgentPref.collectAsState()
|
||||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(id = R.string.label_network),
|
||||
preferenceItems = listOf(
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(id = R.string.pref_clear_cookies),
|
||||
onClick = {
|
||||
networkHelper.cookieManager.removeAll()
|
||||
context.toast(R.string.cookies_cleared)
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(id = R.string.pref_clear_webview_data),
|
||||
onClick = {
|
||||
try {
|
||||
WebView(context).run {
|
||||
setDefaultSettings()
|
||||
clearCache(true)
|
||||
clearFormData()
|
||||
clearHistory()
|
||||
clearSslPreferences()
|
||||
}
|
||||
WebStorage.getInstance().deleteAllData()
|
||||
context.applicationInfo?.dataDir?.let { File("$it/app_webview/").deleteRecursively() }
|
||||
context.toast(R.string.webview_data_deleted)
|
||||
} catch (e: Throwable) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
context.toast(R.string.cache_delete_error)
|
||||
}
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = networkPreferences.dohProvider(),
|
||||
title = stringResource(id = R.string.pref_dns_over_https),
|
||||
entries = mapOf(
|
||||
-1 to stringResource(id = R.string.disabled),
|
||||
PREF_DOH_CLOUDFLARE to "Cloudflare",
|
||||
PREF_DOH_GOOGLE to "Google",
|
||||
PREF_DOH_ADGUARD to "AdGuard",
|
||||
PREF_DOH_QUAD9 to "Quad9",
|
||||
PREF_DOH_ALIDNS to "AliDNS",
|
||||
PREF_DOH_DNSPOD to "DNSPod",
|
||||
PREF_DOH_360 to "360",
|
||||
PREF_DOH_QUAD101 to "Quad 101",
|
||||
PREF_DOH_MULLVAD to "Mullvad",
|
||||
PREF_DOH_CONTROLD to "Control D",
|
||||
PREF_DOH_NJALLA to "Njalla",
|
||||
),
|
||||
onValueChanged = {
|
||||
context.toast(R.string.requires_app_restart)
|
||||
true
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.EditTextPreference(
|
||||
pref = userAgentPref,
|
||||
title = stringResource(id = R.string.pref_user_agent_string),
|
||||
onValueChanged = {
|
||||
if (it.isBlank()) {
|
||||
context.toast(R.string.error_user_agent_string_blank)
|
||||
return@EditTextPreference false
|
||||
}
|
||||
context.toast(R.string.requires_app_restart)
|
||||
true
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(id = R.string.pref_reset_user_agent_string),
|
||||
enabled = remember(userAgent) { userAgent != userAgentPref.defaultValue() },
|
||||
onClick = {
|
||||
userAgentPref.delete()
|
||||
context.toast(R.string.requires_app_restart)
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getLibraryGroup(): Preference.PreferenceGroup {
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val trackManager = remember { Injekt.get<TrackManager>() }
|
||||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(id = R.string.label_library),
|
||||
preferenceItems = listOf(
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(id = R.string.pref_refresh_library_covers),
|
||||
onClick = { LibraryUpdateService.start(context, target = LibraryUpdateService.Target.COVERS) },
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(id = R.string.pref_refresh_library_tracking),
|
||||
subtitle = stringResource(id = R.string.pref_refresh_library_tracking_summary),
|
||||
enabled = trackManager.hasLoggedServices(),
|
||||
onClick = { LibraryUpdateService.start(context, target = LibraryUpdateService.Target.TRACKING) },
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(id = R.string.pref_reset_viewer_flags),
|
||||
subtitle = stringResource(id = R.string.pref_reset_viewer_flags_summary),
|
||||
onClick = {
|
||||
scope.launchNonCancellable {
|
||||
val success = Injekt.get<MangaRepository>().resetViewerFlags()
|
||||
withUIContext {
|
||||
val message = if (success) {
|
||||
R.string.pref_reset_viewer_flags_success
|
||||
} else {
|
||||
R.string.pref_reset_viewer_flags_error
|
||||
}
|
||||
context.toast(message)
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getExtensionsGroup(
|
||||
basePreferences: BasePreferences,
|
||||
): Preference.PreferenceGroup {
|
||||
val context = LocalContext.current
|
||||
var shizukuMissing by rememberSaveable { mutableStateOf(false) }
|
||||
if (shizukuMissing) {
|
||||
val dismiss = { shizukuMissing = false }
|
||||
AlertDialog(
|
||||
onDismissRequest = dismiss,
|
||||
title = { Text(text = stringResource(id = R.string.ext_installer_shizuku)) },
|
||||
text = { Text(text = stringResource(id = R.string.ext_installer_shizuku_unavailable_dialog)) },
|
||||
dismissButton = {
|
||||
TextButton(onClick = dismiss) {
|
||||
Text(text = stringResource(id = android.R.string.cancel))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
dismiss()
|
||||
context.openInBrowser("https://shizuku.rikka.app/download")
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(id = android.R.string.ok))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(id = R.string.label_extensions),
|
||||
preferenceItems = listOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = basePreferences.extensionInstaller(),
|
||||
title = stringResource(id = R.string.ext_installer_pref),
|
||||
entries = PreferenceValues.ExtensionInstaller.values()
|
||||
.run {
|
||||
if (DeviceUtil.isMiui) {
|
||||
filter { it != PreferenceValues.ExtensionInstaller.PACKAGEINSTALLER }
|
||||
} else {
|
||||
toList()
|
||||
}
|
||||
}.associateWith { stringResource(id = it.titleResId) },
|
||||
onValueChanged = {
|
||||
if (it == PreferenceValues.ExtensionInstaller.SHIZUKU &&
|
||||
!(context.isPackageInstalled("moe.shizuku.privileged.api") || Sui.isSui())
|
||||
) {
|
||||
shizukuMissing = true
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getDisplayGroup(): Preference.PreferenceGroup {
|
||||
val context = LocalContext.current
|
||||
val uiPreferences = remember { Injekt.get<UiPreferences>() }
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(id = R.string.pref_category_display),
|
||||
preferenceItems = listOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = uiPreferences.tabletUiMode(),
|
||||
title = stringResource(id = R.string.pref_tablet_ui_mode),
|
||||
entries = TabletUiMode.values().associateWith { stringResource(id = it.titleResId) },
|
||||
onValueChanged = {
|
||||
context.toast(R.string.requires_app_restart)
|
||||
true
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
package eu.kanade.presentation.more.settings.screen
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.core.app.ActivityCompat
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.domain.ui.model.ThemeMode
|
||||
import eu.kanade.presentation.more.settings.Preference
|
||||
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.drop
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.Date
|
||||
|
||||
class SettingsAppearanceScreen : SearchableSettings {
|
||||
|
||||
@ReadOnlyComposable
|
||||
@Composable
|
||||
override fun getTitle(): String = stringResource(id = R.string.pref_category_appearance)
|
||||
|
||||
@Composable
|
||||
override fun getPreferences(): List<Preference> {
|
||||
val context = LocalContext.current
|
||||
val uiPreferences = remember { Injekt.get<UiPreferences>() }
|
||||
val themeModePref = uiPreferences.themeMode()
|
||||
val appThemePref = uiPreferences.appTheme()
|
||||
val amoledPref = uiPreferences.themeDarkAmoled()
|
||||
|
||||
val themeMode by themeModePref.collectAsState()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
merge(appThemePref.changes(), amoledPref.changes())
|
||||
.drop(2)
|
||||
.collectLatest { (context as? Activity)?.let { ActivityCompat.recreate(it) } }
|
||||
}
|
||||
|
||||
return listOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = themeModePref,
|
||||
title = stringResource(id = R.string.pref_category_theme),
|
||||
subtitle = "%s",
|
||||
entries = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
mapOf(
|
||||
ThemeMode.SYSTEM to stringResource(id = R.string.theme_system),
|
||||
ThemeMode.LIGHT to stringResource(id = R.string.theme_light),
|
||||
ThemeMode.DARK to stringResource(id = R.string.theme_dark),
|
||||
)
|
||||
} else {
|
||||
mapOf(
|
||||
ThemeMode.LIGHT to stringResource(id = R.string.theme_light),
|
||||
ThemeMode.DARK to stringResource(id = R.string.theme_dark),
|
||||
)
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.AppThemePreference(
|
||||
title = stringResource(id = R.string.pref_app_theme),
|
||||
pref = appThemePref,
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = amoledPref,
|
||||
title = stringResource(id = R.string.pref_dark_theme_pure_black),
|
||||
enabled = themeMode != ThemeMode.LIGHT,
|
||||
),
|
||||
getNavigationGroup(context = context, uiPreferences = uiPreferences),
|
||||
getTimestampGroup(uiPreferences = uiPreferences),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getNavigationGroup(
|
||||
context: Context,
|
||||
uiPreferences: UiPreferences,
|
||||
): Preference.PreferenceGroup {
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(id = R.string.pref_category_navigation),
|
||||
enabled = remember(context) { context.isTablet() },
|
||||
preferenceItems = listOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = uiPreferences.sideNavIconAlignment(),
|
||||
title = stringResource(id = R.string.pref_side_nav_icon_alignment),
|
||||
subtitle = "%s",
|
||||
entries = mapOf(
|
||||
0 to stringResource(id = R.string.alignment_top),
|
||||
1 to stringResource(id = R.string.alignment_center),
|
||||
2 to stringResource(id = R.string.alignment_bottom),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getTimestampGroup(uiPreferences: UiPreferences): Preference.PreferenceGroup {
|
||||
val now = remember { Date().time }
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(id = R.string.pref_category_timestamps),
|
||||
preferenceItems = listOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = uiPreferences.relativeTime(),
|
||||
title = stringResource(id = R.string.pref_relative_format),
|
||||
subtitle = "%s",
|
||||
entries = mapOf(
|
||||
0 to stringResource(id = R.string.off),
|
||||
2 to stringResource(id = R.string.pref_relative_time_short),
|
||||
7 to stringResource(id = R.string.pref_relative_time_long),
|
||||
),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = uiPreferences.dateFormat(),
|
||||
title = stringResource(id = R.string.pref_date_format),
|
||||
subtitle = "%s",
|
||||
entries = DateFormats
|
||||
.associateWith {
|
||||
val formattedDate = UiPreferences.dateFormat(it).format(now)
|
||||
"${it.ifEmpty { stringResource(id = R.string.label_default) }} ($formattedDate)"
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val DateFormats = listOf(
|
||||
"", // Default
|
||||
"MM/dd/yy",
|
||||
"dd/MM/yy",
|
||||
"yyyy-MM-dd",
|
||||
"dd MMM yyyy",
|
||||
"MMM dd, yyyy",
|
||||
)
|
|
@ -0,0 +1,370 @@
|
|||
package eu.kanade.presentation.more.settings.screen
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.material3.AlertDialog
|
||||
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.LaunchedEffect
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.more.settings.Preference
|
||||
import eu.kanade.presentation.util.collectAsState
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.backup.BackupConst
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
||||
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
|
||||
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.launch
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class SettingsBackupScreen : SearchableSettings {
|
||||
@ReadOnlyComposable
|
||||
@Composable
|
||||
override fun getTitle(): String = stringResource(id = R.string.label_backup)
|
||||
|
||||
@Composable
|
||||
override fun getPreferences(): List<Preference> {
|
||||
val backupPreferences = Injekt.get<BackupPreferences>()
|
||||
|
||||
RequestStoragePermission()
|
||||
|
||||
return listOf(
|
||||
getCreateBackupPref(),
|
||||
getRestoreBackupPref(),
|
||||
getAutomaticBackupGroup(backupPreferences = backupPreferences),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RequestStoragePermission() {
|
||||
val permissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
LaunchedEffect(Unit) {
|
||||
permissionState.launchPermissionRequest()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getCreateBackupPref(): Preference.PreferenceItem.TextPreference {
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
|
||||
var flag by rememberSaveable { mutableStateOf(0) }
|
||||
val chooseBackupDir = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.CreateDocument("application/*"),
|
||||
) {
|
||||
if (it != null) {
|
||||
context.contentResolver.takePersistableUriPermission(
|
||||
it,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
|
||||
)
|
||||
BackupCreatorJob.startNow(context, it, flag)
|
||||
}
|
||||
flag = 0
|
||||
}
|
||||
var showCreateDialog by rememberSaveable { mutableStateOf(false) }
|
||||
if (showCreateDialog) {
|
||||
CreateBackupDialog(
|
||||
onConfirm = {
|
||||
showCreateDialog = false
|
||||
flag = it
|
||||
chooseBackupDir.launch(Backup.getBackupFilename())
|
||||
},
|
||||
onDismissRequest = { showCreateDialog = false },
|
||||
)
|
||||
}
|
||||
|
||||
return Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(id = R.string.pref_create_backup),
|
||||
subtitle = stringResource(id = R.string.pref_create_backup_summ),
|
||||
onClick = {
|
||||
scope.launch {
|
||||
if (!BackupCreatorJob.isManualJobRunning(context)) {
|
||||
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
|
||||
context.toast(R.string.restore_miui_warning, Toast.LENGTH_LONG)
|
||||
}
|
||||
showCreateDialog = true
|
||||
} else {
|
||||
context.toast(R.string.backup_in_progress)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CreateBackupDialog(
|
||||
onConfirm: (flag: Int) -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
val flags = remember { mutableStateListOf<Int>() }
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = { Text(text = stringResource(id = R.string.backup_choice)) },
|
||||
text = {
|
||||
val choices = remember {
|
||||
mapOf(
|
||||
BackupConst.BACKUP_CATEGORY to R.string.categories,
|
||||
BackupConst.BACKUP_CHAPTER to R.string.chapters,
|
||||
BackupConst.BACKUP_TRACK to R.string.track,
|
||||
BackupConst.BACKUP_HISTORY to R.string.history,
|
||||
)
|
||||
}
|
||||
Column {
|
||||
CreateBackupDialogItem(
|
||||
isSelected = true,
|
||||
title = stringResource(id = R.string.manga),
|
||||
)
|
||||
choices.forEach { (k, v) ->
|
||||
val isSelected = flags.contains(k)
|
||||
CreateBackupDialogItem(
|
||||
isSelected = isSelected,
|
||||
title = stringResource(id = v),
|
||||
modifier = Modifier.clickable {
|
||||
if (isSelected) {
|
||||
flags.remove(k)
|
||||
} else {
|
||||
flags.add(k)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(id = android.R.string.cancel))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
val flag = flags.fold(initial = 0, operation = { a, b -> a or b })
|
||||
onConfirm(flag)
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(id = android.R.string.ok))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CreateBackupDialogItem(
|
||||
modifier: Modifier = Modifier,
|
||||
isSelected: Boolean,
|
||||
title: String,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
) {
|
||||
Checkbox(
|
||||
modifier = Modifier.heightIn(min = 48.dp),
|
||||
checked = isSelected,
|
||||
onCheckedChange = null,
|
||||
)
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodyMedium.merge(),
|
||||
modifier = Modifier.padding(start = 24.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getRestoreBackupPref(): Preference.PreferenceItem.TextPreference {
|
||||
val context = LocalContext.current
|
||||
var error by remember { mutableStateOf<Any?>(null) }
|
||||
if (error != null) {
|
||||
val onDismissRequest = { error = null }
|
||||
when (val err = error) {
|
||||
is InvalidRestore -> {
|
||||
val clipboard = LocalClipboardManager.current
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = { Text(text = stringResource(id = R.string.invalid_backup_file)) },
|
||||
text = { Text(text = err.message) },
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
clipboard.setText(AnnotatedString(err.message))
|
||||
context.toast(R.string.copied_to_clipboard)
|
||||
onDismissRequest()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.copy))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(id = android.R.string.ok))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
is MissingRestoreComponents -> {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = { Text(text = stringResource(id = R.string.pref_restore_backup)) },
|
||||
text = {
|
||||
var msg = stringResource(id = R.string.backup_restore_content_full)
|
||||
if (err.sources.isNotEmpty()) {
|
||||
msg += "\n\n${stringResource(R.string.backup_restore_missing_sources)}\n${err.sources.joinToString("\n") { "- $it" }}"
|
||||
}
|
||||
if (err.sources.isNotEmpty()) {
|
||||
msg += "\n\n${stringResource(R.string.backup_restore_missing_trackers)}\n${err.trackers.joinToString("\n") { "- $it" }}"
|
||||
}
|
||||
Text(text = msg)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
BackupRestoreService.start(context, err.uri)
|
||||
onDismissRequest()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.action_restore))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
else -> error = null // Unknown
|
||||
}
|
||||
}
|
||||
|
||||
val chooseBackup = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) {
|
||||
if (it != null) {
|
||||
val results = try {
|
||||
BackupFileValidator().validate(context, it)
|
||||
} catch (e: Exception) {
|
||||
error = InvalidRestore(e.message.toString())
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
|
||||
if (results.missingSources.isEmpty() && results.missingTrackers.isEmpty()) {
|
||||
BackupRestoreService.start(context, it)
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
|
||||
error = MissingRestoreComponents(it, results.missingSources, results.missingTrackers)
|
||||
}
|
||||
}
|
||||
|
||||
return Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(id = R.string.pref_restore_backup),
|
||||
subtitle = stringResource(id = R.string.pref_restore_backup_summ),
|
||||
onClick = {
|
||||
if (!BackupRestoreService.isRunning(context)) {
|
||||
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
|
||||
context.toast(R.string.restore_miui_warning, Toast.LENGTH_LONG)
|
||||
}
|
||||
chooseBackup.launch("*/*")
|
||||
} else {
|
||||
context.toast(R.string.restore_in_progress)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getAutomaticBackupGroup(
|
||||
backupPreferences: BackupPreferences,
|
||||
): Preference.PreferenceGroup {
|
||||
val context = LocalContext.current
|
||||
val backupDirPref = backupPreferences.backupsDirectory()
|
||||
val backupDir by backupDirPref.collectAsState()
|
||||
val pickBackupLocation = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.OpenDocumentTree(),
|
||||
) { uri ->
|
||||
if (uri != null) {
|
||||
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
|
||||
context.contentResolver.takePersistableUriPermission(uri, flags)
|
||||
|
||||
val file = UniFile.fromUri(context, uri)
|
||||
backupDirPref.set(file.uri.toString())
|
||||
}
|
||||
}
|
||||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(id = R.string.pref_backup_service_category),
|
||||
preferenceItems = listOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = backupPreferences.backupInterval(),
|
||||
title = stringResource(id = R.string.pref_backup_interval),
|
||||
entries = mapOf(
|
||||
6 to stringResource(id = R.string.update_6hour),
|
||||
12 to stringResource(id = R.string.update_12hour),
|
||||
24 to stringResource(id = R.string.update_24hour),
|
||||
48 to stringResource(id = R.string.update_48hour),
|
||||
168 to stringResource(id = R.string.update_weekly),
|
||||
),
|
||||
onValueChanged = {
|
||||
BackupCreatorJob.setupTask(context, it)
|
||||
true
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(id = R.string.pref_backup_directory),
|
||||
subtitle = remember(backupDir) {
|
||||
UniFile.fromUri(context, backupDir.toUri()).filePath!! + "/automatic"
|
||||
},
|
||||
onClick = { pickBackupLocation.launch(null) },
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = backupPreferences.numberOfBackups(),
|
||||
title = stringResource(id = R.string.pref_backup_slots),
|
||||
entries = listOf(2, 3, 4, 5).associateWith { it.toString() },
|
||||
),
|
||||
Preference.infoPreference(stringResource(id = R.string.backup_info)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private data class MissingRestoreComponents(
|
||||
val uri: Uri,
|
||||
val sources: List<String>,
|
||||
val trackers: List<String>,
|
||||
)
|
||||
|
||||
data class InvalidRestore(
|
||||
val message: String,
|
||||
)
|
|
@ -0,0 +1,79 @@
|
|||
package eu.kanade.presentation.more.settings.screen
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.presentation.more.settings.Preference
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
||||
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class SettingsBrowseScreen : SearchableSettings {
|
||||
@ReadOnlyComposable
|
||||
@Composable
|
||||
override fun getTitle(): String = stringResource(id = R.string.browse)
|
||||
|
||||
@Composable
|
||||
override fun getPreferences(): List<Preference> {
|
||||
val context = LocalContext.current
|
||||
val sourcePreferences = remember { Injekt.get<SourcePreferences>() }
|
||||
val preferences = remember { Injekt.get<BasePreferences>() }
|
||||
return listOf(
|
||||
Preference.PreferenceGroup(
|
||||
title = stringResource(id = R.string.label_sources),
|
||||
preferenceItems = listOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = sourcePreferences.duplicatePinnedSources(),
|
||||
title = stringResource(id = R.string.pref_duplicate_pinned_sources),
|
||||
subtitle = stringResource(id = R.string.pref_duplicate_pinned_sources_summary),
|
||||
),
|
||||
),
|
||||
),
|
||||
Preference.PreferenceGroup(
|
||||
title = stringResource(id = R.string.label_extensions),
|
||||
preferenceItems = listOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = preferences.automaticExtUpdates(),
|
||||
title = stringResource(id = R.string.pref_enable_automatic_extension_updates),
|
||||
onValueChanged = {
|
||||
ExtensionUpdateJob.setupTask(context, it)
|
||||
true
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Preference.PreferenceGroup(
|
||||
title = stringResource(id = R.string.action_global_search),
|
||||
preferenceItems = listOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = sourcePreferences.searchPinnedSourcesOnly(),
|
||||
title = stringResource(id = R.string.pref_search_pinned_sources_only),
|
||||
),
|
||||
),
|
||||
),
|
||||
Preference.PreferenceGroup(
|
||||
title = stringResource(id = R.string.pref_category_nsfw_content),
|
||||
preferenceItems = listOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = sourcePreferences.showNsfwSource(),
|
||||
title = stringResource(id = R.string.pref_show_nsfw_source),
|
||||
subtitle = stringResource(id = R.string.requires_app_restart),
|
||||
onValueChanged = {
|
||||
(context as FragmentActivity).authenticate(
|
||||
title = context.getString(R.string.pref_category_nsfw_content),
|
||||
)
|
||||
},
|
||||
),
|
||||
Preference.infoPreference(stringResource(id = R.string.parental_controls_info)),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,269 @@
|
|||
package eu.kanade.presentation.more.settings.screen
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Environment
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.core.net.toUri
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.domain.category.interactor.GetCategories
|
||||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.domain.download.service.DownloadPreferences
|
||||
import eu.kanade.presentation.category.visualName
|
||||
import eu.kanade.presentation.more.settings.Preference
|
||||
import eu.kanade.presentation.more.settings.widget.TriStateListDialog
|
||||
import eu.kanade.presentation.util.collectAsState
|
||||
import eu.kanade.tachiyomi.R
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
|
||||
class SettingsDownloadScreen : SearchableSettings {
|
||||
@ReadOnlyComposable
|
||||
@Composable
|
||||
override fun getTitle(): String = stringResource(id = R.string.pref_category_downloads)
|
||||
|
||||
@Composable
|
||||
override fun getPreferences(): List<Preference> {
|
||||
val getCategories = remember { Injekt.get<GetCategories>() }
|
||||
val allCategories by getCategories.subscribe().collectAsState(initial = runBlocking { getCategories.await() })
|
||||
|
||||
val downloadPreferences = remember { Injekt.get<DownloadPreferences>() }
|
||||
return listOf(
|
||||
getDownloadLocationPreference(downloadPreferences = downloadPreferences),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = downloadPreferences.downloadOnlyOverWifi(),
|
||||
title = stringResource(id = R.string.connected_to_wifi),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = downloadPreferences.saveChaptersAsCBZ(),
|
||||
title = stringResource(id = R.string.save_chapter_as_cbz),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = downloadPreferences.splitTallImages(),
|
||||
title = stringResource(id = R.string.split_tall_images),
|
||||
subtitle = stringResource(id = R.string.split_tall_images_summary),
|
||||
),
|
||||
getDeleteChaptersGroup(
|
||||
downloadPreferences = downloadPreferences,
|
||||
categories = allCategories,
|
||||
),
|
||||
getDownloadNewChaptersGroup(
|
||||
downloadPreferences = downloadPreferences,
|
||||
allCategories = allCategories,
|
||||
),
|
||||
getDownloadAheadGroup(downloadPreferences = downloadPreferences),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getDownloadLocationPreference(
|
||||
downloadPreferences: DownloadPreferences,
|
||||
): Preference.PreferenceItem.ListPreference<String> {
|
||||
val context = LocalContext.current
|
||||
val currentDirPref = downloadPreferences.downloadsDirectory()
|
||||
val currentDir by currentDirPref.collectAsState()
|
||||
|
||||
val pickLocation = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.OpenDocumentTree(),
|
||||
) { uri ->
|
||||
if (uri != null) {
|
||||
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
|
||||
context.contentResolver.takePersistableUriPermission(uri, flags)
|
||||
|
||||
val file = UniFile.fromUri(context, uri)
|
||||
currentDirPref.set(file.uri.toString())
|
||||
}
|
||||
}
|
||||
|
||||
val defaultDirPair = rememberDefaultDownloadDir()
|
||||
val customDirEntryKey = currentDir.takeIf { it != defaultDirPair.first } ?: "custom"
|
||||
|
||||
return Preference.PreferenceItem.ListPreference(
|
||||
pref = currentDirPref,
|
||||
title = stringResource(id = R.string.pref_download_directory),
|
||||
subtitle = remember(currentDir) {
|
||||
UniFile.fromUri(context, currentDir.toUri()).filePath!!
|
||||
},
|
||||
entries = mapOf(
|
||||
defaultDirPair,
|
||||
customDirEntryKey to stringResource(id = R.string.custom_dir),
|
||||
),
|
||||
onValueChanged = {
|
||||
val default = it == defaultDirPair.first
|
||||
if (!default) {
|
||||
pickLocation.launch(null)
|
||||
}
|
||||
default // Don't update when non-default chosen
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberDefaultDownloadDir(): Pair<String, String> {
|
||||
val appName = stringResource(id = R.string.app_name)
|
||||
return remember {
|
||||
val file = UniFile.fromFile(
|
||||
File(
|
||||
"${Environment.getExternalStorageDirectory().absolutePath}${File.separator}$appName",
|
||||
"downloads",
|
||||
),
|
||||
)!!
|
||||
file.uri.toString() to file.filePath!!
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getDeleteChaptersGroup(
|
||||
downloadPreferences: DownloadPreferences,
|
||||
categories: List<Category>,
|
||||
): Preference.PreferenceGroup {
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(id = R.string.pref_category_delete_chapters),
|
||||
preferenceItems = listOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = downloadPreferences.removeAfterMarkedAsRead(),
|
||||
title = stringResource(id = R.string.pref_remove_after_marked_as_read),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = downloadPreferences.removeAfterReadSlots(),
|
||||
title = stringResource(id = R.string.pref_remove_after_read),
|
||||
entries = mapOf(
|
||||
-1 to stringResource(id = R.string.disabled),
|
||||
0 to stringResource(id = R.string.last_read_chapter),
|
||||
1 to stringResource(id = R.string.second_to_last),
|
||||
2 to stringResource(id = R.string.third_to_last),
|
||||
3 to stringResource(id = R.string.fourth_to_last),
|
||||
4 to stringResource(id = R.string.fifth_to_last),
|
||||
),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = downloadPreferences.removeBookmarkedChapters(),
|
||||
title = stringResource(id = R.string.pref_remove_bookmarked_chapters),
|
||||
),
|
||||
getExcludedCategoriesPreference(
|
||||
downloadPreferences = downloadPreferences,
|
||||
categories = { categories },
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getExcludedCategoriesPreference(
|
||||
downloadPreferences: DownloadPreferences,
|
||||
categories: () -> List<Category>,
|
||||
): Preference.PreferenceItem.MultiSelectListPreference {
|
||||
val none = stringResource(id = R.string.none)
|
||||
val pref = downloadPreferences.removeExcludeCategories()
|
||||
val entries = categories().associate { it.id.toString() to it.visualName }
|
||||
val subtitle by produceState(initialValue = "") {
|
||||
pref.changes()
|
||||
.stateIn(this)
|
||||
.collect { mutable ->
|
||||
value = mutable
|
||||
.mapNotNull { id -> entries[id] }
|
||||
.sortedBy { entries.values.indexOf(it) }
|
||||
.joinToString()
|
||||
.ifEmpty { none }
|
||||
}
|
||||
}
|
||||
return Preference.PreferenceItem.MultiSelectListPreference(
|
||||
pref = pref,
|
||||
title = stringResource(id = R.string.pref_remove_exclude_categories),
|
||||
subtitle = subtitle,
|
||||
entries = entries,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getDownloadNewChaptersGroup(
|
||||
downloadPreferences: DownloadPreferences,
|
||||
allCategories: List<Category>,
|
||||
): Preference.PreferenceGroup {
|
||||
val downloadNewChaptersPref = downloadPreferences.downloadNewChapters()
|
||||
val downloadNewChapterCategoriesPref = downloadPreferences.downloadNewChapterCategories()
|
||||
val downloadNewChapterCategoriesExcludePref = downloadPreferences.downloadNewChapterCategoriesExclude()
|
||||
|
||||
val downloadNewChapters by downloadNewChaptersPref.collectAsState()
|
||||
|
||||
val included by downloadNewChapterCategoriesPref.collectAsState()
|
||||
val excluded by downloadNewChapterCategoriesExcludePref.collectAsState()
|
||||
var showDialog by rememberSaveable { mutableStateOf(false) }
|
||||
if (showDialog) {
|
||||
TriStateListDialog(
|
||||
title = stringResource(id = R.string.categories),
|
||||
message = stringResource(id = R.string.pref_download_new_categories_details),
|
||||
items = allCategories,
|
||||
initialChecked = included.mapNotNull { id -> allCategories.find { it.id.toString() == id } },
|
||||
initialInversed = excluded.mapNotNull { id -> allCategories.find { it.id.toString() == id } },
|
||||
itemLabel = { it.visualName },
|
||||
onDismissRequest = { showDialog = false },
|
||||
onValueChanged = { newIncluded, newExcluded ->
|
||||
downloadNewChapterCategoriesPref.set(newIncluded.map { it.id.toString() }.toSet())
|
||||
downloadNewChapterCategoriesExcludePref.set(newExcluded.map { it.id.toString() }.toSet())
|
||||
showDialog = false
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(id = R.string.pref_download_new),
|
||||
preferenceItems = listOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = downloadNewChaptersPref,
|
||||
title = stringResource(id = R.string.pref_download_new),
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(id = R.string.categories),
|
||||
subtitle = getCategoriesLabel(
|
||||
allCategories = allCategories,
|
||||
included = included,
|
||||
excluded = excluded,
|
||||
),
|
||||
onClick = { showDialog = true },
|
||||
enabled = downloadNewChapters,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getDownloadAheadGroup(
|
||||
downloadPreferences: DownloadPreferences,
|
||||
): Preference.PreferenceGroup {
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(id = R.string.download_ahead),
|
||||
preferenceItems = listOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = downloadPreferences.autoDownloadWhileReading(),
|
||||
title = stringResource(id = R.string.auto_download_while_reading),
|
||||
entries = listOf(0, 2, 3, 5, 10).associateWith {
|
||||
if (it == 0) {
|
||||
stringResource(id = R.string.disabled)
|
||||
} else {
|
||||
pluralStringResource(id = R.plurals.next_unread_chapters, count = it, it)
|
||||
}
|
||||
},
|
||||
),
|
||||
Preference.infoPreference(stringResource(id = R.string.download_ahead_info)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
package eu.kanade.presentation.more.settings.screen
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
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 org.xmlpull.v1.XmlPullParser
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class SettingsGeneralScreen : SearchableSettings {
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
override fun getTitle(): String = stringResource(id = R.string.pref_category_general)
|
||||
|
||||
@Composable
|
||||
override fun getPreferences(): List<Preference> {
|
||||
val prefs = remember { Injekt.get<BasePreferences>() }
|
||||
val libraryPrefs = remember { Injekt.get<LibraryPreferences>() }
|
||||
return mutableListOf<Preference>().apply {
|
||||
add(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = libraryPrefs.showUpdatesNavBadge(),
|
||||
title = stringResource(id = R.string.pref_library_update_show_tab_badge),
|
||||
),
|
||||
)
|
||||
|
||||
add(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = prefs.confirmExit(),
|
||||
title = stringResource(id = R.string.pref_confirm_exit),
|
||||
),
|
||||
)
|
||||
|
||||
val context = LocalContext.current
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
add(
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(id = R.string.pref_manage_notifications),
|
||||
onClick = {
|
||||
val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
|
||||
putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val langs = remember { getLangs(context) }
|
||||
val currentLanguage = remember { AppCompatDelegate.getApplicationLocales().get(0)?.toLanguageTag() ?: "" }
|
||||
add(
|
||||
Preference.PreferenceItem.BasicListPreference(
|
||||
value = currentLanguage,
|
||||
title = stringResource(id = R.string.pref_app_language),
|
||||
subtitle = "%s",
|
||||
entries = langs,
|
||||
onValueChanged = { newValue ->
|
||||
val locale = if (newValue.isEmpty()) {
|
||||
LocaleListCompat.getEmptyLocaleList()
|
||||
} else {
|
||||
LocaleListCompat.forLanguageTags(newValue)
|
||||
}
|
||||
AppCompatDelegate.setApplicationLocales(locale)
|
||||
true
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLangs(context: Context): Map<String, String> {
|
||||
val langs = mutableListOf<Pair<String, String>>()
|
||||
val parser = context.resources.getXml(R.xml.locales_config)
|
||||
var eventType = parser.eventType
|
||||
while (eventType != XmlPullParser.END_DOCUMENT) {
|
||||
if (eventType == XmlPullParser.START_TAG && parser.name == "locale") {
|
||||
for (i in 0 until parser.attributeCount) {
|
||||
if (parser.getAttributeName(i) == "name") {
|
||||
val langTag = parser.getAttributeValue(i)
|
||||
val displayName = LocaleHelper.getDisplayName(langTag)
|
||||
if (displayName.isNotEmpty()) {
|
||||
langs.add(Pair(langTag, displayName))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
eventType = parser.next()
|
||||
}
|
||||
|
||||
langs.sortBy { it.second }
|
||||
langs.add(0, Pair("", context.getString(R.string.label_default)))
|
||||
|
||||
return langs.toMap()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,360 @@
|
|||
package eu.kanade.presentation.more.settings.screen
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.core.content.ContextCompat
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import com.bluelinelabs.conductor.Router
|
||||
import com.chargemap.compose.numberpicker.NumberPicker
|
||||
import eu.kanade.domain.category.interactor.GetCategories
|
||||
import eu.kanade.domain.category.interactor.ResetCategoryFlags
|
||||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.domain.library.service.LibraryPreferences
|
||||
import eu.kanade.presentation.category.visualName
|
||||
import eu.kanade.presentation.more.settings.Preference
|
||||
import eu.kanade.presentation.more.settings.widget.TriStateListDialog
|
||||
import eu.kanade.presentation.util.LocalRouter
|
||||
import eu.kanade.presentation.util.collectAsState
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||
import eu.kanade.tachiyomi.data.preference.DEVICE_BATTERY_NOT_LOW
|
||||
import eu.kanade.tachiyomi.data.preference.DEVICE_CHARGING
|
||||
import eu.kanade.tachiyomi.data.preference.DEVICE_NETWORK_NOT_METERED
|
||||
import eu.kanade.tachiyomi.data.preference.DEVICE_ONLY_ON_WIFI
|
||||
import eu.kanade.tachiyomi.data.preference.MANGA_HAS_UNREAD
|
||||
import eu.kanade.tachiyomi.data.preference.MANGA_NON_COMPLETED
|
||||
import eu.kanade.tachiyomi.data.preference.MANGA_NON_READ
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
||||
import eu.kanade.tachiyomi.ui.category.CategoryController
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class SettingsLibraryScreen : SearchableSettings {
|
||||
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
override fun getTitle(): String = stringResource(id = R.string.pref_category_library)
|
||||
|
||||
@Composable
|
||||
override fun getPreferences(): List<Preference> {
|
||||
val getCategories = remember { Injekt.get<GetCategories>() }
|
||||
val libraryPreferences = remember { Injekt.get<LibraryPreferences>() }
|
||||
val allCategories by getCategories.subscribe().collectAsState(initial = runBlocking { getCategories.await() })
|
||||
|
||||
return mutableListOf(
|
||||
getDisplayGroup(libraryPreferences),
|
||||
getCategoriesGroup(LocalRouter.currentOrThrow, allCategories, libraryPreferences),
|
||||
getGlobalUpdateGroup(allCategories, libraryPreferences),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getDisplayGroup(libraryPreferences: LibraryPreferences): Preference.PreferenceGroup {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val portraitColumns by libraryPreferences.portraitColumns().stateIn(scope).collectAsState()
|
||||
val landscapeColumns by libraryPreferences.landscapeColumns().stateIn(scope).collectAsState()
|
||||
|
||||
var showDialog by rememberSaveable { mutableStateOf(false) }
|
||||
if (showDialog) {
|
||||
LibraryColumnsDialog(
|
||||
initialPortrait = portraitColumns,
|
||||
initialLandscape = landscapeColumns,
|
||||
onDismissRequest = { showDialog = false },
|
||||
onValueChanged = { portrait, landscape ->
|
||||
libraryPreferences.portraitColumns().set(portrait)
|
||||
libraryPreferences.landscapeColumns().set(landscape)
|
||||
showDialog = false
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(R.string.pref_category_display),
|
||||
preferenceItems = listOf(
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(R.string.pref_library_columns),
|
||||
subtitle = "${stringResource(R.string.portrait)}: ${getColumnValue(context, portraitColumns)}, " +
|
||||
"${stringResource(R.string.landscape)}: ${getColumnValue(context, landscapeColumns)}",
|
||||
onClick = { showDialog = true },
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getCategoriesGroup(
|
||||
router: Router?,
|
||||
allCategories: List<Category>,
|
||||
libraryPreferences: LibraryPreferences,
|
||||
): Preference.PreferenceGroup {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val userCategoriesCount = allCategories.filterNot(Category::isSystemCategory).size
|
||||
|
||||
val defaultCategory by libraryPreferences.defaultCategory().collectAsState()
|
||||
val selectedCategory = allCategories.find { it.id == defaultCategory.toLong() }
|
||||
|
||||
// For default category
|
||||
val ids = listOf(libraryPreferences.defaultCategory().defaultValue()) +
|
||||
allCategories.map { it.id.toInt() }
|
||||
val labels = listOf(stringResource(id = R.string.default_category_summary)) +
|
||||
allCategories.map { it.visualName(context) }
|
||||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(id = R.string.categories),
|
||||
preferenceItems = listOf(
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(id = R.string.action_edit_categories),
|
||||
subtitle = pluralStringResource(
|
||||
id = R.plurals.num_categories,
|
||||
count = userCategoriesCount,
|
||||
userCategoriesCount,
|
||||
),
|
||||
onClick = { router?.pushController(CategoryController()) },
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = libraryPreferences.defaultCategory(),
|
||||
title = stringResource(id = R.string.default_category),
|
||||
subtitle = selectedCategory?.visualName ?: stringResource(id = R.string.default_category_summary),
|
||||
entries = ids.zip(labels).toMap(),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = libraryPreferences.categorizedDisplaySettings(),
|
||||
title = stringResource(id = R.string.categorized_display_settings),
|
||||
onValueChanged = {
|
||||
if (!it) {
|
||||
scope.launch {
|
||||
Injekt.get<ResetCategoryFlags>().await()
|
||||
}
|
||||
}
|
||||
true
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getGlobalUpdateGroup(
|
||||
allCategories: List<Category>,
|
||||
libraryPreferences: LibraryPreferences,
|
||||
): Preference.PreferenceGroup {
|
||||
val context = LocalContext.current
|
||||
|
||||
val libraryUpdateIntervalPref = libraryPreferences.libraryUpdateInterval()
|
||||
val libraryUpdateDeviceRestrictionPref = libraryPreferences.libraryUpdateDeviceRestriction()
|
||||
val libraryUpdateMangaRestrictionPref = libraryPreferences.libraryUpdateMangaRestriction()
|
||||
val libraryUpdateCategoriesPref = libraryPreferences.libraryUpdateCategories()
|
||||
val libraryUpdateCategoriesExcludePref = libraryPreferences.libraryUpdateCategoriesExclude()
|
||||
|
||||
val libraryUpdateInterval by libraryUpdateIntervalPref.collectAsState()
|
||||
|
||||
val deviceRestrictionEntries = mapOf(
|
||||
DEVICE_ONLY_ON_WIFI to stringResource(id = R.string.connected_to_wifi),
|
||||
DEVICE_NETWORK_NOT_METERED to stringResource(id = R.string.network_not_metered),
|
||||
DEVICE_CHARGING to stringResource(id = R.string.charging),
|
||||
DEVICE_BATTERY_NOT_LOW to stringResource(id = R.string.battery_not_low),
|
||||
)
|
||||
val deviceRestrictions = libraryUpdateDeviceRestrictionPref.collectAsState()
|
||||
.value
|
||||
.sorted()
|
||||
.map { deviceRestrictionEntries.getOrElse(it) { it } }
|
||||
.let { if (it.isEmpty()) stringResource(id = R.string.none) else it.joinToString() }
|
||||
|
||||
val mangaRestrictionEntries = mapOf(
|
||||
MANGA_HAS_UNREAD to stringResource(id = R.string.pref_update_only_completely_read),
|
||||
MANGA_NON_READ to stringResource(id = R.string.pref_update_only_started),
|
||||
MANGA_NON_COMPLETED to stringResource(id = R.string.pref_update_only_non_completed),
|
||||
)
|
||||
val mangaRestrictions = libraryUpdateMangaRestrictionPref.collectAsState()
|
||||
.value
|
||||
.map { mangaRestrictionEntries.getOrElse(it) { it } }
|
||||
.let { if (it.isEmpty()) stringResource(id = R.string.none) else it.joinToString() }
|
||||
|
||||
val included by libraryUpdateCategoriesPref.collectAsState()
|
||||
val excluded by libraryUpdateCategoriesExcludePref.collectAsState()
|
||||
var showDialog by rememberSaveable { mutableStateOf(false) }
|
||||
if (showDialog) {
|
||||
TriStateListDialog(
|
||||
title = stringResource(id = R.string.categories),
|
||||
message = stringResource(id = R.string.pref_library_update_categories_details),
|
||||
items = allCategories,
|
||||
initialChecked = included.mapNotNull { id -> allCategories.find { it.id.toString() == id } },
|
||||
initialInversed = excluded.mapNotNull { id -> allCategories.find { it.id.toString() == id } },
|
||||
itemLabel = { it.visualName },
|
||||
onDismissRequest = { showDialog = false },
|
||||
onValueChanged = { newIncluded, newExcluded ->
|
||||
libraryUpdateCategoriesPref.set(newIncluded.map { it.id.toString() }.toSet())
|
||||
libraryUpdateCategoriesExcludePref.set(newExcluded.map { it.id.toString() }.toSet())
|
||||
showDialog = false
|
||||
},
|
||||
)
|
||||
}
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(id = R.string.pref_category_library_update),
|
||||
preferenceItems = listOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = libraryUpdateIntervalPref,
|
||||
title = stringResource(id = R.string.pref_library_update_interval),
|
||||
subtitle = "%s",
|
||||
entries = mapOf(
|
||||
0 to stringResource(id = R.string.update_never),
|
||||
12 to stringResource(id = R.string.update_12hour),
|
||||
24 to stringResource(id = R.string.update_24hour),
|
||||
48 to stringResource(id = R.string.update_48hour),
|
||||
72 to stringResource(id = R.string.update_72hour),
|
||||
168 to stringResource(id = R.string.update_weekly),
|
||||
),
|
||||
onValueChanged = {
|
||||
LibraryUpdateJob.setupTask(context, it)
|
||||
true
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.MultiSelectListPreference(
|
||||
pref = libraryUpdateDeviceRestrictionPref,
|
||||
enabled = libraryUpdateInterval > 0,
|
||||
title = stringResource(id = R.string.pref_library_update_restriction),
|
||||
subtitle = stringResource(id = R.string.restrictions, deviceRestrictions),
|
||||
entries = deviceRestrictionEntries,
|
||||
onValueChanged = {
|
||||
// Post to event looper to allow the preference to be updated.
|
||||
ContextCompat.getMainExecutor(context).execute { LibraryUpdateJob.setupTask(context) }
|
||||
true
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.MultiSelectListPreference(
|
||||
pref = libraryUpdateMangaRestrictionPref,
|
||||
title = stringResource(id = R.string.pref_library_update_manga_restriction),
|
||||
subtitle = mangaRestrictions,
|
||||
entries = mangaRestrictionEntries,
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(id = R.string.categories),
|
||||
subtitle = getCategoriesLabel(
|
||||
allCategories = allCategories,
|
||||
included = included,
|
||||
excluded = excluded,
|
||||
),
|
||||
onClick = { showDialog = true },
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = libraryPreferences.autoUpdateMetadata(),
|
||||
title = stringResource(id = R.string.pref_library_update_refresh_metadata),
|
||||
subtitle = stringResource(id = R.string.pref_library_update_refresh_metadata_summary),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = libraryPreferences.autoUpdateTrackers(),
|
||||
enabled = Injekt.get<TrackManager>().hasLoggedServices(),
|
||||
title = stringResource(id = R.string.pref_library_update_refresh_trackers),
|
||||
subtitle = stringResource(id = R.string.pref_library_update_refresh_trackers_summary),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LibraryColumnsDialog(
|
||||
initialPortrait: Int,
|
||||
initialLandscape: Int,
|
||||
onDismissRequest: () -> Unit,
|
||||
onValueChanged: (portrait: Int, landscape: Int) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var portraitValue by rememberSaveable { mutableStateOf(initialPortrait) }
|
||||
var landscapeValue by rememberSaveable { mutableStateOf(initialLandscape) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = { Text(text = stringResource(id = R.string.pref_library_columns)) },
|
||||
text = {
|
||||
Row {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.portrait),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
NumberPicker(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clipToBounds(),
|
||||
value = portraitValue,
|
||||
onValueChange = { portraitValue = it },
|
||||
range = 0..10,
|
||||
label = { getColumnValue(context, it) },
|
||||
dividersColor = MaterialTheme.colorScheme.primary,
|
||||
textStyle = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.landscape),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
NumberPicker(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clipToBounds(),
|
||||
value = landscapeValue,
|
||||
onValueChange = { landscapeValue = it },
|
||||
range = 0..10,
|
||||
label = { getColumnValue(context, it) },
|
||||
dividersColor = MaterialTheme.colorScheme.primary,
|
||||
textStyle = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(id = android.R.string.cancel))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { onValueChanged(portraitValue, landscapeValue) }) {
|
||||
Text(text = stringResource(id = android.R.string.ok))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun getColumnValue(context: Context, value: Int): String {
|
||||
return if (value == 0) {
|
||||
context.getString(R.string.label_default)
|
||||
} else {
|
||||
value.toString()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
package eu.kanade.presentation.more.settings.screen
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.ChromeReaderMode
|
||||
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.Palette
|
||||
import androidx.compose.material.icons.outlined.Search
|
||||
import androidx.compose.material.icons.outlined.Security
|
||||
import androidx.compose.material.icons.outlined.SettingsBackupRestore
|
||||
import androidx.compose.material.icons.outlined.Sync
|
||||
import androidx.compose.material.icons.outlined.Tune
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.NonRestartableComposable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.AppBarActions
|
||||
import eu.kanade.presentation.more.settings.Preference
|
||||
import eu.kanade.presentation.more.settings.PreferenceScaffold
|
||||
import eu.kanade.presentation.util.LocalBackPress
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
object SettingsMainScreen : SearchableSettings {
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
override fun getTitle(): String = stringResource(id = R.string.label_settings)
|
||||
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
override fun getPreferences(): List<Preference> {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
return listOf(
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(R.string.pref_category_general),
|
||||
icon = Icons.Outlined.Tune,
|
||||
onClick = { navigator.push(SettingsGeneralScreen()) },
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(R.string.pref_category_appearance),
|
||||
icon = Icons.Outlined.Palette,
|
||||
onClick = { navigator.push(SettingsAppearanceScreen()) },
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(R.string.pref_category_library),
|
||||
icon = Icons.Outlined.CollectionsBookmark,
|
||||
onClick = { navigator.push(SettingsLibraryScreen()) },
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(R.string.pref_category_reader),
|
||||
icon = Icons.Outlined.ChromeReaderMode,
|
||||
onClick = { navigator.push(SettingsReaderScreen()) },
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(R.string.pref_category_downloads),
|
||||
icon = Icons.Outlined.GetApp,
|
||||
onClick = { navigator.push(SettingsDownloadScreen()) },
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(R.string.pref_category_tracking),
|
||||
icon = Icons.Outlined.Sync,
|
||||
onClick = { navigator.push(SettingsTrackingScreen()) },
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(R.string.browse),
|
||||
icon = Icons.Outlined.Explore,
|
||||
onClick = { navigator.push(SettingsBrowseScreen()) },
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(R.string.label_backup),
|
||||
icon = Icons.Outlined.SettingsBackupRestore,
|
||||
onClick = { navigator.push(SettingsBackupScreen()) },
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(R.string.pref_category_security),
|
||||
icon = Icons.Outlined.Security,
|
||||
onClick = { navigator.push(SettingsSecurityScreen()) },
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(R.string.pref_category_advanced),
|
||||
icon = Icons.Outlined.Code,
|
||||
onClick = { navigator.push(SettingsAdvancedScreen()) },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val backPress = LocalBackPress.currentOrThrow
|
||||
PreferenceScaffold(
|
||||
title = getTitle(),
|
||||
actions = {
|
||||
AppBarActions(
|
||||
listOf(
|
||||
AppBar.Action(
|
||||
title = stringResource(R.string.action_search),
|
||||
icon = Icons.Outlined.Search,
|
||||
onClick = { navigator.push(SettingsSearchScreen()) },
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
onBackPressed = backPress::invoke,
|
||||
itemsProvider = { getPreferences() },
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,312 @@
|
|||
package eu.kanade.presentation.more.settings.screen
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.stringArrayResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import eu.kanade.presentation.more.settings.Preference
|
||||
import eu.kanade.presentation.util.collectAsState
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.ReaderHideThreshold
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.TappingInvertMode
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class SettingsReaderScreen : SearchableSettings {
|
||||
@ReadOnlyComposable
|
||||
@Composable
|
||||
override fun getTitle(): String = stringResource(id = R.string.pref_category_reader)
|
||||
|
||||
@Composable
|
||||
override fun getPreferences(): List<Preference> {
|
||||
val readerPref = remember { Injekt.get<ReaderPreferences>() }
|
||||
return listOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPref.defaultReadingMode(),
|
||||
title = stringResource(id = R.string.pref_viewer_type),
|
||||
entries = ReadingModeType.values().drop(1)
|
||||
.associate { it.flagValue to stringResource(id = it.stringRes) },
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPref.doubleTapAnimSpeed(),
|
||||
title = stringResource(id = R.string.pref_double_tap_anim_speed),
|
||||
entries = mapOf(
|
||||
1 to stringResource(id = R.string.double_tap_anim_speed_0),
|
||||
500 to stringResource(id = R.string.double_tap_anim_speed_normal),
|
||||
250 to stringResource(id = R.string.double_tap_anim_speed_fast),
|
||||
),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPref.showReadingMode(),
|
||||
title = stringResource(id = R.string.pref_show_reading_mode),
|
||||
subtitle = stringResource(id = R.string.pref_show_reading_mode_summary),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPref.showNavigationOverlayOnStart(),
|
||||
title = stringResource(id = R.string.pref_show_navigation_mode),
|
||||
subtitle = stringResource(id = R.string.pref_show_navigation_mode_summary),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPref.trueColor(),
|
||||
title = stringResource(id = R.string.pref_true_color),
|
||||
subtitle = stringResource(id = R.string.pref_true_color_summary),
|
||||
enabled = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O,
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPref.pageTransitions(),
|
||||
title = stringResource(id = R.string.pref_page_transitions),
|
||||
),
|
||||
getDisplayGroup(readerPreferences = readerPref),
|
||||
getPagedGroup(readerPreferences = readerPref),
|
||||
getWebtoonGroup(readerPreferences = readerPref),
|
||||
getNavigationGroup(readerPreferences = readerPref),
|
||||
getActionsGroup(readerPreferences = readerPref),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getDisplayGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
|
||||
val fullscreenPref = readerPreferences.fullscreen()
|
||||
val fullscreen by fullscreenPref.collectAsState()
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(id = R.string.pref_category_display),
|
||||
preferenceItems = listOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPreferences.defaultOrientationType(),
|
||||
title = stringResource(id = R.string.pref_rotation_type),
|
||||
entries = OrientationType.values().drop(1)
|
||||
.associate { it.flagValue to stringResource(id = it.stringRes) },
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPreferences.readerTheme(),
|
||||
title = stringResource(id = R.string.pref_reader_theme),
|
||||
entries = mapOf(
|
||||
1 to stringResource(id = R.string.black_background),
|
||||
2 to stringResource(id = R.string.gray_background),
|
||||
0 to stringResource(id = R.string.white_background),
|
||||
3 to stringResource(id = R.string.automatic_background),
|
||||
),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = fullscreenPref,
|
||||
title = stringResource(id = R.string.pref_fullscreen),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.cutoutShort(),
|
||||
title = stringResource(id = R.string.pref_cutout_short),
|
||||
enabled = fullscreen &&
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P &&
|
||||
LocalView.current.rootWindowInsets?.displayCutout != null, // has cutout
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.keepScreenOn(),
|
||||
title = stringResource(id = R.string.pref_keep_screen_on),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.showPageNumber(),
|
||||
title = stringResource(id = R.string.pref_show_page_number),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getPagedGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
|
||||
val navModePref = readerPreferences.navigationModePager()
|
||||
val imageScaleTypePref = readerPreferences.imageScaleType()
|
||||
val dualPageSplitPref = readerPreferences.dualPageSplitPaged()
|
||||
|
||||
val navMode by navModePref.collectAsState()
|
||||
val imageScaleType by imageScaleTypePref.collectAsState()
|
||||
val dualPageSplit by dualPageSplitPref.collectAsState()
|
||||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(id = R.string.pager_viewer),
|
||||
preferenceItems = listOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = navModePref,
|
||||
title = stringResource(id = R.string.pref_viewer_nav),
|
||||
entries = stringArrayResource(id = R.array.pager_nav).let {
|
||||
it.indices.zip(it).toMap()
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPreferences.pagerNavInverted(),
|
||||
title = stringResource(id = R.string.pref_read_with_tapping_inverted),
|
||||
entries = mapOf(
|
||||
TappingInvertMode.NONE to stringResource(id = R.string.none),
|
||||
TappingInvertMode.HORIZONTAL to stringResource(id = R.string.tapping_inverted_horizontal),
|
||||
TappingInvertMode.VERTICAL to stringResource(id = R.string.tapping_inverted_vertical),
|
||||
TappingInvertMode.BOTH to stringResource(id = R.string.tapping_inverted_both),
|
||||
),
|
||||
enabled = navMode != 5,
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.navigateToPan(),
|
||||
title = stringResource(id = R.string.pref_navigate_pan),
|
||||
enabled = navMode != 5,
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = imageScaleTypePref,
|
||||
title = stringResource(id = R.string.pref_image_scale_type),
|
||||
entries = mapOf(
|
||||
1 to stringResource(id = R.string.scale_type_fit_screen),
|
||||
2 to stringResource(id = R.string.scale_type_stretch),
|
||||
3 to stringResource(id = R.string.scale_type_fit_width),
|
||||
4 to stringResource(id = R.string.scale_type_fit_height),
|
||||
5 to stringResource(id = R.string.scale_type_original_size),
|
||||
6 to stringResource(id = R.string.scale_type_smart_fit),
|
||||
),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.landscapeZoom(),
|
||||
title = stringResource(id = R.string.pref_landscape_zoom),
|
||||
enabled = imageScaleType == 1,
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPreferences.zoomStart(),
|
||||
title = stringResource(id = R.string.pref_zoom_start),
|
||||
entries = mapOf(
|
||||
1 to stringResource(id = R.string.zoom_start_automatic),
|
||||
2 to stringResource(id = R.string.zoom_start_left),
|
||||
3 to stringResource(id = R.string.zoom_start_right),
|
||||
4 to stringResource(id = R.string.zoom_start_center),
|
||||
),
|
||||
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.cropBorders(),
|
||||
title = stringResource(id = R.string.pref_crop_borders),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = dualPageSplitPref,
|
||||
title = stringResource(id = R.string.pref_dual_page_split),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.dualPageInvertPaged(),
|
||||
title = stringResource(id = R.string.pref_dual_page_invert),
|
||||
subtitle = stringResource(id = R.string.pref_dual_page_invert_summary),
|
||||
enabled = dualPageSplit,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getWebtoonGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
|
||||
val navModePref = readerPreferences.navigationModeWebtoon()
|
||||
val dualPageSplitPref = readerPreferences.dualPageSplitWebtoon()
|
||||
|
||||
val navMode by navModePref.collectAsState()
|
||||
val dualPageSplit by dualPageSplitPref.collectAsState()
|
||||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(id = R.string.webtoon_viewer),
|
||||
preferenceItems = listOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = navModePref,
|
||||
title = stringResource(id = R.string.pref_viewer_nav),
|
||||
entries = stringArrayResource(id = R.array.webtoon_nav).let {
|
||||
it.indices.zip(it).toMap()
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPreferences.webtoonNavInverted(),
|
||||
title = stringResource(id = R.string.pref_read_with_tapping_inverted),
|
||||
entries = mapOf(
|
||||
TappingInvertMode.NONE to stringResource(id = R.string.none),
|
||||
TappingInvertMode.HORIZONTAL to stringResource(id = R.string.tapping_inverted_horizontal),
|
||||
TappingInvertMode.VERTICAL to stringResource(id = R.string.tapping_inverted_vertical),
|
||||
TappingInvertMode.BOTH to stringResource(id = R.string.tapping_inverted_both),
|
||||
),
|
||||
enabled = navMode != 5,
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPreferences.webtoonSidePadding(),
|
||||
title = stringResource(id = R.string.pref_webtoon_side_padding),
|
||||
entries = mapOf(
|
||||
0 to stringResource(id = R.string.webtoon_side_padding_0),
|
||||
5 to stringResource(id = R.string.webtoon_side_padding_5),
|
||||
10 to stringResource(id = R.string.webtoon_side_padding_10),
|
||||
15 to stringResource(id = R.string.webtoon_side_padding_15),
|
||||
20 to stringResource(id = R.string.webtoon_side_padding_20),
|
||||
25 to stringResource(id = R.string.webtoon_side_padding_25),
|
||||
),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPreferences.readerHideThreshold(),
|
||||
title = stringResource(id = R.string.pref_hide_threshold),
|
||||
entries = mapOf(
|
||||
ReaderHideThreshold.HIGHEST to stringResource(id = R.string.pref_highest),
|
||||
ReaderHideThreshold.HIGH to stringResource(id = R.string.pref_high),
|
||||
ReaderHideThreshold.LOW to stringResource(id = R.string.pref_low),
|
||||
ReaderHideThreshold.LOWEST to stringResource(id = R.string.pref_lowest),
|
||||
),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.cropBordersWebtoon(),
|
||||
title = stringResource(id = R.string.pref_crop_borders),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = dualPageSplitPref,
|
||||
title = stringResource(id = R.string.pref_dual_page_split),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.dualPageInvertWebtoon(),
|
||||
title = stringResource(id = R.string.pref_dual_page_invert),
|
||||
subtitle = stringResource(id = R.string.pref_dual_page_invert_summary),
|
||||
enabled = dualPageSplit,
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.longStripSplitWebtoon(),
|
||||
title = stringResource(id = R.string.pref_long_strip_split),
|
||||
subtitle = stringResource(id = R.string.split_tall_images_summary),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getNavigationGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
|
||||
val readWithVolumeKeysPref = readerPreferences.readWithVolumeKeys()
|
||||
val readWithVolumeKeys by readWithVolumeKeysPref.collectAsState()
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(id = R.string.pref_reader_navigation),
|
||||
preferenceItems = listOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readWithVolumeKeysPref,
|
||||
title = stringResource(id = R.string.pref_read_with_volume_keys),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.readWithVolumeKeysInverted(),
|
||||
title = stringResource(id = R.string.pref_read_with_volume_keys_inverted),
|
||||
enabled = readWithVolumeKeys,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getActionsGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(id = R.string.pref_reader_actions),
|
||||
preferenceItems = listOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.readWithLongTap(),
|
||||
title = stringResource(id = R.string.pref_read_with_long_tap),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.folderPerManga(),
|
||||
title = stringResource(id = R.string.pref_create_folder_per_manga),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,303 @@
|
|||
package eu.kanade.presentation.more.settings.screen
|
||||
|
||||
import android.content.res.Resources
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.clickable
|
||||
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.layout.paddingFromBaseline
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.NonRestartableComposable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
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 eu.kanade.presentation.components.Divider
|
||||
import eu.kanade.presentation.components.EmptyScreen
|
||||
import eu.kanade.presentation.components.Scaffold
|
||||
import eu.kanade.presentation.more.settings.Preference
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.system.isLTR
|
||||
|
||||
class SettingsSearchScreen : Screen {
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val softKeyboardController = LocalSoftwareKeyboardController.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
// Hide keyboard on change screen
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
softKeyboardController?.hide()
|
||||
}
|
||||
}
|
||||
|
||||
// Hide keyboard on outside text field is touched
|
||||
LaunchedEffect(listState.isScrollInProgress) {
|
||||
if (listState.isScrollInProgress) {
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
}
|
||||
|
||||
// Request text field focus on launch
|
||||
LaunchedEffect(focusRequester) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
|
||||
var textFieldValue by rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) }
|
||||
Scaffold(
|
||||
topBar = {
|
||||
Column {
|
||||
TopAppBar(
|
||||
navigationIcon = {
|
||||
IconButton(onClick = navigator::pop) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowBack,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
},
|
||||
title = {
|
||||
BasicTextField(
|
||||
value = textFieldValue,
|
||||
onValueChange = { textFieldValue = it },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
textStyle = MaterialTheme.typography.bodyLarge
|
||||
.copy(color = MaterialTheme.colorScheme.onSurface),
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
||||
keyboardActions = KeyboardActions(onSearch = { focusManager.clearFocus() }),
|
||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
||||
decorationBox = {
|
||||
if (textFieldValue.text.isEmpty()) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.action_search_settings),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
}
|
||||
it()
|
||||
},
|
||||
)
|
||||
},
|
||||
actions = {
|
||||
if (textFieldValue.text.isNotEmpty()) {
|
||||
IconButton(onClick = { textFieldValue = TextFieldValue() }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
Divider()
|
||||
}
|
||||
},
|
||||
) { contentPadding ->
|
||||
SearchResult(
|
||||
searchKey = textFieldValue.text,
|
||||
listState = listState,
|
||||
contentPadding = contentPadding,
|
||||
) { result ->
|
||||
SearchableSettings.highlightKey = result.highlightKey
|
||||
navigator.popUntil { it is SettingsMainScreen }
|
||||
navigator.push(result.route)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SearchResult(
|
||||
searchKey: String,
|
||||
modifier: Modifier = Modifier,
|
||||
listState: LazyListState = rememberLazyListState(),
|
||||
contentPadding: PaddingValues = PaddingValues(),
|
||||
onItemClick: (SearchResultItem) -> Unit,
|
||||
) {
|
||||
if (searchKey.isEmpty()) return
|
||||
|
||||
val index = getIndex()
|
||||
val result by produceState<List<SearchResultItem>?>(initialValue = null, searchKey) {
|
||||
value = index.asSequence()
|
||||
.flatMap { settingsData ->
|
||||
settingsData.contents.asSequence()
|
||||
// Only search from enabled prefs and one with valid title
|
||||
.filter { it.enabled && it.title.isNotBlank() }
|
||||
// Flatten items contained inside *enabled* PreferenceGroup
|
||||
.flatMap { p ->
|
||||
when (p) {
|
||||
is Preference.PreferenceGroup -> {
|
||||
if (p.enabled) {
|
||||
p.preferenceItems.asSequence()
|
||||
.filter { it.enabled && it.title.isNotBlank() }
|
||||
.map { p.title to it }
|
||||
} else {
|
||||
emptySequence()
|
||||
}
|
||||
}
|
||||
is Preference.PreferenceItem<*> -> sequenceOf(null to p)
|
||||
else -> emptySequence() // Ignore other prefs
|
||||
}
|
||||
}
|
||||
// Filter by search query
|
||||
.filter { (_, p) ->
|
||||
val inTitle = p.title.contains(searchKey, true)
|
||||
val inSummary = p.subtitle?.contains(searchKey, true) ?: false
|
||||
inTitle || inSummary
|
||||
}
|
||||
// Map result data
|
||||
.map { (categoryTitle, p) ->
|
||||
SearchResultItem(
|
||||
route = settingsData.route,
|
||||
title = p.title,
|
||||
breadcrumbs = getLocalizedBreadcrumb(path = settingsData.title, node = categoryTitle),
|
||||
highlightKey = p.title,
|
||||
)
|
||||
}
|
||||
}
|
||||
.take(10) // Just take top 10 result for quicker result
|
||||
.toList()
|
||||
}
|
||||
|
||||
Crossfade(targetState = result) {
|
||||
LazyColumn(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
state = listState,
|
||||
contentPadding = contentPadding,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
when {
|
||||
it == null -> {
|
||||
/* Don't show anything just yet */
|
||||
}
|
||||
// No result
|
||||
it.isEmpty() -> item { EmptyScreen(stringResource(id = R.string.no_results_found)) }
|
||||
// Show result list
|
||||
else -> items(
|
||||
items = it,
|
||||
key = { i -> i.hashCode() },
|
||||
) { item ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onItemClick(item) }
|
||||
.padding(horizontal = 24.dp, vertical = 14.dp),
|
||||
) {
|
||||
Text(
|
||||
text = item.title,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
fontWeight = FontWeight.Normal,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
Text(
|
||||
text = item.breadcrumbs,
|
||||
modifier = Modifier.paddingFromBaseline(top = 16.dp),
|
||||
maxLines = 1,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
private fun getIndex() = settingScreens
|
||||
.map { screen ->
|
||||
SettingsData(
|
||||
title = screen.getTitle(),
|
||||
route = screen,
|
||||
contents = screen.getPreferences(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getLocalizedBreadcrumb(path: String, node: String?): String {
|
||||
return if (node == null) {
|
||||
path
|
||||
} else {
|
||||
if (Resources.getSystem().isLTR) {
|
||||
// This locale reads left to right.
|
||||
"$path > $node"
|
||||
} else {
|
||||
// This locale reads right to left.
|
||||
"$node < $path"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val settingScreens = listOf(
|
||||
SettingsGeneralScreen(),
|
||||
SettingsAppearanceScreen(),
|
||||
SettingsLibraryScreen(),
|
||||
SettingsReaderScreen(),
|
||||
SettingsDownloadScreen(),
|
||||
SettingsTrackingScreen(),
|
||||
SettingsBrowseScreen(),
|
||||
SettingsBackupScreen(),
|
||||
SettingsSecurityScreen(),
|
||||
SettingsAdvancedScreen(),
|
||||
)
|
||||
|
||||
private data class SettingsData(
|
||||
val title: String,
|
||||
val route: Screen,
|
||||
val contents: List<Preference>,
|
||||
)
|
||||
|
||||
private data class SearchResultItem(
|
||||
val route: Screen,
|
||||
val title: String,
|
||||
val breadcrumbs: String,
|
||||
val highlightKey: String,
|
||||
)
|
|
@ -0,0 +1,89 @@
|
|||
package eu.kanade.presentation.more.settings.screen
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import eu.kanade.presentation.more.settings.Preference
|
||||
import eu.kanade.presentation.util.collectAsState
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.core.security.SecurityPreferences
|
||||
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
|
||||
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.isAuthenticationSupported
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class SettingsSecurityScreen : SearchableSettings {
|
||||
|
||||
@ReadOnlyComposable
|
||||
@Composable
|
||||
override fun getTitle(): String = stringResource(id = R.string.pref_category_security)
|
||||
|
||||
@Composable
|
||||
override fun getPreferences(): List<Preference> {
|
||||
val context = LocalContext.current
|
||||
val securityPreferences = remember { Injekt.get<SecurityPreferences>() }
|
||||
val authSupported = remember { context.isAuthenticationSupported() }
|
||||
|
||||
val useAuthPref = securityPreferences.useAuthenticator()
|
||||
|
||||
val useAuth by useAuthPref.collectAsState()
|
||||
|
||||
return listOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = useAuthPref,
|
||||
title = stringResource(id = R.string.lock_with_biometrics),
|
||||
enabled = authSupported,
|
||||
onValueChanged = {
|
||||
(context as FragmentActivity).authenticate(
|
||||
title = context.getString(R.string.lock_with_biometrics),
|
||||
)
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = securityPreferences.lockAppAfter(),
|
||||
title = stringResource(id = R.string.lock_when_idle),
|
||||
subtitle = "%s",
|
||||
enabled = authSupported && useAuth,
|
||||
entries = LockAfterValues
|
||||
.associateWith {
|
||||
when (it) {
|
||||
-1 -> stringResource(id = R.string.lock_never)
|
||||
0 -> stringResource(id = R.string.lock_always)
|
||||
else -> pluralStringResource(id = R.plurals.lock_after_mins, count = it, it)
|
||||
}
|
||||
},
|
||||
onValueChanged = {
|
||||
(context as FragmentActivity).authenticate(
|
||||
title = context.getString(R.string.lock_when_idle),
|
||||
)
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = securityPreferences.hideNotificationContent(),
|
||||
title = stringResource(id = R.string.hide_notification_content),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = securityPreferences.secureScreen(),
|
||||
title = stringResource(id = R.string.secure_screen),
|
||||
subtitle = "%s",
|
||||
entries = SecurityPreferences.SecureScreenMode.values()
|
||||
.associateWith { stringResource(id = it.titleResId) },
|
||||
),
|
||||
Preference.infoPreference(stringResource(id = R.string.secure_screen_summary)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val LockAfterValues = listOf(
|
||||
0, // Always
|
||||
1,
|
||||
2,
|
||||
5,
|
||||
10,
|
||||
-1, // Never
|
||||
)
|
|
@ -0,0 +1,336 @@
|
|||
package eu.kanade.presentation.more.settings.screen
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.HelpOutline
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.domain.track.service.TrackPreferences
|
||||
import eu.kanade.presentation.more.settings.Preference
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.anilist.AnilistApi
|
||||
import eu.kanade.tachiyomi.data.track.bangumi.BangumiApi
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeListApi
|
||||
import eu.kanade.tachiyomi.data.track.shikimori.ShikimoriApi
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class SettingsTrackingScreen : SearchableSettings {
|
||||
@ReadOnlyComposable
|
||||
@Composable
|
||||
override fun getTitle(): String = stringResource(id = R.string.pref_category_tracking)
|
||||
|
||||
@Composable
|
||||
override fun RowScope.AppBarAction() {
|
||||
val context = LocalContext.current
|
||||
IconButton(onClick = { context.openInBrowser("https://tachiyomi.org/help/guides/tracking/") }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.HelpOutline,
|
||||
contentDescription = stringResource(id = R.string.tracking_guide),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun getPreferences(): List<Preference> {
|
||||
val context = LocalContext.current
|
||||
val trackPreferences = remember { Injekt.get<TrackPreferences>() }
|
||||
val trackManager = remember { Injekt.get<TrackManager>() }
|
||||
|
||||
var dialog by remember { mutableStateOf<Any?>(null) }
|
||||
dialog?.run {
|
||||
when (this) {
|
||||
is LoginDialog -> {
|
||||
TrackingLoginDialog(
|
||||
service = service,
|
||||
uNameStringRes = uNameStringRes,
|
||||
onDismissRequest = { dialog = null },
|
||||
)
|
||||
}
|
||||
is LogoutDialog -> {
|
||||
TrackingLogoutDialog(
|
||||
service = service,
|
||||
onDismissRequest = { dialog = null },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return listOf(
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = trackPreferences.autoUpdateTrack(),
|
||||
title = stringResource(id = R.string.pref_auto_update_manga_sync),
|
||||
),
|
||||
Preference.PreferenceGroup(
|
||||
title = stringResource(id = R.string.services),
|
||||
preferenceItems = listOf(
|
||||
Preference.PreferenceItem.TrackingPreference(
|
||||
title = stringResource(id = trackManager.myAnimeList.nameRes()),
|
||||
service = trackManager.myAnimeList,
|
||||
login = { context.openInBrowser(MyAnimeListApi.authUrl(), forceDefaultBrowser = true) },
|
||||
logout = { dialog = LogoutDialog(trackManager.myAnimeList) },
|
||||
),
|
||||
Preference.PreferenceItem.TrackingPreference(
|
||||
title = stringResource(id = trackManager.aniList.nameRes()),
|
||||
service = trackManager.aniList,
|
||||
login = { context.openInBrowser(AnilistApi.authUrl(), forceDefaultBrowser = true) },
|
||||
logout = { dialog = LogoutDialog(trackManager.aniList) },
|
||||
),
|
||||
Preference.PreferenceItem.TrackingPreference(
|
||||
title = stringResource(id = trackManager.kitsu.nameRes()),
|
||||
service = trackManager.kitsu,
|
||||
login = { dialog = LoginDialog(trackManager.kitsu, R.string.email) },
|
||||
logout = { dialog = LogoutDialog(trackManager.kitsu) },
|
||||
),
|
||||
Preference.PreferenceItem.TrackingPreference(
|
||||
title = stringResource(id = trackManager.mangaUpdates.nameRes()),
|
||||
service = trackManager.mangaUpdates,
|
||||
login = { dialog = LoginDialog(trackManager.mangaUpdates, R.string.username) },
|
||||
logout = { dialog = LogoutDialog(trackManager.mangaUpdates) },
|
||||
),
|
||||
Preference.PreferenceItem.TrackingPreference(
|
||||
title = stringResource(id = trackManager.shikimori.nameRes()),
|
||||
service = trackManager.shikimori,
|
||||
login = { context.openInBrowser(ShikimoriApi.authUrl(), forceDefaultBrowser = true) },
|
||||
logout = { dialog = LogoutDialog(trackManager.shikimori) },
|
||||
),
|
||||
Preference.PreferenceItem.TrackingPreference(
|
||||
title = stringResource(id = trackManager.bangumi.nameRes()),
|
||||
service = trackManager.bangumi,
|
||||
login = { context.openInBrowser(BangumiApi.authUrl(), forceDefaultBrowser = true) },
|
||||
logout = { dialog = LogoutDialog(trackManager.bangumi) },
|
||||
),
|
||||
Preference.infoPreference(stringResource(id = R.string.tracking_info)),
|
||||
),
|
||||
),
|
||||
Preference.PreferenceGroup(
|
||||
title = stringResource(id = R.string.enhanced_services),
|
||||
preferenceItems = listOf(
|
||||
Preference.PreferenceItem.TrackingPreference(
|
||||
title = stringResource(id = trackManager.komga.nameRes()),
|
||||
service = trackManager.komga,
|
||||
login = {
|
||||
val sourceManager = Injekt.get<SourceManager>()
|
||||
val acceptedSources = trackManager.komga.getAcceptedSources()
|
||||
val hasValidSourceInstalled = sourceManager.getCatalogueSources()
|
||||
.any { it::class.qualifiedName in acceptedSources }
|
||||
|
||||
if (hasValidSourceInstalled) {
|
||||
trackManager.komga.loginNoop()
|
||||
} else {
|
||||
context.toast(R.string.tracker_komga_warning, Toast.LENGTH_LONG)
|
||||
}
|
||||
},
|
||||
logout = trackManager.komga::logout,
|
||||
),
|
||||
Preference.infoPreference(stringResource(id = R.string.enhanced_tracking_info)),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TrackingLoginDialog(
|
||||
service: TrackService,
|
||||
@StringRes uNameStringRes: Int,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
var username by remember { mutableStateOf(TextFieldValue(service.getUsername())) }
|
||||
var password by remember { mutableStateOf(TextFieldValue(service.getPassword())) }
|
||||
var processing by remember { mutableStateOf(false) }
|
||||
var inputError by remember { mutableStateOf(false) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = { Text(text = stringResource(id = R.string.login_title, stringResource(id = service.nameRes()))) },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = username,
|
||||
onValueChange = { username = it },
|
||||
label = { Text(text = stringResource(id = uNameStringRes)) },
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
||||
singleLine = true,
|
||||
isError = inputError && username.text.isEmpty(),
|
||||
)
|
||||
|
||||
var hidePassword by remember { mutableStateOf(true) }
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text(text = stringResource(id = R.string.password)) },
|
||||
trailingIcon = {
|
||||
IconButton(onClick = { hidePassword = !hidePassword }) {
|
||||
Icon(
|
||||
imageVector = if (hidePassword) {
|
||||
Icons.Default.Visibility
|
||||
} else {
|
||||
Icons.Default.VisibilityOff
|
||||
},
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
},
|
||||
visualTransformation = if (hidePassword) {
|
||||
PasswordVisualTransformation()
|
||||
} else {
|
||||
VisualTransformation.None
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
singleLine = true,
|
||||
isError = inputError && password.text.isEmpty(),
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Column {
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = !processing,
|
||||
onClick = {
|
||||
if (username.text.isEmpty() || password.text.isEmpty()) {
|
||||
inputError = true
|
||||
return@Button
|
||||
}
|
||||
scope.launchIO {
|
||||
inputError = false
|
||||
processing = true
|
||||
val result = checkLogin(
|
||||
context = context,
|
||||
service = service,
|
||||
username = username.text,
|
||||
password = password.text,
|
||||
)
|
||||
if (result) onDismissRequest()
|
||||
processing = false
|
||||
}
|
||||
},
|
||||
) {
|
||||
val id = if (processing) R.string.loading else R.string.login
|
||||
Text(text = stringResource(id = id))
|
||||
}
|
||||
TextButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = onDismissRequest,
|
||||
) {
|
||||
Text(text = stringResource(id = android.R.string.cancel))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun checkLogin(
|
||||
context: Context,
|
||||
service: TrackService,
|
||||
username: String,
|
||||
password: String,
|
||||
): Boolean {
|
||||
return try {
|
||||
service.login(username, password)
|
||||
withUIContext { context.toast(R.string.login_success) }
|
||||
true
|
||||
} catch (e: Throwable) {
|
||||
service.logout()
|
||||
withUIContext { context.toast(e.message.toString()) }
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TrackingLogoutDialog(
|
||||
service: TrackService,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.logout_title, stringResource(id = service.nameRes())),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
OutlinedButton(
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = onDismissRequest,
|
||||
) {
|
||||
Text(text = stringResource(id = android.R.string.cancel))
|
||||
}
|
||||
Button(
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = {
|
||||
service.logout()
|
||||
onDismissRequest()
|
||||
context.toast(R.string.logout_success)
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.error,
|
||||
contentColor = MaterialTheme.colorScheme.onError,
|
||||
),
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.logout))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private data class LoginDialog(
|
||||
val service: TrackService,
|
||||
@StringRes val uNameStringRes: Int,
|
||||
)
|
||||
|
||||
private data class LogoutDialog(
|
||||
val service: TrackService,
|
||||
)
|
|
@ -0,0 +1,270 @@
|
|||
package eu.kanade.presentation.more.settings.widget
|
||||
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.domain.ui.model.AppTheme
|
||||
import eu.kanade.presentation.components.DIVIDER_ALPHA
|
||||
import eu.kanade.presentation.components.MangaCover
|
||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||
import eu.kanade.presentation.util.secondaryItemAlpha
|
||||
|
||||
@Composable
|
||||
internal fun AppThemePreferenceWidget(
|
||||
title: String,
|
||||
value: AppTheme,
|
||||
amoled: Boolean,
|
||||
onItemClick: (AppTheme) -> Unit,
|
||||
) {
|
||||
BasePreferenceWidget(
|
||||
title = title,
|
||||
subcomponent = {
|
||||
AppThemesList(
|
||||
currentTheme = value,
|
||||
amoled = amoled,
|
||||
onItemClick = onItemClick,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AppThemesList(
|
||||
currentTheme: AppTheme,
|
||||
amoled: Boolean,
|
||||
onItemClick: (AppTheme) -> Unit,
|
||||
) {
|
||||
val appThemes = remember {
|
||||
AppTheme.values().filter { it.titleResId != null }
|
||||
}
|
||||
LazyRow(
|
||||
modifier = Modifier
|
||||
.animateContentSize()
|
||||
.padding(vertical = 8.dp),
|
||||
contentPadding = PaddingValues(horizontal = HorizontalPadding),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
items(
|
||||
items = appThemes,
|
||||
key = { it.name },
|
||||
) { appTheme ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.width(114.dp)
|
||||
.padding(top = 8.dp),
|
||||
) {
|
||||
TachiyomiTheme(
|
||||
appTheme = appTheme,
|
||||
amoled = amoled,
|
||||
) {
|
||||
AppThemePreviewItem(
|
||||
selected = currentTheme == appTheme,
|
||||
onClick = { onItemClick(appTheme) },
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(id = appTheme.titleResId!!),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp)
|
||||
.secondaryItemAlpha(),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 2,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppThemePreviewItem(
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val dividerColor = MaterialTheme.colorScheme.onSurface.copy(alpha = DIVIDER_ALPHA)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(9f / 16f)
|
||||
.border(
|
||||
width = 4.dp,
|
||||
color = if (selected) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
dividerColor
|
||||
},
|
||||
shape = RoundedCornerShape(17.dp),
|
||||
)
|
||||
.padding(4.dp)
|
||||
.clip(RoundedCornerShape(13.dp))
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.clickable(onClick = onClick),
|
||||
) {
|
||||
// App Bar
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(40.dp)
|
||||
.padding(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight(0.8f)
|
||||
.weight(0.7f)
|
||||
.padding(end = 4.dp)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
shape = RoundedCornerShape(9.dp),
|
||||
),
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier.weight(0.3f),
|
||||
contentAlignment = Alignment.CenterEnd,
|
||||
) {
|
||||
if (selected) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.CheckCircle,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cover
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(start = 8.dp, top = 2.dp)
|
||||
.background(
|
||||
color = dividerColor,
|
||||
shape = RoundedCornerShape(9.dp),
|
||||
)
|
||||
.fillMaxWidth(0.5f)
|
||||
.aspectRatio(MangaCover.Book.ratio),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(4.dp)
|
||||
.size(width = 24.dp, height = 16.dp)
|
||||
.clip(RoundedCornerShape(5.dp)),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.width(12.dp)
|
||||
.background(MaterialTheme.colorScheme.tertiary),
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.width(12.dp)
|
||||
.background(MaterialTheme.colorScheme.secondary),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom bar
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
contentAlignment = Alignment.BottomCenter,
|
||||
) {
|
||||
Surface(
|
||||
tonalElevation = 3.dp,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.height(32.dp)
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
.padding(horizontal = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(17.dp)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
shape = CircleShape,
|
||||
),
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(start = 8.dp)
|
||||
.alpha(0.6f)
|
||||
.height(17.dp)
|
||||
.weight(1f)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
shape = RoundedCornerShape(9.dp),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(
|
||||
name = "light",
|
||||
showBackground = true,
|
||||
)
|
||||
@Preview(
|
||||
name = "dark",
|
||||
showBackground = true,
|
||||
uiMode = UI_MODE_NIGHT_YES,
|
||||
)
|
||||
@Composable
|
||||
private fun AppThemesListPreview() {
|
||||
var appTheme by remember { mutableStateOf(AppTheme.DEFAULT) }
|
||||
TachiyomiTheme {
|
||||
AppThemesList(
|
||||
currentTheme = appTheme,
|
||||
amoled = false,
|
||||
onItemClick = { appTheme = it },
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,176 @@
|
|||
package eu.kanade.presentation.more.settings.widget
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.RepeatMode
|
||||
import androidx.compose.animation.core.StartOffset
|
||||
import androidx.compose.animation.core.StartOffsetType
|
||||
import androidx.compose.animation.core.repeatable
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.more.settings.LocalPreferenceHighlighted
|
||||
import eu.kanade.presentation.util.secondaryItemAlpha
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@Composable
|
||||
internal fun BasePreferenceWidget(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String,
|
||||
subtitle: String? = null,
|
||||
icon: ImageVector? = null,
|
||||
onClick: (() -> Unit)? = null,
|
||||
widget: @Composable (() -> Unit)? = null,
|
||||
) {
|
||||
BasePreferenceWidget(
|
||||
modifier = modifier,
|
||||
title = title,
|
||||
subcomponent = if (!subtitle.isNullOrBlank()) {
|
||||
{
|
||||
Text(
|
||||
text = subtitle,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = HorizontalPadding,
|
||||
top = 4.dp,
|
||||
end = HorizontalPadding,
|
||||
)
|
||||
.secondaryItemAlpha(),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
},
|
||||
icon = icon,
|
||||
onClick = onClick,
|
||||
widget = widget,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun BasePreferenceWidget(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String,
|
||||
subcomponent: @Composable (ColumnScope.() -> Unit)? = null,
|
||||
icon: ImageVector? = null,
|
||||
onClick: (() -> Unit)? = null,
|
||||
widget: @Composable (() -> Unit)? = null,
|
||||
) {
|
||||
BasePreferenceWidgetImpl(modifier, title, subcomponent, icon, onClick, widget)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BasePreferenceWidgetImpl(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String,
|
||||
subcomponent: @Composable (ColumnScope.() -> Unit)? = null,
|
||||
icon: ImageVector? = null,
|
||||
onClick: (() -> Unit)? = null,
|
||||
widget: @Composable (() -> Unit)? = null,
|
||||
) {
|
||||
val highlighted = LocalPreferenceHighlighted.current
|
||||
Box(modifier = Modifier.highlightBackground(highlighted)) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.sizeIn(minHeight = 56.dp)
|
||||
.clickable(enabled = onClick != null, onClick = { onClick?.invoke() })
|
||||
.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (icon != null) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(start = HorizontalPadding, end = 12.dp)
|
||||
.secondaryItemAlpha(),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(vertical = 14.dp),
|
||||
) {
|
||||
if (title.isNotBlank()) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = HorizontalPadding),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 2,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
}
|
||||
}
|
||||
subcomponent?.invoke(this)
|
||||
}
|
||||
if (widget != null) {
|
||||
Box(modifier = Modifier.padding(end = HorizontalPadding)) {
|
||||
widget()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun Modifier.highlightBackground(highlighted: Boolean): Modifier = composed {
|
||||
var highlightFlag by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(Unit) {
|
||||
if (highlighted) {
|
||||
highlightFlag = true
|
||||
delay(3000)
|
||||
highlightFlag = false
|
||||
}
|
||||
}
|
||||
val highlight by animateColorAsState(
|
||||
targetValue = if (highlightFlag) {
|
||||
MaterialTheme.colorScheme.surfaceTint.copy(alpha = .12f)
|
||||
} else {
|
||||
Color.Transparent
|
||||
},
|
||||
animationSpec = if (highlightFlag) {
|
||||
repeatable(
|
||||
iterations = 5,
|
||||
animation = tween(durationMillis = 200),
|
||||
repeatMode = RepeatMode.Reverse,
|
||||
initialStartOffset = StartOffset(
|
||||
offsetMillis = 600,
|
||||
offsetType = StartOffsetType.Delay,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
tween(200)
|
||||
},
|
||||
)
|
||||
then(Modifier.background(color = highlight))
|
||||
}
|
||||
|
||||
internal val TrailingWidgetBuffer = 16.dp
|
||||
internal val HorizontalPadding = 16.dp
|
|
@ -0,0 +1,79 @@
|
|||
package eu.kanade.presentation.more.settings.widget
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun EditTextPreferenceWidget(
|
||||
title: String,
|
||||
subtitle: String?,
|
||||
icon: ImageVector?,
|
||||
value: String,
|
||||
onConfirm: suspend (String) -> Boolean,
|
||||
) {
|
||||
val (isDialogShown, showDialog) = remember { mutableStateOf(false) }
|
||||
|
||||
TextPreferenceWidget(
|
||||
title = title,
|
||||
subtitle = subtitle?.format(value),
|
||||
icon = icon,
|
||||
onPreferenceClick = { showDialog(true) },
|
||||
)
|
||||
|
||||
if (isDialogShown) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val onDismissRequest = { showDialog(false) }
|
||||
var textFieldValue by rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||
mutableStateOf(TextFieldValue(value))
|
||||
}
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = { Text(text = title) },
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = textFieldValue,
|
||||
onValueChange = { textFieldValue = it },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
},
|
||||
properties = DialogProperties(
|
||||
usePlatformDefaultWidth = true,
|
||||
),
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
if (onConfirm(textFieldValue.text)) {
|
||||
onDismissRequest()
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(id = android.R.string.ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(id = android.R.string.cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
package eu.kanade.presentation.more.settings.widget
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.components.Divider
|
||||
import eu.kanade.presentation.components.ScrollbarLazyColumn
|
||||
import eu.kanade.presentation.util.isScrolledToEnd
|
||||
import eu.kanade.presentation.util.isScrolledToStart
|
||||
|
||||
@Composable
|
||||
fun <T> ListPreferenceWidget(
|
||||
value: T,
|
||||
title: String,
|
||||
subtitle: String?,
|
||||
icon: ImageVector?,
|
||||
entries: Map<out T, String>,
|
||||
onValueChange: (T) -> Unit,
|
||||
) {
|
||||
val (isDialogShown, showDialog) = remember { mutableStateOf(false) }
|
||||
|
||||
TextPreferenceWidget(
|
||||
title = title,
|
||||
subtitle = subtitle?.format(entries[value]),
|
||||
icon = icon,
|
||||
onPreferenceClick = { showDialog(true) },
|
||||
)
|
||||
|
||||
if (isDialogShown) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showDialog(false) },
|
||||
title = { Text(text = title) },
|
||||
text = {
|
||||
Box {
|
||||
val state = rememberLazyListState()
|
||||
ScrollbarLazyColumn(state = state) {
|
||||
entries.forEach { current ->
|
||||
val isSelected = value == current.key
|
||||
item {
|
||||
DialogRow(
|
||||
label = current.value,
|
||||
isSelected = isSelected,
|
||||
onSelected = {
|
||||
onValueChange(current.key!!)
|
||||
showDialog(false)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!state.isScrolledToStart()) Divider(modifier = Modifier.align(Alignment.TopCenter))
|
||||
if (!state.isScrolledToEnd()) Divider(modifier = Modifier.align(Alignment.BottomCenter))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { showDialog(false) }) {
|
||||
Text(text = stringResource(id = android.R.string.cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DialogRow(
|
||||
label: String,
|
||||
isSelected: Boolean,
|
||||
onSelected: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.selectable(
|
||||
selected = isSelected,
|
||||
onClick = { if (!isSelected) onSelected() },
|
||||
),
|
||||
) {
|
||||
RadioButton(
|
||||
selected = isSelected,
|
||||
onClick = { if (!isSelected) onSelected() },
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodyLarge.merge(),
|
||||
modifier = Modifier.padding(start = 12.dp),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
package eu.kanade.presentation.more.settings.widget
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.AlertDialog
|
||||
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.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import eu.kanade.presentation.more.settings.Preference
|
||||
|
||||
@Composable
|
||||
fun MultiSelectListPreferenceWidget(
|
||||
preference: Preference.PreferenceItem.MultiSelectListPreference,
|
||||
values: Set<String>,
|
||||
onValuesChange: (Set<String>) -> Unit,
|
||||
) {
|
||||
val (isDialogShown, showDialog) = remember { mutableStateOf(false) }
|
||||
|
||||
TextPreferenceWidget(
|
||||
title = preference.title,
|
||||
subtitle = preference.subtitle,
|
||||
icon = preference.icon,
|
||||
onPreferenceClick = { showDialog(true) },
|
||||
)
|
||||
|
||||
if (isDialogShown) {
|
||||
val selected = remember {
|
||||
preference.entries.keys
|
||||
.filter { values.contains(it) }
|
||||
.toMutableStateList()
|
||||
}
|
||||
AlertDialog(
|
||||
onDismissRequest = { showDialog(false) },
|
||||
title = { Text(text = preference.title) },
|
||||
text = {
|
||||
LazyColumn {
|
||||
preference.entries.forEach { current ->
|
||||
item {
|
||||
val isSelected = selected.contains(current.key)
|
||||
val onSelectionChanged = {
|
||||
when (!isSelected) {
|
||||
true -> selected.add(current.key)
|
||||
false -> selected.remove(current.key)
|
||||
}
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onSelectionChanged() },
|
||||
) {
|
||||
Checkbox(
|
||||
checked = isSelected,
|
||||
onCheckedChange = { onSelectionChanged() },
|
||||
)
|
||||
Text(
|
||||
text = current.value,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(start = 12.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
properties = DialogProperties(
|
||||
usePlatformDefaultWidth = true,
|
||||
),
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onValuesChange(selected.toMutableSet())
|
||||
showDialog(false)
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(id = android.R.string.ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showDialog(false) }) {
|
||||
Text(text = stringResource(id = android.R.string.cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package eu.kanade.presentation.more.settings.widget
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
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.unit.dp
|
||||
|
||||
@Composable
|
||||
fun PreferenceGroupHeader(title: String) {
|
||||
Box(
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 8.dp, top = 14.dp),
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package eu.kanade.presentation.more.settings.widget
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Preview
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
||||
@Composable
|
||||
fun SwitchPreferenceWidget(
|
||||
title: String,
|
||||
subtitle: String? = null,
|
||||
icon: ImageVector? = null,
|
||||
checked: Boolean = false,
|
||||
onCheckedChanged: (Boolean) -> Unit,
|
||||
) {
|
||||
BasePreferenceWidget(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
icon = icon,
|
||||
onClick = { onCheckedChanged(!checked) },
|
||||
) {
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = null,
|
||||
modifier = Modifier.padding(start = TrailingWidgetBuffer),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun SwitchPreferenceWidgetPreview() {
|
||||
MaterialTheme {
|
||||
Surface {
|
||||
Column {
|
||||
SwitchPreferenceWidget(
|
||||
title = "Text preference with icon",
|
||||
subtitle = "Text preference summary",
|
||||
icon = Icons.Default.Preview,
|
||||
checked = true,
|
||||
onCheckedChanged = {},
|
||||
)
|
||||
SwitchPreferenceWidget(
|
||||
title = "Text preference",
|
||||
subtitle = "Text preference summary",
|
||||
checked = false,
|
||||
onCheckedChanged = {},
|
||||
)
|
||||
SwitchPreferenceWidget(
|
||||
title = "Text preference no summary",
|
||||
checked = false,
|
||||
onCheckedChanged = {},
|
||||
)
|
||||
SwitchPreferenceWidget(
|
||||
title = "Another text preference no summary",
|
||||
checked = false,
|
||||
onCheckedChanged = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package eu.kanade.presentation.more.settings.widget
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Preview
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
||||
@Composable
|
||||
fun TextPreferenceWidget(
|
||||
title: String,
|
||||
subtitle: String? = null,
|
||||
icon: ImageVector? = null,
|
||||
onPreferenceClick: (() -> Unit)? = null,
|
||||
) {
|
||||
// TODO: Handle auth requirement here?
|
||||
BasePreferenceWidget(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
icon = icon,
|
||||
onClick = onPreferenceClick,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun TextPreferenceWidgetPreview() {
|
||||
MaterialTheme {
|
||||
Surface {
|
||||
Column {
|
||||
TextPreferenceWidget(
|
||||
title = "Text preference with icon",
|
||||
subtitle = "Text preference summary",
|
||||
icon = Icons.Default.Preview,
|
||||
onPreferenceClick = {},
|
||||
)
|
||||
TextPreferenceWidget(
|
||||
title = "Text preference",
|
||||
subtitle = "Text preference summary",
|
||||
onPreferenceClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
package eu.kanade.presentation.more.settings.widget
|
||||
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material3.Icon
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.more.settings.LocalPreferenceHighlighted
|
||||
|
||||
@Composable
|
||||
fun TrackingPreferenceWidget(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String,
|
||||
@DrawableRes logoRes: Int,
|
||||
@ColorInt logoColor: Int,
|
||||
checked: Boolean,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
val highlighted = LocalPreferenceHighlighted.current
|
||||
Box(modifier = Modifier.highlightBackground(highlighted)) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clickable(enabled = onClick != null, onClick = { onClick?.invoke() })
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.background(color = Color(logoColor), shape = RoundedCornerShape(8.dp))
|
||||
.padding(4.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = logoRes),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = title,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = 16.dp),
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
if (checked) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
modifier = Modifier
|
||||
.padding(4.dp)
|
||||
.size(32.dp),
|
||||
tint = Color(0xFF4CAF50),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
package eu.kanade.presentation.more.settings.widget
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
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.rounded.CheckBox
|
||||
import androidx.compose.material.icons.rounded.CheckBoxOutlineBlank
|
||||
import androidx.compose.material.icons.rounded.DisabledByDefault
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.components.Divider
|
||||
import eu.kanade.presentation.components.LazyColumn
|
||||
import eu.kanade.presentation.util.isScrolledToEnd
|
||||
import eu.kanade.presentation.util.isScrolledToStart
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
private enum class State {
|
||||
CHECKED, INVERSED, UNCHECKED
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T> TriStateListDialog(
|
||||
title: String,
|
||||
message: String? = null,
|
||||
items: List<T>,
|
||||
initialChecked: List<T>,
|
||||
initialInversed: List<T>,
|
||||
itemLabel: @Composable (T) -> String,
|
||||
onDismissRequest: () -> Unit,
|
||||
onValueChanged: (newIncluded: List<T>, newExcluded: List<T>) -> Unit,
|
||||
) {
|
||||
val selected = remember {
|
||||
items
|
||||
.map {
|
||||
when (it) {
|
||||
in initialChecked -> State.CHECKED
|
||||
in initialInversed -> State.INVERSED
|
||||
else -> State.UNCHECKED
|
||||
}
|
||||
}
|
||||
.toMutableStateList()
|
||||
}
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = { Text(text = title) },
|
||||
text = {
|
||||
Column {
|
||||
if (message != null) {
|
||||
Text(
|
||||
text = message,
|
||||
modifier = Modifier.padding(bottom = 8.dp),
|
||||
)
|
||||
}
|
||||
|
||||
Box {
|
||||
val listState = rememberLazyListState()
|
||||
LazyColumn(state = listState) {
|
||||
itemsIndexed(items = items) { index, item ->
|
||||
val state = selected[index]
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(25))
|
||||
.clickable {
|
||||
selected[index] = when (state) {
|
||||
State.UNCHECKED -> State.CHECKED
|
||||
State.CHECKED -> State.INVERSED
|
||||
State.INVERSED -> State.UNCHECKED
|
||||
}
|
||||
}
|
||||
.defaultMinSize(minHeight = 48.dp)
|
||||
.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.padding(end = 20.dp),
|
||||
imageVector = when (state) {
|
||||
State.UNCHECKED -> Icons.Rounded.CheckBoxOutlineBlank
|
||||
State.CHECKED -> Icons.Rounded.CheckBox
|
||||
State.INVERSED -> Icons.Rounded.DisabledByDefault
|
||||
},
|
||||
tint = if (state == State.UNCHECKED) {
|
||||
LocalContentColor.current
|
||||
} else {
|
||||
MaterialTheme.colorScheme.primary
|
||||
},
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(text = itemLabel(item))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!listState.isScrolledToStart()) Divider(modifier = Modifier.align(Alignment.TopCenter))
|
||||
if (!listState.isScrolledToEnd()) Divider(modifier = Modifier.align(Alignment.BottomCenter))
|
||||
}
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(id = android.R.string.cancel))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
val included = items.mapIndexedNotNull { index, category ->
|
||||
if (selected[index] == State.CHECKED) category else null
|
||||
}
|
||||
val excluded = items.mapIndexedNotNull { index, category ->
|
||||
if (selected[index] == State.INVERSED) category else null
|
||||
}
|
||||
onValueChanged(included, excluded)
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(id = android.R.string.ok))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
|
@ -1,10 +1,15 @@
|
|||
package eu.kanade.presentation.theme
|
||||
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import com.google.android.material.composethemeadapter3.createMdc3Theme
|
||||
import eu.kanade.domain.ui.model.AppTheme
|
||||
import eu.kanade.tachiyomi.ui.base.delegate.ThemingDelegate
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
@Composable
|
||||
fun TachiyomiTheme(content: @Composable () -> Unit) {
|
||||
|
@ -22,3 +27,29 @@ fun TachiyomiTheme(content: @Composable () -> Unit) {
|
|||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TachiyomiTheme(
|
||||
appTheme: AppTheme,
|
||||
amoled: Boolean,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val originalContext = LocalContext.current
|
||||
val layoutDirection = LocalLayoutDirection.current
|
||||
val themedContext = remember(appTheme, originalContext) {
|
||||
val themeResIds = ThemingDelegate.getThemeResIds(appTheme, amoled)
|
||||
themeResIds.fold(originalContext) { context, themeResId ->
|
||||
ContextThemeWrapper(context, themeResId)
|
||||
}
|
||||
}
|
||||
val (colorScheme, typography) = createMdc3Theme(
|
||||
context = themedContext,
|
||||
layoutDirection = layoutDirection,
|
||||
)
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme!!,
|
||||
typography = typography!!,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -8,6 +8,16 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
|
||||
@Composable
|
||||
fun LazyListState.isScrolledToStart(): Boolean {
|
||||
return remember {
|
||||
derivedStateOf {
|
||||
val firstItem = layoutInfo.visibleItemsInfo.firstOrNull()
|
||||
firstItem == null || firstItem.offset == layoutInfo.viewportStartOffset
|
||||
}
|
||||
}.value
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LazyListState.isScrolledToEnd(): Boolean {
|
||||
return remember {
|
||||
|
|
15
app/src/main/java/eu/kanade/presentation/util/Navigator.kt
Normal file
15
app/src/main/java/eu/kanade/presentation/util/Navigator.kt
Normal file
|
@ -0,0 +1,15 @@
|
|||
package eu.kanade.presentation.util
|
||||
|
||||
import androidx.compose.runtime.ProvidableCompositionLocal
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import com.bluelinelabs.conductor.Router
|
||||
|
||||
/**
|
||||
* For interop with Conductor
|
||||
*/
|
||||
val LocalRouter: ProvidableCompositionLocal<Router?> = staticCompositionLocalOf { null }
|
||||
|
||||
/**
|
||||
* For invoking back press to the parent activity
|
||||
*/
|
||||
val LocalBackPress: ProvidableCompositionLocal<(() -> Unit)?> = staticCompositionLocalOf { null }
|
13
app/src/main/java/eu/kanade/presentation/util/Preference.kt
Normal file
13
app/src/main/java/eu/kanade/presentation/util/Preference.kt
Normal file
|
@ -0,0 +1,13 @@
|
|||
package eu.kanade.presentation.util
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import eu.kanade.tachiyomi.core.preference.Preference
|
||||
|
||||
@Composable
|
||||
fun <T> Preference<T>.collectAsState(): State<T> {
|
||||
val flow = remember(this) { changes() }
|
||||
return flow.collectAsState(initial = get())
|
||||
}
|
|
@ -56,6 +56,17 @@ abstract class BasicFullComposeController(bundle: Bundle? = null) :
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Let Compose view handle this
|
||||
override fun handleBack(): Boolean {
|
||||
val dispatcher = (activity as? OnBackPressedDispatcherOwner)?.onBackPressedDispatcher ?: return false
|
||||
return if (dispatcher.hasEnabledCallbacks()) {
|
||||
dispatcher.onBackPressed()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface ComposeContentController {
|
||||
|
|
|
@ -7,7 +7,6 @@ import eu.kanade.tachiyomi.ui.base.controller.RootController
|
|||
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
||||
import eu.kanade.tachiyomi.ui.category.CategoryController
|
||||
import eu.kanade.tachiyomi.ui.download.DownloadController
|
||||
import eu.kanade.tachiyomi.ui.setting.SettingsBackupController
|
||||
import eu.kanade.tachiyomi.ui.setting.SettingsMainController
|
||||
|
||||
class MoreController :
|
||||
|
@ -22,7 +21,7 @@ class MoreController :
|
|||
presenter = presenter,
|
||||
onClickDownloadQueue = { router.pushController(DownloadController()) },
|
||||
onClickCategories = { router.pushController(CategoryController()) },
|
||||
onClickBackupAndRestore = { router.pushController(SettingsBackupController()) },
|
||||
onClickBackupAndRestore = { router.pushController(SettingsMainController(toBackupScreen = true)) },
|
||||
onClickSettings = { router.pushController(SettingsMainController()) },
|
||||
onClickAbout = { router.pushController(AboutController()) },
|
||||
)
|
||||
|
|
|
@ -1,85 +1,49 @@
|
|||
package eu.kanade.tachiyomi.ui.setting
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.ChromeReaderMode
|
||||
import androidx.compose.material.icons.outlined.Code
|
||||
import androidx.compose.material.icons.outlined.GetApp
|
||||
import androidx.compose.material.icons.outlined.Palette
|
||||
import androidx.compose.material.icons.outlined.Security
|
||||
import androidx.compose.material.icons.outlined.SettingsBackupRestore
|
||||
import androidx.compose.material.icons.outlined.Sync
|
||||
import androidx.compose.material.icons.outlined.Tune
|
||||
import android.os.Bundle
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import eu.kanade.presentation.more.settings.SettingsMainScreen
|
||||
import eu.kanade.presentation.more.settings.SettingsSection
|
||||
import eu.kanade.tachiyomi.R
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.core.os.bundleOf
|
||||
import cafe.adriel.voyager.core.stack.StackEvent
|
||||
import cafe.adriel.voyager.navigator.Navigator
|
||||
import cafe.adriel.voyager.transitions.ScreenTransition
|
||||
import eu.kanade.presentation.more.settings.screen.SettingsBackupScreen
|
||||
import eu.kanade.presentation.more.settings.screen.SettingsMainScreen
|
||||
import eu.kanade.presentation.util.LocalBackPress
|
||||
import eu.kanade.presentation.util.LocalRouter
|
||||
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
||||
import eu.kanade.tachiyomi.ui.setting.search.SettingsSearchController
|
||||
import soup.compose.material.motion.animation.materialSharedAxisZ
|
||||
|
||||
class SettingsMainController : BasicFullComposeController() {
|
||||
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))
|
||||
|
||||
private val toBackupScreen = args.getBoolean(TO_BACKUP_SCREEN)
|
||||
|
||||
@Composable
|
||||
override fun ComposeContent() {
|
||||
val settingsSections = listOf(
|
||||
SettingsSection(
|
||||
titleRes = R.string.pref_category_general,
|
||||
painter = rememberVectorPainter(Icons.Outlined.Tune),
|
||||
onClick = { router.pushController(SettingsGeneralController()) },
|
||||
),
|
||||
SettingsSection(
|
||||
titleRes = R.string.pref_category_appearance,
|
||||
painter = rememberVectorPainter(Icons.Outlined.Palette),
|
||||
onClick = { router.pushController(SettingsAppearanceController()) },
|
||||
),
|
||||
SettingsSection(
|
||||
titleRes = R.string.pref_category_library,
|
||||
painter = painterResource(R.drawable.ic_library_outline_24dp),
|
||||
onClick = { router.pushController(SettingsLibraryController()) },
|
||||
),
|
||||
SettingsSection(
|
||||
titleRes = R.string.pref_category_reader,
|
||||
painter = rememberVectorPainter(Icons.Outlined.ChromeReaderMode),
|
||||
onClick = { router.pushController(SettingsReaderController()) },
|
||||
),
|
||||
SettingsSection(
|
||||
titleRes = R.string.pref_category_downloads,
|
||||
painter = rememberVectorPainter(Icons.Outlined.GetApp),
|
||||
onClick = { router.pushController(SettingsDownloadController()) },
|
||||
),
|
||||
SettingsSection(
|
||||
titleRes = R.string.pref_category_tracking,
|
||||
painter = rememberVectorPainter(Icons.Outlined.Sync),
|
||||
onClick = { router.pushController(SettingsTrackingController()) },
|
||||
),
|
||||
SettingsSection(
|
||||
titleRes = R.string.browse,
|
||||
painter = painterResource(R.drawable.ic_browse_outline_24dp),
|
||||
onClick = { router.pushController(SettingsBrowseController()) },
|
||||
),
|
||||
SettingsSection(
|
||||
titleRes = R.string.label_backup,
|
||||
painter = rememberVectorPainter(Icons.Outlined.SettingsBackupRestore),
|
||||
onClick = { router.pushController(SettingsBackupController()) },
|
||||
),
|
||||
SettingsSection(
|
||||
titleRes = R.string.pref_category_security,
|
||||
painter = rememberVectorPainter(Icons.Outlined.Security),
|
||||
onClick = { router.pushController(SettingsSecurityController()) },
|
||||
),
|
||||
SettingsSection(
|
||||
titleRes = R.string.pref_category_advanced,
|
||||
painter = rememberVectorPainter(Icons.Outlined.Code),
|
||||
onClick = { router.pushController(SettingsAdvancedController()) },
|
||||
),
|
||||
Navigator(
|
||||
screen = if (toBackupScreen) SettingsBackupScreen() else SettingsMainScreen,
|
||||
content = {
|
||||
CompositionLocalProvider(
|
||||
LocalRouter provides router,
|
||||
LocalBackPress provides this::back,
|
||||
) {
|
||||
ScreenTransition(
|
||||
navigator = it,
|
||||
transition = { materialSharedAxisZ(forward = it.lastEvent != StackEvent.Pop) },
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
SettingsMainScreen(
|
||||
navigateUp = router::popCurrentController,
|
||||
sections = settingsSections,
|
||||
onClickSearch = { router.pushController(SettingsSearchController()) },
|
||||
)
|
||||
private fun back() {
|
||||
activity?.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
private const val TO_BACKUP_SCREEN = "to_backup_screen"
|
||||
|
|
|
@ -10,6 +10,9 @@ import androidx.biometric.auth.AuthPromptCallback
|
|||
import androidx.biometric.auth.startClass2BiometricOrCredentialAuthentication
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import eu.kanade.tachiyomi.R
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
object AuthenticatorUtil {
|
||||
|
||||
|
@ -43,6 +46,45 @@ object AuthenticatorUtil {
|
|||
)
|
||||
}
|
||||
|
||||
suspend fun FragmentActivity.authenticate(
|
||||
title: String,
|
||||
subtitle: String? = getString(R.string.confirm_lock_change),
|
||||
): Boolean = suspendCancellableCoroutine { cont ->
|
||||
if (!isAuthenticationSupported()) {
|
||||
cont.resume(true)
|
||||
return@suspendCancellableCoroutine
|
||||
}
|
||||
|
||||
startAuthentication(
|
||||
title,
|
||||
subtitle,
|
||||
callback = object : AuthenticationCallback() {
|
||||
override fun onAuthenticationSucceeded(
|
||||
activity: FragmentActivity?,
|
||||
result: BiometricPrompt.AuthenticationResult,
|
||||
) {
|
||||
super.onAuthenticationSucceeded(activity, result)
|
||||
cont.resume(true)
|
||||
}
|
||||
|
||||
override fun onAuthenticationError(
|
||||
activity: FragmentActivity?,
|
||||
errorCode: Int,
|
||||
errString: CharSequence,
|
||||
) {
|
||||
super.onAuthenticationError(activity, errorCode, errString)
|
||||
activity?.toast(errString.toString())
|
||||
cont.resume(false)
|
||||
}
|
||||
|
||||
override fun onAuthenticationFailed(activity: FragmentActivity?) {
|
||||
super.onAuthenticationFailed(activity)
|
||||
cont.resume(false)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if Class 2 biometric or credential lock is set and available to use
|
||||
*/
|
||||
|
|
|
@ -69,6 +69,6 @@ class NetworkHelper(context: Context) {
|
|||
}
|
||||
|
||||
val defaultUserAgent by lazy {
|
||||
preferences.defaultUserAgent().get()
|
||||
preferences.defaultUserAgent().get().trim()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,3 +22,4 @@ accompanist-swiperefresh = { module = "com.google.accompanist:accompanist-swiper
|
|||
accompanist-flowlayout = { module = "com.google.accompanist:accompanist-flowlayout", version.ref = "accompanist" }
|
||||
accompanist-pager-core = { module = "com.google.accompanist:accompanist-pager", version.ref = "accompanist" }
|
||||
accompanist-pager-indicators = { module = "com.google.accompanist:accompanist-pager-indicators", version.ref = "accompanist" }
|
||||
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
|
||||
|
|
|
@ -8,6 +8,7 @@ flowbinding_version = "1.2.0"
|
|||
shizuku_version = "12.2.0"
|
||||
sqldelight = "1.5.4"
|
||||
leakcanary = "2.9.1"
|
||||
voyager = "1.0.0-beta16"
|
||||
|
||||
[libraries]
|
||||
android-shortcut-gradle = "com.github.zellius:android-shortcut-gradle-plugin:0.1.2"
|
||||
|
@ -90,6 +91,12 @@ sqldelight-gradle = { module = "com.squareup.sqldelight:gradle-plugin", version.
|
|||
|
||||
junit = "org.junit.jupiter:junit-jupiter:5.9.1"
|
||||
|
||||
voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
|
||||
voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyager" }
|
||||
|
||||
materialmotion-core = "io.github.fornewid:material-motion-compose-core:0.10.2-beta"
|
||||
numberpicker= "com.chargemap.compose:numberpicker:1.0.3"
|
||||
|
||||
[bundles]
|
||||
reactivex = ["rxandroid", "rxjava", "rxrelay"]
|
||||
okhttp = ["okhttp-core", "okhttp-logging", "okhttp-dnsoverhttps"]
|
||||
|
@ -100,6 +107,7 @@ coil = ["coil-core", "coil-gif", "coil-compose"]
|
|||
flowbinding = ["flowbinding-android", "flowbinding-appcompat"]
|
||||
conductor = ["conductor-core", "conductor-support-preference"]
|
||||
shizuku = ["shizuku-api", "shizuku-provider"]
|
||||
voyager = ["voyager-navigator", "voyager-transitions"]
|
||||
|
||||
[plugins]
|
||||
kotlinter = { id = "org.jmailen.kotlinter", version = "3.12.0" }
|
||||
|
|
Reference in a new issue