Settings: M3 and two pane ui (#8211)

* Settings: M3 and two pane ui

* TrackingLoginDialog: Move close button

* Use small top bar

* Revert "Update voyager to v1.0.0-rc02"

This reverts commit 570fec6ea622a7deae44668f4d9c3317699de2aa.

https://github.com/adrielcafe/voyager/issues/62
(cherry picked from commit 5c5468f9af74be984d0d9cb79da804370ee0e775)

# Conflicts:
#	app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt
#	app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsMainScreen.kt
#	app/src/main/java/eu/kanade/presentation/more/settings/widget/TextPreferenceWidget.kt
This commit is contained in:
Ivan Iskandar 2022-10-16 23:15:01 +07:00 committed by Jobobby04
parent 0b5b9cb5cc
commit a2f6b90547
25 changed files with 581 additions and 356 deletions

View File

@ -56,6 +56,7 @@ import androidx.compose.ui.unit.dp
* @sample androidx.compose.material3.samples.ScaffoldWithSimpleSnackbar
*
* Tachiyomi changes:
* * Pass scroll behavior to top bar by default
* * Remove height constraint for expanded app bar
* * Also take account of fab height when providing inner padding
*
@ -80,6 +81,7 @@ import androidx.compose.ui.unit.dp
@Composable
fun Scaffold(
modifier: Modifier = Modifier,
topBarScrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()),
topBar: @Composable (TopAppBarScrollBehavior) -> Unit = {},
bottomBar: @Composable () -> Unit = {},
snackbarHost: @Composable () -> Unit = {},
@ -89,21 +91,16 @@ fun Scaffold(
contentColor: Color = contentColorFor(containerColor),
content: @Composable (PaddingValues) -> Unit,
) {
/**
* Tachiyomi: Pass scroll behavior to topBar
*/
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
androidx.compose.material3.Surface(
modifier = Modifier
.nestedScroll(scrollBehavior.nestedScrollConnection)
.nestedScroll(topBarScrollBehavior.nestedScrollConnection)
.then(modifier),
color = containerColor,
contentColor = contentColor,
) {
ScaffoldLayout(
fabPosition = floatingActionButtonPosition,
topBar = { topBar(scrollBehavior) },
topBar = { topBar(topBarScrollBehavior) },
bottomBar = bottomBar,
content = content,
snackbar = snackbarHost,

View File

@ -0,0 +1,35 @@
package eu.kanade.presentation.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun TwoPanelBox(
modifier: Modifier = Modifier,
startContent: @Composable BoxScope.() -> Unit,
endContent: @Composable BoxScope.() -> Unit,
) {
BoxWithConstraints(modifier = modifier.fillMaxSize()) {
val firstWidth = (maxWidth / 2).coerceAtMost(450.dp)
val secondWidth = maxWidth - firstWidth
Box(
modifier = Modifier
.align(Alignment.TopStart)
.width(firstWidth),
content = startContent,
)
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.width(secondWidth),
content = endContent,
)
}
}

View File

@ -6,7 +6,6 @@ import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
@ -15,13 +14,11 @@ import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
@ -47,7 +44,6 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.presentation.components.ChapterDownloadAction
import eu.kanade.presentation.components.ExtendedFloatingActionButton
@ -55,6 +51,7 @@ import eu.kanade.presentation.components.LazyColumn
import eu.kanade.presentation.components.MangaBottomActionMenu
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.components.SwipeRefresh
import eu.kanade.presentation.components.TwoPanelBox
import eu.kanade.presentation.components.VerticalFastScroller
import eu.kanade.presentation.manga.components.ChapterHeader
import eu.kanade.presentation.manga.components.ExpandableMangaDescription
@ -636,108 +633,103 @@ fun MangaScreenLargeImpl(
}
},
) { contentPadding ->
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
val firstWidth = (maxWidth / 2).coerceAtMost(450.dp)
val secondWidth = maxWidth - firstWidth
Column(
modifier = Modifier
.align(Alignment.TopStart)
.width(firstWidth)
.verticalScroll(rememberScrollState()),
) {
MangaInfoBox(
windowWidthSizeClass = windowWidthSizeClass,
appBarPadding = contentPadding.calculateTopPadding(),
title = state.manga.title,
author = state.manga.author,
artist = state.manga.artist,
sourceName = remember { state.source.getNameForMangaInfo(state.mergedData?.sources) },
isStubSource = remember { state.source is SourceManager.StubSource },
coverDataProvider = { state.manga },
status = state.manga.status,
onCoverClick = onCoverClicked,
doSearch = onSearch,
)
MangaActionRow(
favorite = state.manga.favorite,
trackingCount = state.trackingCount,
onAddToLibraryClicked = onAddToLibraryClicked,
onWebViewClicked = onWebViewClicked,
onTrackingClicked = onTrackingClicked,
onEditCategory = onEditCategoryClicked,
// SY -->
onMergeClicked = onMergeClicked.takeUnless { state.showMergeInOverflow },
// SY <--
)
// SY -->
metadataDescription?.invoke(
state = state,
openMetadataViewer = onMetadataViewerClicked,
search = { onSearch(it, false) },
)
// SY <--
ExpandableMangaDescription(
defaultExpandState = true,
description = state.manga.description,
tagsProvider = { state.manga.genre },
onTagClicked = onTagClicked,
// SY -->
doSearch = onSearch,
searchMetadataChips = remember(state.meta, state.source.id, state.manga.genre) {
SearchMetadataChips(state.meta, state.source, state.manga.genre)
},
// SY <--
)
// SY -->
if (!state.showRecommendationsInOverflow || state.showMergeWithAnother) {
MangaInfoButtons(
showRecommendsButton = !state.showRecommendationsInOverflow,
showMergeWithAnotherButton = state.showMergeWithAnother,
onRecommendClicked = onRecommendClicked,
onMergeWithAnotherClicked = onMergeWithAnotherClicked,
)
}
if (state.pagePreviewsState !is PagePreviewState.Unused) {
PagePreviews(state.pagePreviewsState, onMorePreviewsClicked)
}
// SY <--
}
VerticalFastScroller(
listState = chapterListState,
modifier = Modifier
.align(Alignment.TopEnd)
.width(secondWidth),
topContentPadding = contentPadding.calculateTopPadding(),
) {
LazyColumn(
modifier = Modifier.fillMaxHeight(),
state = chapterListState,
contentPadding = PaddingValues(
top = contentPadding.calculateTopPadding(),
bottom = contentPadding.calculateBottomPadding(),
),
TwoPanelBox(
startContent = {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState()),
) {
item(
key = MangaScreenItem.CHAPTER_HEADER,
contentType = MangaScreenItem.CHAPTER_HEADER,
) {
ChapterHeader(
chapterCount = chapters.size,
onClick = onFilterButtonClicked,
MangaInfoBox(
windowWidthSizeClass = windowWidthSizeClass,
appBarPadding = contentPadding.calculateTopPadding(),
title = state.manga.title,
author = state.manga.author,
artist = state.manga.artist,
sourceName = remember { state.source.getNameForMangaInfo(state.mergedData?.sources) },
isStubSource = remember { state.source is SourceManager.StubSource },
coverDataProvider = { state.manga },
status = state.manga.status,
onCoverClick = onCoverClicked,
doSearch = onSearch,
)
MangaActionRow(
favorite = state.manga.favorite,
trackingCount = state.trackingCount,
onAddToLibraryClicked = onAddToLibraryClicked,
onWebViewClicked = onWebViewClicked,
onTrackingClicked = onTrackingClicked,
onEditCategory = onEditCategoryClicked,
// SY -->
onMergeClicked = onMergeClicked.takeUnless { state.showMergeInOverflow },
// SY <--
)
// SY -->
metadataDescription?.invoke(
state = state,
openMetadataViewer = onMetadataViewerClicked,
search = { onSearch(it, false) },
)
// SY <--
ExpandableMangaDescription(
defaultExpandState = true,
description = state.manga.description,
tagsProvider = { state.manga.genre },
onTagClicked = onTagClicked,
// SY -->
doSearch = onSearch,
searchMetadataChips = remember(state.meta, state.source.id, state.manga.genre) {
SearchMetadataChips(state.meta, state.source, state.manga.genre)
},
// SY <--
)
// SY -->
if (!state.showRecommendationsInOverflow || state.showMergeWithAnother) {
MangaInfoButtons(
showRecommendsButton = !state.showRecommendationsInOverflow,
showMergeWithAnotherButton = state.showMergeWithAnother,
onRecommendClicked = onRecommendClicked,
onMergeWithAnotherClicked = onMergeWithAnotherClicked,
)
}
sharedChapterItems(
chapters = chapters,
onChapterClicked = onChapterClicked,
onDownloadChapter = onDownloadChapter,
onChapterSelected = onChapterSelected,
)
if (state.pagePreviewsState !is PagePreviewState.Unused) {
PagePreviews(state.pagePreviewsState, onMorePreviewsClicked)
}
// SY <--
}
}
}
},
endContent = {
VerticalFastScroller(
listState = chapterListState,
topContentPadding = contentPadding.calculateTopPadding(),
) {
LazyColumn(
modifier = Modifier.fillMaxHeight(),
state = chapterListState,
contentPadding = PaddingValues(
top = contentPadding.calculateTopPadding(),
bottom = contentPadding.calculateBottomPadding(),
),
) {
item(
key = MangaScreenItem.CHAPTER_HEADER,
contentType = MangaScreenItem.CHAPTER_HEADER,
) {
ChapterHeader(
chapterCount = chapters.size,
onClick = onFilterButtonClicked,
)
}
sharedChapterItems(
chapters = chapters,
onChapterClicked = onChapterClicked,
onDownloadChapter = onDownloadChapter,
onChapterSelected = onChapterSelected,
)
}
}
},
)
}
}
}

View File

@ -2,25 +2,48 @@ package eu.kanade.presentation.more.settings
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.components.AppBar
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.Scaffold
import eu.kanade.tachiyomi.R
@Composable
fun PreferenceScaffold(
@StringRes titleRes: Int,
actions: @Composable RowScope.() -> Unit = {},
onBackPressed: () -> Unit = {},
onBackPressed: (() -> Unit)? = null,
itemsProvider: @Composable () -> List<Preference>,
) {
Scaffold(
topBar = { scrollBehavior ->
AppBar(
title = stringResource(titleRes),
navigateUp = onBackPressed,
topBar = {
TopAppBar(
title = {
Text(
text = stringResource(id = titleRes),
modifier = Modifier.padding(start = 8.dp),
)
},
navigationIcon = {
if (onBackPressed != null) {
IconButton(onClick = onBackPressed) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = stringResource(R.string.abc_action_bar_up_description),
)
}
}
},
actions = actions,
scrollBehavior = scrollBehavior,
scrollBehavior = it,
)
},
content = { contentPadding ->

View File

@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
@ -12,7 +11,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import eu.kanade.presentation.components.Divider
import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.presentation.more.settings.screen.SearchableSettings
import eu.kanade.presentation.more.settings.widget.PreferenceGroupHeader
@ -55,9 +53,6 @@ fun PreferenceScreen(
item {
Column {
if (i != 0) {
Divider(modifier = Modifier.padding(bottom = 8.dp))
}
PreferenceGroupHeader(title = preference.title)
}
}
@ -68,7 +63,9 @@ fun PreferenceScreen(
)
}
item {
Spacer(modifier = Modifier.height(12.dp))
if (i < items.lastIndex) {
Spacer(modifier = Modifier.height(12.dp))
}
}
}

View File

@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.RowScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.PreferenceScaffold
import eu.kanade.presentation.util.LocalBackPress
@ -26,10 +25,10 @@ interface SearchableSettings : Screen {
@Composable
override fun Content() {
val handleBack = LocalBackPress.currentOrThrow
val handleBack = LocalBackPress.current
PreferenceScaffold(
titleRes = getTitleRes(),
onBackPressed = handleBack::invoke,
onBackPressed = if (handleBack != null) handleBack::invoke else null,
actions = { AppBarAction() },
itemsProvider = { getPreferences() },
)

View File

@ -1,7 +1,13 @@
package eu.kanade.presentation.more.settings.screen
import androidx.annotation.StringRes
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.outlined.ChromeReaderMode
import androidx.compose.material.icons.outlined.Code
import androidx.compose.material.icons.outlined.CollectionsBookmark
@ -13,130 +19,227 @@ import androidx.compose.material.icons.outlined.Security
import androidx.compose.material.icons.outlined.SettingsBackupRestore
import androidx.compose.material.icons.outlined.Sync
import androidx.compose.material.icons.outlined.Tune
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.NonRestartableComposable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastFirstOrNull
import androidx.core.graphics.ColorUtils
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.domain.UnsortedPreferences
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.PreferenceScaffold
import eu.kanade.presentation.components.LazyColumn
import eu.kanade.presentation.components.Scaffold
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
import eu.kanade.presentation.util.LocalBackPress
import eu.kanade.presentation.util.collectAsState
import eu.kanade.tachiyomi.R
import exh.assets.EhAssets
import exh.assets.ehassets.EhLogo
import exh.assets.ehassets.MangadexLogo
import exh.md.utils.MdUtil
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
object SettingsMainScreen : SearchableSettings {
@Composable
@ReadOnlyComposable
@StringRes
override fun getTitleRes() = R.string.label_settings
@Composable
@NonRestartableComposable
override fun getPreferences(): List<Preference> {
val navigator = LocalNavigator.currentOrThrow
// SY -->
val isHentaiEnabled by remember { Injekt.get<UnsortedPreferences>() }.isHentaiEnabled().collectAsState()
// SY <--
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()) },
),
// SY -->
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_category_eh),
icon = EhAssets.EhLogo,
onClick = { navigator.push(SettingsEhScreen()) },
enabled = isHentaiEnabled,
),
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_category_mangadex),
icon = EhAssets.MangadexLogo,
onClick = { navigator.push(SettingsMangadexScreen()) },
enabled = remember { MdUtil.getEnabledMangaDexs(Injekt.get()).isNotEmpty() },
),
// SY <--
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_category_advanced),
icon = Icons.Outlined.Code,
onClick = { navigator.push(SettingsAdvancedScreen()) },
),
)
}
object SettingsMainScreen : Screen {
@Composable
override fun Content() {
Content(twoPane = false)
}
@Composable
private fun getPalerSurface(): Color {
val surface = MaterialTheme.colorScheme.surface
val dark = isSystemInDarkTheme()
return remember(surface, dark) {
val arr = FloatArray(3)
ColorUtils.colorToHSL(surface.toArgb(), arr)
arr[2] = if (dark) {
arr[2] - 0.05f
} else {
arr[2] + 0.02f
}.coerceIn(0f, 1f)
Color.hsl(arr[0], arr[1], arr[2])
}
}
@Composable
fun Content(twoPane: Boolean) {
val navigator = LocalNavigator.currentOrThrow
val backPress = LocalBackPress.currentOrThrow
PreferenceScaffold(
titleRes = getTitleRes(),
actions = {
AppBarActions(
listOf(
AppBar.Action(
title = stringResource(R.string.action_search),
icon = Icons.Outlined.Search,
onClick = { navigator.push(SettingsSearchScreen()) },
),
),
)
val containerColor = if (twoPane) getPalerSurface() else MaterialTheme.colorScheme.surface
Scaffold(
topBar = { scrollBehavior ->
// https://issuetracker.google.com/issues/249688556
MaterialTheme(
colorScheme = MaterialTheme.colorScheme.copy(surface = containerColor),
) {
TopAppBar(
title = {
Text(
text = stringResource(R.string.label_settings),
modifier = Modifier.padding(start = 8.dp),
)
},
navigationIcon = {
IconButton(onClick = backPress::invoke) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = stringResource(R.string.abc_action_bar_up_description),
)
}
},
actions = {
AppBarActions(
listOf(
AppBar.Action(
title = stringResource(R.string.action_search),
icon = Icons.Outlined.Search,
onClick = { navigator.navigate(SettingsSearchScreen(), twoPane) },
),
),
)
},
scrollBehavior = scrollBehavior,
)
}
},
containerColor = containerColor,
content = { contentPadding ->
LazyColumn(contentPadding = contentPadding) {
items(
items = items.filter { it.screen.isEnabled() },
key = { it.hashCode() },
) { item ->
var modifier: Modifier = Modifier
var contentColor = LocalContentColor.current
if (twoPane) {
val selected = navigator.items.fastFirstOrNull { it::class == item.screen::class } != null
modifier = Modifier
.padding(horizontal = 8.dp)
.clip(RoundedCornerShape(24.dp))
.then(
if (selected) {
Modifier.background(MaterialTheme.colorScheme.surfaceVariant)
} else {
Modifier
},
)
if (selected) {
contentColor = MaterialTheme.colorScheme.onSurfaceVariant
}
}
CompositionLocalProvider(LocalContentColor provides contentColor) {
TextPreferenceWidget(
modifier = modifier,
title = stringResource(item.titleRes),
subtitle = stringResource(item.subtitleRes),
icon = item.icon,
onPreferenceClick = { navigator.navigate(item.screen, twoPane) },
)
}
}
}
},
onBackPressed = backPress::invoke,
itemsProvider = { getPreferences() },
)
}
private fun Navigator.navigate(screen: Screen, twoPane: Boolean) {
if (twoPane) replaceAll(screen) else push(screen)
}
}
private data class Item(
@StringRes val titleRes: Int,
@StringRes val subtitleRes: Int,
val icon: ImageVector,
val screen: /* SY --> */SearchableSettings /* SY <-- */,
)
private val items = listOf(
Item(
titleRes = R.string.pref_category_general,
subtitleRes = R.string.pref_general_summary,
icon = Icons.Outlined.Tune,
screen = SettingsGeneralScreen(),
),
Item(
titleRes = R.string.pref_category_appearance,
subtitleRes = R.string.pref_appearance_summary,
icon = Icons.Outlined.Palette,
screen = SettingsAppearanceScreen(),
),
Item(
titleRes = R.string.pref_category_library,
subtitleRes = R.string.pref_library_summary,
icon = Icons.Outlined.CollectionsBookmark,
screen = SettingsLibraryScreen(),
),
Item(
titleRes = R.string.pref_category_reader,
subtitleRes = R.string.pref_reader_summary,
icon = Icons.Outlined.ChromeReaderMode,
screen = SettingsReaderScreen(),
),
Item(
titleRes = R.string.pref_category_downloads,
subtitleRes = R.string.pref_downloads_summary,
icon = Icons.Outlined.GetApp,
screen = SettingsDownloadScreen(),
),
Item(
titleRes = R.string.pref_category_tracking,
subtitleRes = R.string.pref_tracking_summary,
icon = Icons.Outlined.Sync,
screen = SettingsTrackingScreen(),
),
Item(
titleRes = R.string.browse,
subtitleRes = R.string.pref_browse_summary,
icon = Icons.Outlined.Explore,
screen = SettingsBrowseScreen(),
),
Item(
titleRes = R.string.label_backup,
subtitleRes = R.string.pref_backup_summary,
icon = Icons.Outlined.SettingsBackupRestore,
screen = SettingsBackupScreen(),
),
Item(
titleRes = R.string.pref_category_security,
subtitleRes = R.string.pref_security_summary,
icon = Icons.Outlined.Security,
screen = SettingsSecurityScreen(),
),
// SY -->
Item(
titleRes = R.string.pref_category_eh,
subtitleRes = R.string.pref_ehentai_summary,
icon = EhAssets.EhLogo,
screen = SettingsEhScreen(),
),
Item(
titleRes = R.string.pref_category_mangadex,
subtitleRes = R.string.pref_mangadex_summary,
icon = EhAssets.MangadexLogo,
screen = SettingsMangadexScreen(),
),
// SY <--
Item(
titleRes = R.string.pref_category_advanced,
subtitleRes = R.string.pref_advanced_summary,
icon = Icons.Outlined.Code,
screen = SettingsAdvancedScreen(),
),
)

View File

@ -11,6 +11,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.AlertDialog
@ -95,7 +96,20 @@ class SettingsMangadexScreen : SearchableSettings {
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(R.string.login_title, mdex.name)) },
title = {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = stringResource(R.string.login_title, mdex.name),
modifier = Modifier.weight(1f),
)
IconButton(onClick = onDismissRequest) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(R.string.action_close),
)
}
}
},
text = {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
OutlinedTextField(
@ -138,51 +152,43 @@ class SettingsMangadexScreen : SearchableSettings {
}
},
confirmButton = {
Column {
Button(
modifier = Modifier.fillMaxWidth(),
enabled = !processing,
onClick = {
if (username.text.isEmpty() || password.text.isEmpty()) {
inputError = true
return@Button
}
scope.launchIO {
try {
inputError = false
processing = true
val result = mdex.login(
username = username.text,
password = password.text,
twoFactorCode = null,
)
if (result) {
onDismissRequest()
onLoginSuccess()
withUIContext {
context.toast(R.string.login_success)
}
}
} catch (e: Exception) {
xLogW("Login to Mangadex error", e)
Button(
modifier = Modifier.fillMaxWidth(),
enabled = !processing,
onClick = {
if (username.text.isEmpty() || password.text.isEmpty()) {
inputError = true
return@Button
}
scope.launchIO {
try {
inputError = false
processing = true
val result = mdex.login(
username = username.text,
password = password.text,
twoFactorCode = null,
)
if (result) {
onDismissRequest()
onLoginSuccess()
withUIContext {
e.message?.let { context.toast(it) }
context.toast(R.string.login_success)
}
} finally {
processing = false
}
} catch (e: Exception) {
xLogW("Login to Mangadex error", e)
withUIContext {
e.message?.let { context.toast(it) }
}
} finally {
processing = false
}
},
) {
val id = if (processing) R.string.loading else R.string.login
Text(text = stringResource(id))
}
TextButton(
modifier = Modifier.fillMaxWidth(),
onClick = onDismissRequest,
) {
Text(text = stringResource(android.R.string.cancel))
}
}
},
) {
val id = if (processing) R.string.loading else R.string.login
Text(text = stringResource(id))
}
},
properties = DialogProperties(

View File

@ -146,8 +146,7 @@ class SettingsSearchScreen : Screen {
contentPadding = contentPadding,
) { result ->
SearchableSettings.highlightKey = result.highlightKey
navigator.popUntil { it is SettingsMainScreen }
navigator.push(result.route)
navigator.replace(result.route)
}
}
}

View File

@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.HelpOutline
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
@ -22,7 +23,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue
@ -30,6 +30,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
@ -189,7 +190,20 @@ class SettingsTrackingScreen : SearchableSettings {
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(R.string.login_title, stringResource(service.nameRes()))) },
title = {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = stringResource(R.string.login_title, stringResource(service.nameRes())),
modifier = Modifier.weight(1f),
)
IconButton(onClick = onDismissRequest) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(R.string.action_close),
)
}
}
},
text = {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
OutlinedTextField(
@ -232,38 +246,30 @@ class SettingsTrackingScreen : SearchableSettings {
}
},
confirmButton = {
Column {
Button(
modifier = Modifier.fillMaxWidth(),
enabled = !processing,
onClick = {
if (username.text.isEmpty() || password.text.isEmpty()) {
inputError = true
return@Button
}
scope.launchIO {
inputError = false
processing = true
val result = checkLogin(
context = context,
service = service,
username = username.text,
password = password.text,
)
if (result) onDismissRequest()
processing = false
}
},
) {
val id = if (processing) R.string.loading else R.string.login
Text(text = stringResource(id))
}
TextButton(
modifier = Modifier.fillMaxWidth(),
onClick = onDismissRequest,
) {
Text(text = stringResource(android.R.string.cancel))
}
Button(
modifier = Modifier.fillMaxWidth(),
enabled = !processing,
onClick = {
if (username.text.isEmpty() || password.text.isEmpty()) {
inputError = true
return@Button
}
scope.launchIO {
inputError = false
processing = true
val result = checkLogin(
context = context,
service = service,
username = username.text,
password = password.text,
)
if (result) onDismissRequest()
processing = false
}
},
) {
val id = if (processing) R.string.loading else R.string.login
Text(text = stringResource(id))
}
},
)

View File

@ -109,7 +109,7 @@ private fun AppThemesList(
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center,
maxLines = 2,
style = MaterialTheme.typography.bodySmall,
style = MaterialTheme.typography.bodyMedium,
)
}
}

View File

@ -31,6 +31,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import eu.kanade.presentation.more.settings.LocalPreferenceHighlighted
import eu.kanade.presentation.util.secondaryItemAlpha
import kotlinx.coroutines.delay
@ -54,12 +55,12 @@ internal fun BasePreferenceWidget(
modifier = Modifier
.padding(
start = HorizontalPadding,
top = 4.dp,
top = 0.dp,
end = HorizontalPadding,
)
.secondaryItemAlpha(),
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodySmall,
style = MaterialTheme.typography.bodyMedium,
maxLines = 10,
)
}
} else {
@ -106,15 +107,13 @@ private fun BasePreferenceWidgetImpl(
imageVector = icon,
contentDescription = null,
modifier = Modifier
.padding(start = HorizontalPadding, end = 12.dp)
.secondaryItemAlpha(),
tint = MaterialTheme.colorScheme.onSurface,
.padding(start = HorizontalPadding, end = 0.dp),
)
}
Column(
modifier = Modifier
.weight(1f)
.padding(vertical = 14.dp),
.padding(vertical = 16.dp),
) {
if (title.isNotBlank()) {
Row(
@ -125,7 +124,8 @@ private fun BasePreferenceWidgetImpl(
text = title,
overflow = TextOverflow.Ellipsis,
maxLines = 2,
style = MaterialTheme.typography.bodyLarge,
style = MaterialTheme.typography.titleLarge,
fontSize = 20.sp,
)
}
}
@ -173,4 +173,4 @@ internal fun Modifier.highlightBackground(highlighted: Boolean): Modifier = comp
}
internal val TrailingWidgetBuffer = 16.dp
internal val HorizontalPadding = 16.dp
internal val HorizontalPadding = 24.dp

View File

@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
@ -16,6 +17,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@ -23,6 +25,7 @@ import eu.kanade.presentation.components.Divider
import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.presentation.util.isScrolledToEnd
import eu.kanade.presentation.util.isScrolledToStart
import eu.kanade.presentation.util.minimumTouchTargetSize
@Composable
fun <T> ListPreferenceWidget(
@ -86,20 +89,22 @@ private fun DialogRow(
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.selectable(
selected = isSelected,
onClick = { if (!isSelected) onSelected() },
),
)
.fillMaxWidth()
.minimumTouchTargetSize(),
) {
RadioButton(
selected = isSelected,
onClick = { if (!isSelected) onSelected() },
onClick = null,
)
Text(
text = label,
style = MaterialTheme.typography.bodyLarge.merge(),
modifier = Modifier.padding(start = 12.dp),
modifier = Modifier.padding(start = 24.dp),
)
}
}

View File

@ -1,10 +1,11 @@
package eu.kanade.presentation.more.settings.widget
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme
@ -16,10 +17,12 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.util.minimumTouchTargetSize
@Composable
fun MultiSelectListPreferenceWidget(
@ -59,17 +62,22 @@ fun MultiSelectListPreferenceWidget(
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable { onSelectionChanged() },
.clip(RoundedCornerShape(8.dp))
.selectable(
selected = isSelected,
onClick = { onSelectionChanged() },
)
.minimumTouchTargetSize()
.fillMaxWidth(),
) {
Checkbox(
checked = isSelected,
onCheckedChange = { onSelectionChanged() },
onCheckedChange = null,
)
Text(
text = current.value,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(start = 12.dp),
modifier = Modifier.padding(start = 24.dp),
)
}
}

View File

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

View File

@ -6,18 +6,20 @@ 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.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
@Composable
fun TextPreferenceWidget(
modifier: Modifier = Modifier,
title: String,
subtitle: String? = null,
icon: ImageVector? = null,
onPreferenceClick: (() -> Unit)? = null,
) {
// TODO: Handle auth requirement here?
BasePreferenceWidget(
modifier = modifier,
title = title,
subtitle = subtitle,
icon = icon,

View File

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

View File

@ -78,7 +78,7 @@ fun <T> TriStateListDialog(
val state = selected[index]
Row(
modifier = Modifier
.clip(RoundedCornerShape(25))
.clip(RoundedCornerShape(8.dp))
.clickable {
selected[index] = when (state) {
State.UNCHECKED -> State.CHECKED

View File

@ -1,18 +1,23 @@
package eu.kanade.tachiyomi.ui.setting
import android.os.Bundle
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.core.os.bundleOf
import cafe.adriel.voyager.core.stack.StackEvent
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.transitions.ScreenTransition
import eu.kanade.presentation.components.TwoPanelBox
import eu.kanade.presentation.more.settings.screen.SettingsBackupScreen
import eu.kanade.presentation.more.settings.screen.SettingsGeneralScreen
import eu.kanade.presentation.more.settings.screen.SettingsMainScreen
import eu.kanade.presentation.util.LocalBackPress
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.presentation.util.calculateWindowWidthSizeClass
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
import soup.compose.material.motion.animation.materialSharedAxisZ
import soup.compose.material.motion.animation.materialSharedAxisX
import soup.compose.material.motion.animation.rememberSlideDistance
class SettingsMainController : BasicFullComposeController {
@ -25,20 +30,52 @@ class SettingsMainController : BasicFullComposeController {
@Composable
override fun ComposeContent() {
Navigator(
screen = if (toBackupScreen) SettingsBackupScreen() else SettingsMainScreen,
content = {
CompositionLocalProvider(
LocalRouter provides router,
LocalBackPress provides this::back,
CompositionLocalProvider(LocalRouter provides router) {
val widthSizeClass = calculateWindowWidthSizeClass()
if (widthSizeClass == WindowWidthSizeClass.Compact) {
Navigator(
screen = if (toBackupScreen) SettingsBackupScreen() else SettingsMainScreen,
content = {
CompositionLocalProvider(LocalBackPress provides this::back) {
val slideDistance = rememberSlideDistance()
ScreenTransition(
navigator = it,
transition = {
materialSharedAxisX(
forward = it.lastEvent != StackEvent.Pop,
slideDistance = slideDistance,
)
},
)
}
},
)
} else {
Navigator(
screen = if (toBackupScreen) SettingsBackupScreen() else SettingsGeneralScreen(),
) {
ScreenTransition(
navigator = it,
transition = { materialSharedAxisZ(forward = it.lastEvent != StackEvent.Pop) },
TwoPanelBox(
startContent = {
CompositionLocalProvider(LocalBackPress provides this@SettingsMainController::back) {
SettingsMainScreen.Content(twoPane = true)
}
},
endContent = {
val slideDistance = rememberSlideDistance()
ScreenTransition(
navigator = it,
transition = {
materialSharedAxisX(
forward = it.lastEvent != StackEvent.Pop,
slideDistance = slideDistance,
)
},
)
},
)
}
},
)
}
}
}
private fun back() {

View File

@ -513,10 +513,10 @@
<!-- MangaDex -->
<string name="md_follows_unfollowed">Désabonné</string>
<string name="mangadex_specific_settings">Paramètres de MangaDex</string>
<string name="mangadex_sync_follows_to_library">Sync Mangadex manga dans Neko</string>
<string name="mangadex_sync_follows_to_library">Sync MangaDex manga dans Neko</string>
<string name="mangadex_sync_follows_to_library_summary">Extrait le manga de lecture/relecture de Mangadex dans votre bibliothèque Neko</string>
<string name="mangadex_preffered_source">Source MangaDex préférée</string>
<string name="mangadex_preffered_source_summary">Définissez votre source mangadex choisie, cela sera utilisé pour les suivis et un tas d\'autres fonctionnalités autour de l\'application</string>
<string name="mangadex_preffered_source_summary">Définissez votre source MangaDex choisie, cela sera utilisé pour les suivis et un tas d\'autres fonctionnalités autour de l\'application</string>
<string name="two_factor">Code de double identification </string>
<string name="fields_cannot_be_blank">Les champs ne peut pas être vide</string>
<string name="mangadex_add_to_follows">Ajouter au suivie MangaDex</string>
@ -525,8 +525,8 @@
<!-- Scanlator filters -->
<!-- <string name="select_scanlators">Scanlator groups to show</string>-->
<!-- <string name="no_scanlators">No scanlators available</string>-->
<!--<string name="select_scanlators">Scanlator groups to show</string>-->
<!--<string name="no_scanlators">No scanlators available</string>-->
<!-- Similar -->
<string name="similar">Manga similaire %1$s</string>

View File

@ -601,7 +601,7 @@
<string name="mangadex_sync_follows_to_library">Sincronizar mangá do MangaDex a sua bilioteca</string>
<string name="mangadex_sync_follows_to_library_summary">Move mangás do MangaDex a sua biblioteca, se ainda não adicionados.</string>
<string name="mangadex_preffered_source">Fonte do MangaDex preferida</string>
<string name="mangadex_preffered_source_summary">Define sua fonte do mangadex escolhida, ela será usada para os follows e muitas outras funções ao redor do app</string>
<string name="mangadex_preffered_source_summary">Define sua fonte do MangaDex escolhida, ela será usada para os follows e muitas outras funções ao redor do app</string>
<string name="two_factor">Código 2FA</string>
<string name="fields_cannot_be_blank">Campos não podem estar vazios</string>
<string name="mangadex_add_to_follows">Adicionar aos follows do MangaDex</string>

View File

@ -678,7 +678,7 @@
<!-- MangaDex -->
<string name="md_follows_unfollowed">Без отслеживания</string>
<string name="mangadex_specific_settings">Настройки (MangaDex)</string>
<string name="mangadex_sync_follows_to_library">Синхронизировать серии (Mangadex)</string>
<string name="mangadex_sync_follows_to_library">Синхронизировать серии (MangaDex)</string>
<string name="mangadex_sync_follows_to_library_summary">Добавляет серии из MangaDex в библиотеку, если они ещё не добавлены.</string>
<string name="mangadex_preffered_source">Предпочитаемый источник (MangaDex)</string>
<string name="mangadex_preffered_source_summary">Установить языковой источник MangaDex. Этот источник будет использовать приложение для некоторых функций.</string>

View File

@ -635,10 +635,10 @@
<!-- MangaDex -->
<string name="md_follows_unfollowed">取消关注</string>
<string name="mangadex_specific_settings">MangaDex 设置</string>
<string name="mangadex_sync_follows_to_library">同步 Mangadex 中的漫画到书架</string>
<string name="mangadex_sync_follows_to_library">同步 MangaDex 中的漫画到书架</string>
<string name="mangadex_sync_follows_to_library_summary">如果未被添加则从 MangaDex 拉取漫画到你的书架</string>
<string name="mangadex_preffered_source">首选 MangaDex 来源</string>
<string name="mangadex_preffered_source_summary">设置你所选择的 mangadex 来源,这将用于关注和应用中一系列其他功能。</string>
<string name="mangadex_preffered_source_summary">设置你所选择的 MangaDex 来源,这将用于关注和应用中一系列其他功能。</string>
<string name="two_factor">2FA 验证码</string>
<string name="fields_cannot_be_blank">字段不能为空</string>
<string name="mangadex_add_to_follows">添加到 MangaDex 关注</string>

View File

@ -159,6 +159,17 @@
<string name="pref_category_advanced">Advanced</string>
<string name="pref_category_about">About</string>
<string name="pref_general_summary">App language, notifications</string>
<string name="pref_appearance_summary">Theme, date &amp; time format</string>
<string name="pref_library_summary">Categories, global update</string>
<string name="pref_reader_summary">Reading mode, display, navigation</string>
<string name="pref_downloads_summary">Automatic download, download ahead</string>
<string name="pref_tracking_summary">One-way progress sync, enhanced sync</string>
<string name="pref_browse_summary">Sources, extensions, global search</string>
<string name="pref_backup_summary">Manual &amp; automatic backups</string>
<string name="pref_security_summary">App lock, secure screen</string>
<string name="pref_advanced_summary">Dump crash logs, battery optimizations</string>
<!-- General section -->
<string name="pref_category_theme">Theme</string>
<string name="pref_theme_mode">Dark mode</string>

View File

@ -34,6 +34,9 @@
<string name="pref_category_fork">Fork Settings</string>
<string name="pref_category_mangadex">MangaDex</string>
<string name="pref_ehentai_summary">E-Hentai, ExHentai login, gallery sync</string>
<string name="pref_mangadex_summary">MangaDex, login, follows sync</string>
<!-- EH Settings -->
<string name="ehentai_prefs_account_settings">E-Hentai Website Account Settings</string>
<string name="enable_exhentai">Enable ExHentai</string>
@ -664,10 +667,10 @@
<string name="mdlist">MDList</string>
<string name="md_follows_unfollowed">Unfollowed</string>
<string name="mangadex_specific_settings">MangaDex settings</string>
<string name="mangadex_sync_follows_to_library">Sync Mangadex manga to your library</string>
<string name="mangadex_sync_follows_to_library">Sync MangaDex manga to your library</string>
<string name="mangadex_sync_follows_to_library_summary">Pulls manga from MangaDex into your library if they are not already added.</string>
<string name="mangadex_preffered_source">Preferred MangaDex source</string>
<string name="mangadex_preffered_source_summary">Set your chosen mangadex source, this will be used for follows and a bunch more features around the app</string>
<string name="mangadex_preffered_source_summary">Set your chosen MangaDex source, this will be used for follows and a bunch more features around the app</string>
<string name="two_factor">2FA Code</string>
<string name="fields_cannot_be_blank">Fields cannot be blank</string>
<string name="mangadex_add_to_follows">Add to MangaDex follows</string>
@ -685,7 +688,7 @@
<string name="similar">Similar to %1$s</string>
<string name="similar_no_results">No Similar Manga found</string>
<!-- Mangadex relations-->
<!-- MangaDex relations-->
<string name="relation_similar">Similar</string>
<string name="relation_monochrome">Monochrome</string>
<string name="relation_main_story">Main story</string>