chore: merge upstream

Signed-off-by: KaiserBh <kaiserbh@proton.me>
This commit is contained in:
KaiserBh 2024-01-04 04:34:20 +11:00
commit 227d723622
No known key found for this signature in database
GPG Key ID: 14D73B142042BBA9
67 changed files with 701 additions and 587 deletions

View File

@ -246,7 +246,7 @@ dependencies {
implementation(libs.logcat)
// Crash reports/analytics
implementation(libs.acra.http)
implementation(libs.bundles.acra)
"standardImplementation"(libs.firebase.analytics)
// Shizuku

View File

@ -22,7 +22,6 @@ import tachiyomi.domain.chapter.service.ChapterRecognition
import tachiyomi.domain.manga.model.Manga
import tachiyomi.source.local.isLocal
import java.lang.Long.max
import java.time.Instant
import java.time.ZonedDateTime
import java.util.TreeSet
@ -57,6 +56,7 @@ class SyncChaptersWithSource(
}
val now = ZonedDateTime.now()
val nowMillis = now.toInstant().toEpochMilli()
val sourceChapters = rawSourceChapters
.distinctBy { it.url }
@ -67,36 +67,27 @@ class SyncChaptersWithSource(
.copy(mangaId = manga.id, sourceOrder = i.toLong())
}
// Chapters from db.
val dbChapters = getChaptersByMangaId.await(manga.id)
// Chapters from the source not in db.
val toAdd = mutableListOf<Chapter>()
// Chapters whose metadata have changed.
val toChange = mutableListOf<Chapter>()
// Chapters from the db not in source.
val toDelete = dbChapters.filterNot { dbChapter ->
val newChapters = mutableListOf<Chapter>()
val updatedChapters = mutableListOf<Chapter>()
val removedChapters = dbChapters.filterNot { dbChapter ->
sourceChapters.any { sourceChapter ->
dbChapter.url == sourceChapter.url
}
}
val rightNow = Instant.now().toEpochMilli()
// Used to not set upload date of older chapters
// to a higher value than newer chapters
var maxSeenUploadDate = 0L
val sManga = manga.toSManga()
for (sourceChapter in sourceChapters) {
var chapter = sourceChapter
// Update metadata from source if necessary.
if (source is HttpSource) {
val sChapter = chapter.toSChapter()
source.prepareNewChapter(sChapter, sManga)
source.prepareNewChapter(sChapter, manga.toSManga())
chapter = chapter.copyFromSChapter(sChapter)
}
@ -108,13 +99,13 @@ class SyncChaptersWithSource(
if (dbChapter == null) {
val toAddChapter = if (chapter.dateUpload == 0L) {
val altDateUpload = if (maxSeenUploadDate == 0L) rightNow else maxSeenUploadDate
val altDateUpload = if (maxSeenUploadDate == 0L) nowMillis else maxSeenUploadDate
chapter.copy(dateUpload = altDateUpload)
} else {
maxSeenUploadDate = max(maxSeenUploadDate, sourceChapter.dateUpload)
chapter
}
toAdd.add(toAddChapter)
newChapters.add(toAddChapter)
} else {
if (shouldUpdateDbChapter.await(dbChapter, chapter)) {
val shouldRenameChapter = downloadProvider.isChapterDirNameChanged(dbChapter, chapter) &&
@ -134,13 +125,13 @@ class SyncChaptersWithSource(
if (chapter.dateUpload != 0L) {
toChangeChapter = toChangeChapter.copy(dateUpload = chapter.dateUpload)
}
toChange.add(toChangeChapter)
updatedChapters.add(toChangeChapter)
}
}
}
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
// Return if there's nothing to add, delete, or update to avoid unnecessary db transactions.
if (newChapters.isEmpty() && removedChapters.isEmpty() && updatedChapters.isEmpty()) {
if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchWindow.first) {
updateManga.awaitUpdateFetchInterval(
manga,
@ -157,20 +148,20 @@ class SyncChaptersWithSource(
val deletedReadChapterNumbers = TreeSet<Double>()
val deletedBookmarkedChapterNumbers = TreeSet<Double>()
toDelete.forEach { chapter ->
removedChapters.forEach { chapter ->
if (chapter.read) deletedReadChapterNumbers.add(chapter.chapterNumber)
if (chapter.bookmark) deletedBookmarkedChapterNumbers.add(chapter.chapterNumber)
deletedChapterNumbers.add(chapter.chapterNumber)
}
val deletedChapterNumberDateFetchMap = toDelete.sortedByDescending { it.dateFetch }
val deletedChapterNumberDateFetchMap = removedChapters.sortedByDescending { it.dateFetch }
.associate { it.chapterNumber to it.dateFetch }
// Date fetch is set in such a way that the upper ones will have bigger value than the lower ones
// Sources MUST return the chapters from most to less recent, which is common.
var itemCount = toAdd.size
var updatedToAdd = toAdd.map { toAddItem ->
var chapter = toAddItem.copy(dateFetch = rightNow + itemCount--)
var itemCount = newChapters.size
var updatedToAdd = newChapters.map { toAddItem ->
var chapter = toAddItem.copy(dateFetch = nowMillis + itemCount--)
if (chapter.isRecognizedNumber.not() || chapter.chapterNumber !in deletedChapterNumbers) return@map chapter
@ -189,8 +180,8 @@ class SyncChaptersWithSource(
chapter
}
if (toDelete.isNotEmpty()) {
val toDeleteIds = toDelete.map { it.id }
if (removedChapters.isNotEmpty()) {
val toDeleteIds = removedChapters.map { it.id }
chapterRepository.removeChaptersWithIds(toDeleteIds)
}
@ -198,8 +189,8 @@ class SyncChaptersWithSource(
updatedToAdd = chapterRepository.addAll(updatedToAdd)
}
if (toChange.isNotEmpty()) {
val chapterUpdates = toChange.map { it.toChapterUpdate() }
if (updatedChapters.isNotEmpty()) {
val chapterUpdates = updatedChapters.map { it.toChapterUpdate() }
updateChapter.awaitAll(chapterUpdates)
}
updateManga.awaitUpdateFetchInterval(manga, now, fetchWindow)

View File

@ -295,7 +295,7 @@ private fun DetailsHeader(
top = MaterialTheme.padding.small,
bottom = MaterialTheme.padding.medium,
),
horizontalArrangement = Arrangement.spacedBy(16.dp),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
) {
OutlinedButton(
modifier = Modifier.weight(1f),

View File

@ -71,7 +71,7 @@ fun ExtensionScreen(
searchQuery: String?,
onLongClickItem: (Extension) -> Unit,
onClickItemCancel: (Extension) -> Unit,
onClickItemWebView: (Extension.Available) -> Unit,
onOpenWebView: (Extension.Available) -> Unit,
onInstallExtension: (Extension.Available) -> Unit,
onUninstallExtension: (Extension) -> Unit,
onUpdateExtension: (Extension.Installed) -> Unit,
@ -104,7 +104,7 @@ fun ExtensionScreen(
contentPadding = contentPadding,
onLongClickItem = onLongClickItem,
onClickItemCancel = onClickItemCancel,
onClickItemWebView = onClickItemWebView,
onOpenWebView = onOpenWebView,
onInstallExtension = onInstallExtension,
onUninstallExtension = onUninstallExtension,
onUpdateExtension = onUpdateExtension,
@ -122,8 +122,8 @@ private fun ExtensionContent(
state: ExtensionsScreenModel.State,
contentPadding: PaddingValues,
onLongClickItem: (Extension) -> Unit,
onClickItemWebView: (Extension.Available) -> Unit,
onClickItemCancel: (Extension) -> Unit,
onOpenWebView: (Extension.Available) -> Unit,
onInstallExtension: (Extension.Available) -> Unit,
onUninstallExtension: (Extension) -> Unit,
onUpdateExtension: (Extension.Installed) -> Unit,
@ -202,7 +202,13 @@ private fun ExtensionContent(
}
},
onLongClickItem = onLongClickItem,
onClickItemWebView = onClickItemWebView,
onClickItemSecondaryAction = {
when (it) {
is Extension.Available -> onOpenWebView(it)
is Extension.Installed -> onOpenExtension(it)
else -> {}
}
},
onClickItemCancel = onClickItemCancel,
onClickItemAction = {
when (it) {
@ -243,9 +249,9 @@ private fun ExtensionItem(
item: ExtensionUiModel.Item,
onClickItem: (Extension) -> Unit,
onLongClickItem: (Extension) -> Unit,
onClickItemWebView: (Extension.Available) -> Unit,
onClickItemCancel: (Extension) -> Unit,
onClickItemAction: (Extension) -> Unit,
onClickItemSecondaryAction: (Extension) -> Unit,
modifier: Modifier = Modifier,
) {
val (extension, installStep) = item
@ -287,9 +293,9 @@ private fun ExtensionItem(
ExtensionItemActions(
extension = extension,
installStep = installStep,
onClickItemWebView = onClickItemWebView,
onClickItemCancel = onClickItemCancel,
onClickItemAction = onClickItemAction,
onClickItemSecondaryAction = onClickItemSecondaryAction,
)
},
) {
@ -319,7 +325,7 @@ private fun ExtensionItemContent(
// Won't look good but it's not like we can ellipsize overflowing content
FlowRow(
modifier = Modifier.secondaryItemAlpha(),
horizontalArrangement = Arrangement.spacedBy(4.dp),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
) {
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
if (extension is Extension.Installed && extension.lang.isNotEmpty()) {
@ -371,15 +377,15 @@ private fun ExtensionItemActions(
extension: Extension,
installStep: InstallStep,
modifier: Modifier = Modifier,
onClickItemWebView: (Extension.Available) -> Unit = {},
onClickItemCancel: (Extension) -> Unit = {},
onClickItemAction: (Extension) -> Unit = {},
onClickItemSecondaryAction: (Extension) -> Unit = {},
) {
val isIdle = installStep.isCompleted()
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
when {
!isIdle -> {
@ -401,7 +407,7 @@ private fun ExtensionItemActions(
installStep == InstallStep.Idle -> {
when (extension) {
is Extension.Installed -> {
IconButton(onClick = { onClickItemAction(extension) }) {
IconButton(onClick = { onClickItemSecondaryAction(extension) }) {
Icon(
imageVector = Icons.Outlined.Settings,
contentDescription = stringResource(MR.strings.action_settings),
@ -428,7 +434,7 @@ private fun ExtensionItemActions(
is Extension.Available -> {
if (extension.sources.isNotEmpty()) {
IconButton(
onClick = { onClickItemWebView(extension) },
onClick = { onClickItemSecondaryAction(extension) },
) {
Icon(
imageVector = Icons.Outlined.Public,

View File

@ -37,7 +37,7 @@ fun GlobalSearchCardRow(
LazyRow(
contentPadding = PaddingValues(MaterialTheme.padding.small),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
) {
items(titles) {
val title by getManga(it)

View File

@ -39,7 +39,7 @@ fun GlobalSearchResultItem(
modifier = Modifier
.padding(
start = MaterialTheme.padding.medium,
end = MaterialTheme.padding.tiny,
end = MaterialTheme.padding.extraSmall,
)
.fillMaxWidth()
.clickable(onClick = onClick),

View File

@ -0,0 +1,40 @@
package eu.kanade.presentation.components
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.tachiyomi.util.lang.toRelativeString
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Date
@Composable
fun relativeDateText(
dateEpochMillis: Long,
): String {
return relativeDateText(
date = Date(dateEpochMillis).takeIf { dateEpochMillis > 0L },
)
}
@Composable
fun relativeDateText(
date: Date?,
): String {
val context = LocalContext.current
val preferences = remember { Injekt.get<UiPreferences>() }
val relativeTime = remember { preferences.relativeTime().get() }
val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
return date
?.toRelativeString(
context = context,
relative = relativeTime,
dateFormat = dateFormat,
)
?: stringResource(MR.strings.not_applicable)
}

View File

@ -1,30 +0,0 @@
package eu.kanade.presentation.components
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import eu.kanade.tachiyomi.util.lang.toRelativeString
import tachiyomi.presentation.core.components.ListGroupHeader
import java.text.DateFormat
import java.util.Date
@Composable
fun RelativeDateHeader(
date: Date,
relativeTime: Boolean,
dateFormat: DateFormat,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
ListGroupHeader(
modifier = modifier,
text = remember {
date.toRelativeString(
context,
relativeTime,
dateFormat,
)
},
)
}

View File

@ -8,30 +8,26 @@ import androidx.compose.material.icons.outlined.DeleteSweep
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.AppBarTitle
import eu.kanade.presentation.components.RelativeDateHeader
import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.presentation.components.relativeDateText
import eu.kanade.presentation.history.components.HistoryItem
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.ui.history.HistoryScreenModel
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.core.preference.InMemoryPreferenceStore
import tachiyomi.domain.history.model.HistoryWithRelations
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.ListGroupHeader
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Date
@Composable
@ -42,7 +38,6 @@ fun HistoryScreen(
onClickCover: (mangaId: Long) -> Unit,
onClickResume: (mangaId: Long, chapterId: Long) -> Unit,
onDialogChange: (HistoryScreenModel.Dialog?) -> Unit,
preferences: UiPreferences = Injekt.get(),
) {
Scaffold(
topBar = { scrollBehavior ->
@ -88,7 +83,6 @@ fun HistoryScreen(
onClickCover = { history -> onClickCover(history.mangaId) },
onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) },
onClickDelete = { item -> onDialogChange(HistoryScreenModel.Dialog.Delete(item)) },
preferences = preferences,
)
}
}
@ -102,11 +96,7 @@ private fun HistoryScreenContent(
onClickCover: (HistoryWithRelations) -> Unit,
onClickResume: (HistoryWithRelations) -> Unit,
onClickDelete: (HistoryWithRelations) -> Unit,
preferences: UiPreferences,
) {
val relativeTime = remember { preferences.relativeTime().get() }
val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
FastScrollLazyColumn(
contentPadding = contentPadding,
) {
@ -122,11 +112,9 @@ private fun HistoryScreenContent(
) { item ->
when (item) {
is HistoryUiModel.Header -> {
RelativeDateHeader(
ListGroupHeader(
modifier = Modifier.animateItemPlacement(),
date = item.date,
relativeTime = relativeTime,
dateFormat = dateFormat,
text = relativeDateText(item.date),
)
}
is HistoryUiModel.Item -> {
@ -163,17 +151,6 @@ internal fun HistoryScreenPreviews(
onClickCover = {},
onClickResume = { _, _ -> run {} },
onDialogChange = {},
preferences = UiPreferences(
InMemoryPreferenceStore(
sequenceOf(
InMemoryPreferenceStore.InMemoryPreference(
key = "relative_time_v2",
data = false,
defaultValue = false,
),
),
),
),
)
}
}

View File

@ -3,6 +3,7 @@ package eu.kanade.presentation.history.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@ -11,10 +12,10 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.theme.TachiyomiTheme
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
@Composable
@ -30,7 +31,7 @@ fun HistoryDeleteDialog(
},
text = {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
Text(text = stringResource(MR.strings.dialog_with_checkbox_remove_description))

View File

@ -4,12 +4,13 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Spacer
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
@Composable
@ -28,7 +29,7 @@ fun DuplicateMangaDialog(
},
confirmButton = {
FlowRow(
horizontalArrangement = Arrangement.spacedBy(4.dp),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
) {
TextButton(
onClick = {

View File

@ -47,6 +47,7 @@ import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastMap
import eu.kanade.presentation.components.relativeDateText
import eu.kanade.presentation.manga.components.ChapterDownloadAction
import eu.kanade.presentation.manga.components.ChapterHeader
import eu.kanade.presentation.manga.components.ExpandableMangaDescription
@ -61,7 +62,6 @@ import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.source.getNameForMangaInfo
import eu.kanade.tachiyomi.ui.manga.ChapterList
import eu.kanade.tachiyomi.ui.manga.MangaScreenModel
import eu.kanade.tachiyomi.util.lang.toRelativeString
import eu.kanade.tachiyomi.util.system.copyToClipboard
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.chapter.service.missingChaptersCount
@ -78,16 +78,12 @@ import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.isScrolledToEnd
import tachiyomi.presentation.core.util.isScrollingUp
import tachiyomi.source.local.isLocal
import java.text.DateFormat
import java.util.Date
@Composable
fun MangaScreen(
state: MangaScreenModel.State.Success,
snackbarHostState: SnackbarHostState,
fetchInterval: Int?,
dateRelativeTime: Boolean,
dateFormat: DateFormat,
isTabletUi: Boolean,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
@ -142,8 +138,6 @@ fun MangaScreen(
MangaScreenSmallImpl(
state = state,
snackbarHostState = snackbarHostState,
dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat,
fetchInterval = fetchInterval,
chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction,
@ -179,10 +173,8 @@ fun MangaScreen(
MangaScreenLargeImpl(
state = state,
snackbarHostState = snackbarHostState,
dateRelativeTime = dateRelativeTime,
chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction,
dateFormat = dateFormat,
fetchInterval = fetchInterval,
onBackClicked = onBackClicked,
onChapterClicked = onChapterClicked,
@ -219,8 +211,6 @@ fun MangaScreen(
private fun MangaScreenSmallImpl(
state: MangaScreenModel.State.Success,
snackbarHostState: SnackbarHostState,
dateRelativeTime: Boolean,
dateFormat: DateFormat,
fetchInterval: Int?,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
@ -455,8 +445,6 @@ private fun MangaScreenSmallImpl(
manga = state.manga,
chapters = listItem,
isAnyChapterSelected = chapters.fastAny { it.selected },
dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat,
chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction,
onChapterClicked = onChapterClicked,
@ -474,8 +462,6 @@ private fun MangaScreenSmallImpl(
fun MangaScreenLargeImpl(
state: MangaScreenModel.State.Success,
snackbarHostState: SnackbarHostState,
dateRelativeTime: Boolean,
dateFormat: DateFormat,
fetchInterval: Int?,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
@ -705,8 +691,6 @@ fun MangaScreenLargeImpl(
manga = state.manga,
chapters = listItem,
isAnyChapterSelected = chapters.fastAny { it.selected },
dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat,
chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction,
onChapterClicked = onChapterClicked,
@ -768,8 +752,6 @@ private fun LazyListScope.sharedChapterItems(
manga: Manga,
chapters: List<ChapterList>,
isAnyChapterSelected: Boolean,
dateRelativeTime: Boolean,
dateFormat: DateFormat,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
onChapterClicked: (Chapter) -> Unit,
@ -788,7 +770,6 @@ private fun LazyListScope.sharedChapterItems(
contentType = { MangaScreenItem.CHAPTER },
) { item ->
val haptic = LocalHapticFeedback.current
val context = LocalContext.current
when (item) {
is ChapterList.MissingCount -> {
@ -804,15 +785,7 @@ private fun LazyListScope.sharedChapterItems(
} else {
item.chapter.name
},
date = item.chapter.dateUpload
.takeIf { it > 0L }
?.let {
Date(it).toRelativeString(
context,
dateRelativeTime,
dateFormat,
)
},
date = relativeDateText(item.chapter.dateUpload),
readProgress = item.chapter.lastPageRead
.takeIf { !item.chapter.read && it > 0L }
?.let {

View File

@ -19,8 +19,8 @@ import tachiyomi.presentation.core.components.material.padding
@Composable
fun BaseMangaListItem(
modifier: Modifier = Modifier,
manga: Manga,
modifier: Modifier = Modifier,
onClickItem: () -> Unit = {},
onClickCover: () -> Unit = onClickItem,
cover: @Composable RowScope.() -> Unit = { defaultCover(manga, onClickCover) },

View File

@ -13,6 +13,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.pluralStringResource
import tachiyomi.presentation.core.i18n.stringResource
@ -22,16 +23,17 @@ fun ChapterHeader(
chapterCount: Int?,
missingChapterCount: Int,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = Modifier
modifier = modifier
.fillMaxWidth()
.clickable(
enabled = enabled,
onClick = onClick,
)
.padding(horizontal = 16.dp, vertical = 4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
) {
Text(
text = if (chapterCount == null) {

View File

@ -2,13 +2,24 @@ package eu.kanade.presentation.manga.components
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable
fun DotSeparatorText() {
Text(text = "")
fun DotSeparatorText(
modifier: Modifier = Modifier,
) {
Text(
text = "",
modifier = modifier,
)
}
@Composable
fun DotSeparatorNoSpaceText() {
Text(text = "")
fun DotSeparatorNoSpaceText(
modifier: Modifier = Modifier,
) {
Text(
text = "",
modifier = modifier,
)
}

View File

@ -222,12 +222,12 @@ private fun RowScope.Button(
@Composable
fun LibraryBottomActionMenu(
visible: Boolean,
modifier: Modifier = Modifier,
onChangeCategoryClicked: () -> Unit,
onMarkAsReadClicked: () -> Unit,
onMarkAsUnreadClicked: () -> Unit,
onDownloadClicked: ((DownloadAction) -> Unit)?,
onDeleteClicked: () -> Unit,
modifier: Modifier = Modifier,
) {
AnimatedVisibility(
visible = visible,

View File

@ -22,8 +22,8 @@ enum class MangaCover(val ratio: Float) {
@Composable
operator fun invoke(
modifier: Modifier = Modifier,
data: Any?,
modifier: Modifier = Modifier,
contentDescription: String = "",
shape: Shape = MaterialTheme.shapes.extraSmall,
onClick: (() -> Unit)? = null,

View File

@ -1,6 +1,7 @@
package eu.kanade.presentation.manga.components
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
@ -8,6 +9,7 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
@ -18,7 +20,10 @@ import kotlinx.collections.immutable.toImmutableList
import tachiyomi.domain.manga.interactor.FetchInterval
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.WheelTextPicker
import tachiyomi.presentation.core.i18n.pluralStringResource
import tachiyomi.presentation.core.i18n.stringResource
import java.time.Instant
import java.time.temporal.ChronoUnit
@Composable
fun DeleteChaptersDialog(
@ -54,35 +59,58 @@ fun DeleteChaptersDialog(
@Composable
fun SetIntervalDialog(
interval: Int,
nextUpdate: Long,
onDismissRequest: () -> Unit,
onValueChanged: (Int) -> Unit,
) {
var selectedInterval by rememberSaveable { mutableIntStateOf(if (interval < 0) -interval else 0) }
val nextUpdateDays = remember(nextUpdate) {
val now = Instant.now()
val nextUpdateInstant = Instant.ofEpochMilli(nextUpdate)
now.until(nextUpdateInstant, ChronoUnit.DAYS)
}
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(MR.strings.manga_modify_calculated_interval_title)) },
title = { Text(stringResource(MR.strings.manga_modify_calculated_interval_title)) },
text = {
BoxWithConstraints(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
val size = DpSize(width = maxWidth / 2, height = 128.dp)
val items = (0..FetchInterval.MAX_INTERVAL)
.map {
if (it == 0) {
stringResource(MR.strings.label_default)
} else {
it.toString()
Column {
if (nextUpdateDays >= 0) {
Text(
stringResource(
MR.strings.manga_interval_expected_update,
pluralStringResource(
MR.plurals.day,
count = nextUpdateDays.toInt(),
nextUpdateDays,
),
),
)
}
BoxWithConstraints(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
val size = DpSize(width = maxWidth / 2, height = 128.dp)
val items = (0..FetchInterval.MAX_INTERVAL)
.map {
if (it == 0) {
stringResource(MR.strings.label_default)
} else {
it.toString()
}
}
}
.toImmutableList()
WheelTextPicker(
items = items,
size = size,
startIndex = selectedInterval,
onSelectionChanged = { selectedInterval = it },
)
.toImmutableList()
WheelTextPicker(
items = items,
size = size,
startIndex = selectedInterval,
onSelectionChanged = { selectedInterval = it },
)
}
}
},
dismissButton = {

View File

@ -9,6 +9,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
@ -283,7 +284,7 @@ fun ExpandableMangaDescription(
if (expanded) {
FlowRow(
modifier = Modifier.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
) {
tags.forEach {
TagsChip(
@ -299,7 +300,7 @@ fun ExpandableMangaDescription(
} else {
LazyRow(
contentPadding = PaddingValues(horizontal = MaterialTheme.padding.medium),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
) {
items(items = tags) {
TagsChip(
@ -402,7 +403,7 @@ private fun MangaAndSourceTitlesSmall(
}
@Composable
private fun MangaContentInfo(
private fun ColumnScope.MangaContentInfo(
title: String,
doSearch: (query: String, global: Boolean) -> Unit,
author: String?,
@ -434,7 +435,7 @@ private fun MangaContentInfo(
Row(
modifier = Modifier.secondaryItemAlpha(),
horizontalArrangement = Arrangement.spacedBy(4.dp),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
@ -465,7 +466,7 @@ private fun MangaContentInfo(
if (!artist.isNullOrBlank() && author != artist) {
Row(
modifier = Modifier.secondaryItemAlpha(),
horizontalArrangement = Arrangement.spacedBy(4.dp),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(

View File

@ -59,7 +59,7 @@ fun NewUpdateScreen(
modifier = Modifier.padding(top = MaterialTheme.padding.small),
) {
Text(text = stringResource(MR.strings.update_check_open))
Spacer(modifier = Modifier.width(MaterialTheme.padding.tiny))
Spacer(modifier = Modifier.width(MaterialTheme.padding.extraSmall))
Icon(imageVector = Icons.AutoMirrored.Outlined.OpenInNew, contentDescription = null)
}
}

View File

@ -15,6 +15,7 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.theme.TachiyomiTheme
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
internal class GuidesStep(
@ -29,7 +30,7 @@ internal class GuidesStep(
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
Text(stringResource(MR.strings.onboarding_guides_new_user, stringResource(MR.strings.app_name)))
Button(

View File

@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
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.runtime.LaunchedEffect
@ -20,6 +21,7 @@ import kotlinx.coroutines.flow.collectLatest
import tachiyomi.domain.storage.service.StoragePreferences
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Button
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -40,7 +42,7 @@ internal class StorageStep : OnboardingStep {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
Text(
stringResource(

View File

@ -1,6 +1,7 @@
package eu.kanade.presentation.more.settings.screen
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.activity.compose.ManagedActivityResultLauncher
@ -37,17 +38,20 @@ import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
import eu.kanade.presentation.util.relativeTimeSpanString
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.sync.SyncDataJob
import eu.kanade.tachiyomi.data.sync.SyncManager
import eu.kanade.tachiyomi.data.sync.service.GoogleDriveService
import eu.kanade.tachiyomi.data.sync.service.GoogleDriveSyncService
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.coroutines.launch
import logcat.LogPriority
import tachiyomi.core.i18n.stringResource
import tachiyomi.core.storage.displayablePath
import tachiyomi.core.util.lang.launchNonCancellable
import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.logcat
@ -121,7 +125,7 @@ object SettingsDataScreen : SearchableSettings {
return remember(storageDir) {
val file = UniFile.fromUri(context, storageDir.toUri())
file?.filePath ?: file?.uri?.toString()
file?.displayablePath
} ?: stringResource(MR.strings.invalid_location, storageDir)
}
@ -152,6 +156,22 @@ object SettingsDataScreen : SearchableSettings {
val lastAutoBackup by backupPreferences.lastAutoBackupTimestamp().collectAsState()
val chooseBackup = rememberLauncherForActivityResult(
object : ActivityResultContracts.GetContent() {
override fun createIntent(context: Context, input: String): Intent {
val intent = super.createIntent(context, input)
return Intent.createChooser(intent, context.stringResource(MR.strings.file_select_backup))
}
},
) {
if (it == null) {
context.toast(MR.strings.file_null_uri_error)
return@rememberLauncherForActivityResult
}
navigator.push(RestoreBackupScreen(it.toString()))
}
return Preference.PreferenceGroup(
title = stringResource(MR.strings.label_backup),
preferenceItems = persistentListOf(
@ -175,7 +195,18 @@ object SettingsDataScreen : SearchableSettings {
}
SegmentedButton(
checked = false,
onCheckedChange = { navigator.push(RestoreBackupScreen()) },
onCheckedChange = {
if (!BackupRestoreJob.isRunning(context)) {
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
context.toast(MR.strings.restore_miui_warning)
}
// no need to catch because it's wrapped with a chooser
chooseBackup.launch("*/*")
} else {
context.toast(MR.strings.restore_in_progress)
}
},
shape = SegmentedButtonDefaults.itemShape(1, 2),
) {
Text(stringResource(MR.strings.pref_restore_backup))

View File

@ -317,7 +317,7 @@ object SettingsTrackingScreen : SearchableSettings {
)
},
confirmButton = {
Row(horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny)) {
Row(horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall)) {
OutlinedButton(
modifier = Modifier.weight(1f),
onClick = onDismissRequest,

View File

@ -1,14 +1,12 @@
package eu.kanade.presentation.more.settings.screen.about
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import com.mikepenz.aboutlibraries.ui.compose.LibrariesContainer
import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults
import com.mikepenz.aboutlibraries.ui.compose.util.htmlReadyLicenseContent
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
import com.mikepenz.aboutlibraries.ui.compose.m3.util.htmlReadyLicenseContent
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.util.Screen
import tachiyomi.i18n.MR
@ -33,12 +31,6 @@ class OpenSourceLicensesScreen : Screen() {
modifier = Modifier
.fillMaxSize(),
contentPadding = contentPadding,
colors = LibraryDefaults.libraryColors(
backgroundColor = MaterialTheme.colorScheme.background,
contentColor = MaterialTheme.colorScheme.onBackground,
badgeBackgroundColor = MaterialTheme.colorScheme.primary,
badgeContentColor = MaterialTheme.colorScheme.onPrimary,
),
onLibraryClick = {
val libraryLicenseScreen = OpenSourceLibraryLicenseScreen(
name = it.library.name,

View File

@ -3,19 +3,14 @@ package eu.kanade.presentation.more.settings.screen.advanced
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.FlipToBack
import androidx.compose.material.icons.outlined.SelectAll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@ -50,6 +45,7 @@ import tachiyomi.domain.source.interactor.GetSourcesWithNonLibraryManga
import tachiyomi.domain.source.model.Source
import tachiyomi.domain.source.model.SourceWithCount
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.LazyColumnWithAction
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen
@ -114,7 +110,7 @@ class ClearDatabaseScreen : Screen() {
onClick = model::selectAll,
),
AppBar.Action(
title = stringResource(MR.strings.action_select_all),
title = stringResource(MR.strings.action_select_inverse),
icon = Icons.Outlined.FlipToBack,
onClick = model::invertSelection,
),
@ -132,36 +128,18 @@ class ClearDatabaseScreen : Screen() {
modifier = Modifier.padding(contentPadding),
)
} else {
Column(
modifier = Modifier
.padding(contentPadding)
.fillMaxSize(),
LazyColumnWithAction(
contentPadding = contentPadding,
actionLabel = stringResource(MR.strings.action_delete),
actionEnabled = s.selection.isNotEmpty(),
onClickAction = model::showConfirmation,
) {
LazyColumn(
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) },
)
}
}
HorizontalDivider()
Button(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
.fillMaxWidth(),
onClick = model::showConfirmation,
enabled = s.selection.isNotEmpty(),
) {
Text(
text = stringResource(MR.strings.action_delete),
color = MaterialTheme.colorScheme.onPrimary,
items(s.items) { sourceWithCount ->
ClearDatabaseItem(
source = sourceWithCount.source,
count = sourceWithCount.count,
isSelected = s.selection.contains(sourceWithCount.id),
onClickSelect = { model.toggleSelection(sourceWithCount.source) },
)
}
}

View File

@ -6,22 +6,12 @@ import android.content.Intent
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
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.LazyColumn
import androidx.compose.material3.Button
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
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.unit.dp
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
@ -34,11 +24,13 @@ import eu.kanade.tachiyomi.data.backup.create.BackupCreator
import eu.kanade.tachiyomi.data.backup.create.BackupOptions
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.update
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.components.LazyColumnWithAction
import tachiyomi.presentation.core.components.SectionCard
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
class CreateBackupScreen : Screen() {
@ -73,69 +65,60 @@ class CreateBackupScreen : Screen() {
)
},
) { contentPadding ->
Column(
modifier = Modifier
.padding(contentPadding)
.fillMaxSize(),
LazyColumnWithAction(
contentPadding = contentPadding,
actionLabel = stringResource(MR.strings.action_create),
actionEnabled = state.options.anyEnabled(),
onClickAction = {
if (!BackupCreateJob.isManualJobRunning(context)) {
try {
chooseBackupDir.launch(BackupCreator.getFilename())
} catch (e: ActivityNotFoundException) {
context.toast(MR.strings.file_picker_error)
}
} else {
context.toast(MR.strings.backup_in_progress)
}
},
) {
LazyColumn(
modifier = Modifier.weight(1f),
) {
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
item {
WarningBanner(MR.strings.restore_miui_warning)
}
}
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
item {
LabeledCheckbox(
label = stringResource(MR.strings.manga),
checked = true,
onCheckedChange = {},
enabled = false,
modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium),
)
}
BackupOptions.entries.forEach { option ->
item {
LabeledCheckbox(
label = stringResource(option.label),
checked = option.getter(state.options),
onCheckedChange = {
model.toggle(option.setter, it)
},
modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium),
)
}
WarningBanner(MR.strings.restore_miui_warning)
}
}
HorizontalDivider()
item {
SectionCard(MR.strings.label_library) {
Options(BackupOptions.libraryOptions, state, model)
}
}
Button(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
.fillMaxWidth(),
onClick = {
if (!BackupCreateJob.isManualJobRunning(context)) {
try {
chooseBackupDir.launch(BackupCreator.getFilename())
} catch (e: ActivityNotFoundException) {
context.toast(MR.strings.file_picker_error)
}
} else {
context.toast(MR.strings.backup_in_progress)
}
},
) {
Text(
text = stringResource(MR.strings.action_create),
color = MaterialTheme.colorScheme.onPrimary,
)
item {
SectionCard(MR.strings.label_settings) {
Options(BackupOptions.settingsOptions, state, model)
}
}
}
}
}
@Composable
private fun ColumnScope.Options(
options: ImmutableList<BackupOptions.Entry>,
state: CreateBackupScreenModel.State,
model: CreateBackupScreenModel,
) {
options.forEach { option ->
LabeledCheckbox(
label = stringResource(option.label),
checked = option.getter(state.options),
onCheckedChange = {
model.toggle(option.setter, it)
},
enabled = option.enabled(state.options),
)
}
}
}
private class CreateBackupScreenModel : StateScreenModel<CreateBackupScreenModel.State>(State()) {

View File

@ -1,28 +1,25 @@
package eu.kanade.presentation.more.settings.screen.data
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Arrangement
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.LazyColumn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
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.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import androidx.core.net.toUri
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
@ -34,22 +31,24 @@ import eu.kanade.tachiyomi.data.backup.BackupFileValidator
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
import eu.kanade.tachiyomi.data.backup.restore.RestoreOptions
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.update
import tachiyomi.core.i18n.stringResource
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.components.LazyColumnWithAction
import tachiyomi.presentation.core.components.SectionCard
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
class RestoreBackupScreen : Screen() {
class RestoreBackupScreen(
private val uri: String,
) : Screen() {
@Composable
override fun Content() {
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val model = rememberScreenModel { RestoreBackupScreenModel() }
val model = rememberScreenModel { RestoreBackupScreenModel(context, uri) }
val state by model.state.collectAsState()
Scaffold(
@ -61,121 +60,14 @@ class RestoreBackupScreen : Screen() {
)
},
) { contentPadding ->
if (state.error != null) {
val onDismissRequest = model::clearError
when (val err = state.error) {
is InvalidRestore -> {
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(MR.strings.invalid_backup_file)) },
text = { Text(text = listOfNotNull(err.uri, err.message).joinToString("\n\n")) },
dismissButton = {
TextButton(
onClick = {
context.copyToClipboard(err.message, err.message)
onDismissRequest()
},
) {
Text(text = stringResource(MR.strings.action_copy_to_clipboard))
}
},
confirmButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_ok))
}
},
)
}
is MissingRestoreComponents -> {
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(MR.strings.pref_restore_backup)) },
text = {
Column(
modifier = Modifier.verticalScroll(rememberScrollState()),
) {
val msg = buildString {
append(stringResource(MR.strings.backup_restore_content_full))
if (err.sources.isNotEmpty()) {
append(
"\n\n",
).append(stringResource(MR.strings.backup_restore_missing_sources))
err.sources.joinTo(
this,
separator = "\n- ",
prefix = "\n- ",
)
}
if (err.trackers.isNotEmpty()) {
append(
"\n\n",
).append(stringResource(MR.strings.backup_restore_missing_trackers))
err.trackers.joinTo(
this,
separator = "\n- ",
prefix = "\n- ",
)
}
}
Text(text = msg)
}
},
confirmButton = {
TextButton(
onClick = {
BackupRestoreJob.start(
context = context,
uri = err.uri,
options = state.options,
)
onDismissRequest()
},
) {
Text(text = stringResource(MR.strings.action_restore))
}
},
)
}
else -> onDismissRequest() // Unknown
}
}
val chooseBackup = rememberLauncherForActivityResult(
object : ActivityResultContracts.GetContent() {
override fun createIntent(context: Context, input: String): Intent {
val intent = super.createIntent(context, input)
return Intent.createChooser(intent, context.stringResource(MR.strings.file_select_backup))
}
LazyColumnWithAction(
contentPadding = contentPadding,
actionLabel = stringResource(MR.strings.action_restore),
actionEnabled = state.canRestore && state.options.anyEnabled(),
onClickAction = {
model.startRestore()
navigator.pop()
},
) {
if (it == null) {
context.toast(MR.strings.file_null_uri_error)
return@rememberLauncherForActivityResult
}
val results = try {
BackupFileValidator(context).validate(it)
} catch (e: Exception) {
model.setError(InvalidRestore(it, e.message.toString()))
return@rememberLauncherForActivityResult
}
if (results.missingSources.isEmpty() && results.missingTrackers.isEmpty()) {
BackupRestoreJob.start(
context = context,
uri = it,
options = state.options,
)
return@rememberLauncherForActivityResult
}
model.setError(MissingRestoreComponents(it, results.missingSources, results.missingTrackers))
}
LazyColumn(
modifier = Modifier
.padding(contentPadding)
.fillMaxSize(),
) {
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
item {
@ -183,49 +75,155 @@ class RestoreBackupScreen : Screen() {
}
}
item {
Button(
modifier = Modifier
.padding(horizontal = MaterialTheme.padding.medium)
.fillMaxWidth(),
onClick = {
if (!BackupRestoreJob.isRunning(context)) {
// no need to catch because it's wrapped with a chooser
chooseBackup.launch("*/*")
} else {
context.toast(MR.strings.restore_in_progress)
if (state.canRestore) {
item {
SectionCard {
RestoreOptions.options.forEach { option ->
LabeledCheckbox(
label = stringResource(option.label),
checked = option.getter(state.options),
onCheckedChange = {
model.toggle(option.setter, it)
},
)
}
},
) {
Text(stringResource(MR.strings.pref_restore_backup))
}
}
}
// TODO: show validation errors inline
// TODO: show options for what to restore
if (state.error != null) {
errorMessageItem(state.error)
}
}
}
}
private fun LazyListScope.errorMessageItem(
error: Any?,
) {
item {
SectionCard {
Column(
modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
val msg = buildAnnotatedString {
when (error) {
is MissingRestoreComponents -> {
appendLine(stringResource(MR.strings.backup_restore_content_full))
if (error.sources.isNotEmpty()) {
appendLine()
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
appendLine(stringResource(MR.strings.backup_restore_missing_sources))
}
error.sources.joinTo(
this,
separator = "\n- ",
prefix = "- ",
)
}
if (error.trackers.isNotEmpty()) {
appendLine()
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
appendLine(stringResource(MR.strings.backup_restore_missing_trackers))
}
error.trackers.joinTo(
this,
separator = "\n- ",
prefix = "- ",
)
}
}
is InvalidRestore -> {
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
appendLine(stringResource(MR.strings.invalid_backup_file))
}
appendLine(error.uri.toString())
appendLine()
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
appendLine(stringResource(MR.strings.invalid_backup_file_error))
}
appendLine(error.message)
}
else -> {
appendLine(error.toString())
}
}
}
SelectionContainer {
Text(text = msg)
}
}
}
}
}
}
private class RestoreBackupScreenModel : StateScreenModel<RestoreBackupScreenModel.State>(State()) {
private class RestoreBackupScreenModel(
private val context: Context,
private val uri: String,
) : StateScreenModel<RestoreBackupScreenModel.State>(State()) {
fun setError(error: Any) {
init {
validate(uri.toUri())
}
fun toggle(setter: (RestoreOptions, Boolean) -> RestoreOptions, enabled: Boolean) {
mutableState.update {
it.copy(error = error)
it.copy(
options = setter(it.options, enabled),
)
}
}
fun clearError() {
fun startRestore() {
BackupRestoreJob.start(
context = context,
uri = uri.toUri(),
options = state.value.options,
)
}
private fun validate(uri: Uri) {
val results = try {
BackupFileValidator(context).validate(uri)
} catch (e: Exception) {
setError(
error = InvalidRestore(uri, e.message.toString()),
canRestore = false,
)
return
}
if (results.missingSources.isNotEmpty() || results.missingTrackers.isNotEmpty()) {
setError(
error = MissingRestoreComponents(uri, results.missingSources, results.missingTrackers),
canRestore = true,
)
return
}
setError(error = null, canRestore = true)
}
private fun setError(error: Any?, canRestore: Boolean) {
mutableState.update {
it.copy(error = null)
it.copy(
error = error,
canRestore = canRestore,
)
}
}
@Immutable
data class State(
val error: Any? = null,
// TODO: allow user-selectable restore options
val canRestore: Boolean = false,
val options: RestoreOptions = RestoreOptions(),
)
}

View File

@ -16,6 +16,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import eu.kanade.tachiyomi.util.storage.DiskUtil
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.theme.header
import tachiyomi.presentation.core.util.secondaryItemAlpha
@ -30,7 +31,7 @@ fun StorageInfo(
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
storages.forEach {
StorageInfo(it)
@ -50,7 +51,7 @@ private fun StorageInfo(
val totalText = remember(total) { Formatter.formatFileSize(context, total) }
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
) {
Text(
text = file.absolutePath,

View File

@ -6,7 +6,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.CollectionsBookmark
import androidx.compose.material.icons.outlined.LocalLibrary
@ -18,10 +18,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import eu.kanade.presentation.more.stats.components.StatsItem
import eu.kanade.presentation.more.stats.components.StatsOverviewItem
import eu.kanade.presentation.more.stats.components.StatsSection
import eu.kanade.presentation.more.stats.data.StatsData
import eu.kanade.presentation.util.toDurationString
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.SectionCard
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import java.util.Locale
@ -33,9 +33,7 @@ fun StatsScreenContent(
state: StatsScreenState.Success,
paddingValues: PaddingValues,
) {
val statListState = rememberLazyListState()
LazyColumn(
state = statListState,
contentPadding = paddingValues,
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
@ -55,7 +53,7 @@ fun StatsScreenContent(
}
@Composable
private fun OverviewSection(
private fun LazyItemScope.OverviewSection(
data: StatsData.Overview,
) {
val none = stringResource(MR.strings.none)
@ -65,7 +63,7 @@ private fun OverviewSection(
.toDuration(DurationUnit.MILLISECONDS)
.toDurationString(context, fallback = none)
}
StatsSection(MR.strings.label_overview_section) {
SectionCard(MR.strings.label_overview_section) {
Row(
modifier = Modifier.height(IntrinsicSize.Min),
) {
@ -89,10 +87,10 @@ private fun OverviewSection(
}
@Composable
private fun TitlesStats(
private fun LazyItemScope.TitlesStats(
data: StatsData.Titles,
) {
StatsSection(MR.strings.label_titles_section) {
SectionCard(MR.strings.label_titles_section) {
Row {
StatsItem(
data.globalUpdateItemCount.toString(),
@ -111,10 +109,10 @@ private fun TitlesStats(
}
@Composable
private fun ChapterStats(
private fun LazyItemScope.ChapterStats(
data: StatsData.Chapters,
) {
StatsSection(MR.strings.chapters) {
SectionCard(MR.strings.chapters) {
Row {
StatsItem(
data.totalChapterCount.toString(),
@ -133,7 +131,7 @@ private fun ChapterStats(
}
@Composable
private fun TrackerStats(
private fun LazyItemScope.TrackerStats(
data: StatsData.Trackers,
) {
val notApplicable = stringResource(MR.strings.not_applicable)
@ -145,7 +143,7 @@ private fun TrackerStats(
notApplicable
}
}
StatsSection(MR.strings.label_tracker_section) {
SectionCard(MR.strings.label_tracker_section) {
Row {
StatsItem(
data.trackedTitleCount.toString(),

View File

@ -29,6 +29,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.Viewer
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
private val animationSpec = tween<IntOffset>(200)
@ -156,7 +157,7 @@ fun ReaderAppBars(
) {
Column(
modifier = modifierWithInsetsPadding,
verticalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
ChapterNavigator(
isRtl = isRtl,

View File

@ -10,6 +10,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Check
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
@ -21,6 +22,7 @@ import androidx.compose.ui.unit.dp
import eu.kanade.presentation.theme.TachiyomiTheme
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.SettingsItemsPaddings
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
@Composable
@ -50,7 +52,7 @@ fun ModeSelectionDialog(
onClick = onApply,
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(

View File

@ -309,7 +309,7 @@ private fun SearchResultItemDetails(
title: String,
text: String,
) {
Row(horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny)) {
Row(horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall)) {
Text(
text = title,
maxLines = 1,

View File

@ -17,7 +17,6 @@ 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.util.fastAll
import androidx.compose.ui.util.fastAny
import eu.kanade.presentation.components.AppBar
@ -37,6 +36,7 @@ import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen
import java.util.Date
import kotlin.time.Duration.Companion.seconds
@Composable
@ -44,7 +44,6 @@ fun UpdateScreen(
state: UpdatesScreenModel.State,
snackbarHostState: SnackbarHostState,
lastUpdated: Long,
relativeTime: Boolean,
onClickCover: (UpdatesItem) -> Unit,
onSelectAll: (Boolean) -> Unit,
onInvertSelection: () -> Unit,
@ -58,8 +57,6 @@ fun UpdateScreen(
) {
BackHandler(enabled = state.selectionMode, onBack = { onSelectAll(false) })
val context = LocalContext.current
Scaffold(
topBar = { scrollBehavior ->
UpdatesAppBar(
@ -113,7 +110,7 @@ fun UpdateScreen(
updatesLastUpdatedItem(lastUpdated)
updatesUiItems(
uiModels = state.getUiModel(context, relativeTime),
uiModels = state.getUiModel(),
selectionMode = state.selectionMode,
onUpdateSelected = onUpdateSelected,
onClickCover = onClickCover,
@ -209,6 +206,6 @@ private fun UpdatesBottomBar(
}
sealed interface UpdatesUiModel {
data class Header(val date: String) : UpdatesUiModel
data class Header(val date: Date) : UpdatesUiModel
data class Item(val item: UpdatesItem) : UpdatesUiModel
}

View File

@ -32,6 +32,7 @@ import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.relativeDateText
import eu.kanade.presentation.manga.components.ChapterDownloadAction
import eu.kanade.presentation.manga.components.ChapterDownloadIndicator
import eu.kanade.presentation.manga.components.DotSeparatorText
@ -91,7 +92,7 @@ internal fun LazyListScope.updatesUiItems(
is UpdatesUiModel.Header -> {
ListGroupHeader(
modifier = Modifier.animateItemPlacement(),
text = item.date,
text = relativeDateText(item.date),
)
}
is UpdatesUiModel.Item -> {

View File

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi
import android.annotation.SuppressLint
import android.app.Application
import android.app.PendingIntent
import android.app.job.JobInfo
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
@ -52,10 +53,12 @@ import logcat.AndroidLogcatLogger
import logcat.LogPriority
import logcat.LogcatLogger
import org.acra.config.httpSender
import org.acra.config.scheduler
import org.acra.ktx.initAcra
import org.acra.sender.HttpSender
import org.conscrypt.Conscrypt
import tachiyomi.core.i18n.stringResource
import tachiyomi.core.preference.Preference
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.sync.SyncPreferences
import tachiyomi.i18n.MR
@ -213,12 +216,20 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
if (isPreviewBuildType || isReleaseBuildType) {
initAcra {
buildConfigClass = BuildConfig::class.java
excludeMatchingSharedPreferencesKeys = listOf(".*username.*", ".*password.*", ".*token.*")
excludeMatchingSharedPreferencesKeys = listOf(
Preference.privateKey(".*"), ".*username.*", ".*password.*", ".*token.*",
)
httpSender {
uri = BuildConfig.ACRA_URI
httpMethod = HttpSender.Method.PUT
}
scheduler {
requiresBatteryNotLow = true
requiresDeviceIdle = true
requiresNetworkType = JobInfo.NETWORK_TYPE_UNMETERED
}
}
}
}

View File

@ -3,9 +3,7 @@ package eu.kanade.tachiyomi.data.backup
import android.content.Context
import android.net.Uri
import eu.kanade.tachiyomi.data.track.TrackerManager
import tachiyomi.core.i18n.stringResource
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -19,7 +17,6 @@ class BackupFileValidator(
/**
* Checks for critical backup file data.
*
* @throws Exception if manga cannot be found.
* @return List of missing sources or missing trackers.
*/
fun validate(uri: Uri): Results {
@ -29,10 +26,6 @@ class BackupFileValidator(
throw IllegalStateException(e)
}
if (backup.backupManga.isEmpty()) {
throw IllegalStateException(context.stringResource(MR.strings.invalid_backup_file_missing_manga))
}
val sources = backup.backupSources.associate { it.sourceId to it.name }
val missingSources = sources
.filter { sourceManager.get(it.key) == null }

View File

@ -14,6 +14,7 @@ import eu.kanade.tachiyomi.util.system.notificationBuilder
import eu.kanade.tachiyomi.util.system.notify
import tachiyomi.core.i18n.pluralStringResource
import tachiyomi.core.i18n.stringResource
import tachiyomi.core.storage.displayablePath
import tachiyomi.i18n.MR
import uy.kohesive.injekt.injectLazy
import java.io.File
@ -73,7 +74,7 @@ class BackupNotifier(private val context: Context) {
with(completeNotificationBuilder) {
setContentTitle(context.stringResource(MR.strings.backup_created))
setContentText(file.filePath ?: file.name)
setContentText(file.displayablePath)
clearActions()
addAction(

View File

@ -19,8 +19,6 @@ import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.backup.BackupNotifier
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.lang.asBooleanArray
import eu.kanade.tachiyomi.util.lang.asDataClass
import eu.kanade.tachiyomi.util.system.cancelNotification
import eu.kanade.tachiyomi.util.system.isRunning
import eu.kanade.tachiyomi.util.system.setForegroundSafely
@ -49,7 +47,7 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
setForegroundSafely()
val options: BackupOptions = inputData.getBooleanArray(OPTIONS_KEY)?.asDataClass()
val options = inputData.getBooleanArray(OPTIONS_KEY)?.let { BackupOptions.fromBooleanArray(it) }
?: BackupOptions()
return try {

View File

@ -15,28 +15,53 @@ data class BackupOptions(
val privateSettings: Boolean = false,
) {
fun asBooleanArray() = booleanArrayOf(
libraryEntries,
categories,
chapters,
tracking,
history,
appSettings,
sourceSettings,
privateSettings,
)
fun anyEnabled() = libraryEntries || appSettings || sourceSettings
companion object {
val entries = persistentListOf(
val libraryOptions = persistentListOf(
Entry(
label = MR.strings.manga,
getter = BackupOptions::libraryEntries,
setter = { options, enabled -> options.copy(libraryEntries = enabled) },
),
Entry(
label = MR.strings.categories,
getter = BackupOptions::categories,
setter = { options, enabled -> options.copy(categories = enabled) },
enabled = { it.libraryEntries },
),
Entry(
label = MR.strings.chapters,
getter = BackupOptions::chapters,
setter = { options, enabled -> options.copy(chapters = enabled) },
enabled = { it.libraryEntries },
),
Entry(
label = MR.strings.track,
getter = BackupOptions::tracking,
setter = { options, enabled -> options.copy(tracking = enabled) },
enabled = { it.libraryEntries },
),
Entry(
label = MR.strings.history,
getter = BackupOptions::history,
setter = { options, enabled -> options.copy(history = enabled) },
enabled = { it.libraryEntries },
),
)
val settingsOptions = persistentListOf(
Entry(
label = MR.strings.app_settings,
getter = BackupOptions::appSettings,
@ -51,13 +76,26 @@ data class BackupOptions(
label = MR.strings.private_settings,
getter = BackupOptions::privateSettings,
setter = { options, enabled -> options.copy(privateSettings = enabled) },
enabled = { it.appSettings || it.sourceSettings },
),
)
fun fromBooleanArray(array: BooleanArray) = BackupOptions(
libraryEntries = array[0],
categories = array[1],
chapters = array[2],
tracking = array[3],
history = array[4],
appSettings = array[5],
sourceSettings = array[6],
privateSettings = array[7],
)
}
data class Entry(
val label: StringResource,
val getter: (BackupOptions) -> Boolean,
val setter: (BackupOptions, Boolean) -> BackupOptions,
val enabled: (BackupOptions) -> Boolean = { true },
)
}

View File

@ -37,6 +37,7 @@ class PreferenceBackupCreator(
.withPrivatePreferences(includePrivatePreferences),
)
}
.filter { it.prefs.isNotEmpty() }
}
@Suppress("UNCHECKED_CAST")

View File

@ -13,8 +13,6 @@ import androidx.work.WorkerParameters
import androidx.work.workDataOf
import eu.kanade.tachiyomi.data.backup.BackupNotifier
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.lang.asBooleanArray
import eu.kanade.tachiyomi.util.lang.asDataClass
import eu.kanade.tachiyomi.util.system.cancelNotification
import eu.kanade.tachiyomi.util.system.isRunning
import eu.kanade.tachiyomi.util.system.setForegroundSafely
@ -32,7 +30,7 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet
override suspend fun doWork(): Result {
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri()
val options: RestoreOptions? = inputData.getBooleanArray(OPTIONS_KEY)?.asDataClass()
val options = inputData.getBooleanArray(OPTIONS_KEY)?.let { RestoreOptions.fromBooleanArray(it) }
if (uri == null || options == null) {
return Result.failure()

View File

@ -1,7 +1,52 @@
package eu.kanade.tachiyomi.data.backup.restore
import dev.icerock.moko.resources.StringResource
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.i18n.MR
data class RestoreOptions(
val library: Boolean = true,
val appSettings: Boolean = true,
val sourceSettings: Boolean = true,
val library: Boolean = true,
)
) {
fun asBooleanArray() = booleanArrayOf(
library,
appSettings,
sourceSettings,
)
fun anyEnabled() = library || appSettings || sourceSettings
companion object {
val options = persistentListOf(
Entry(
label = MR.strings.label_library,
getter = RestoreOptions::library,
setter = { options, enabled -> options.copy(library = enabled) },
),
Entry(
label = MR.strings.app_settings,
getter = RestoreOptions::appSettings,
setter = { options, enabled -> options.copy(appSettings = enabled) },
),
Entry(
label = MR.strings.source_settings,
getter = RestoreOptions::sourceSettings,
setter = { options, enabled -> options.copy(sourceSettings = enabled) },
),
)
fun fromBooleanArray(array: BooleanArray) = RestoreOptions(
library = array[0],
appSettings = array[1],
sourceSettings = array[2],
)
}
data class Entry(
val label: StringResource,
val getter: (RestoreOptions) -> Boolean,
val setter: (RestoreOptions, Boolean) -> RestoreOptions,
)
}

View File

@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.util.storage.DiskUtil
import logcat.LogPriority
import tachiyomi.core.i18n.stringResource
import tachiyomi.core.storage.displayablePath
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.manga.model.Manga
@ -41,7 +42,12 @@ class DownloadProvider(
.createDirectory(getMangaDirName(mangaTitle))!!
} catch (e: Throwable) {
logcat(LogPriority.ERROR, e) { "Invalid download directory" }
throw Exception(context.stringResource(MR.strings.invalid_location, downloadsDir ?: ""))
throw Exception(
context.stringResource(
MR.strings.invalid_location,
downloadsDir?.displayablePath ?: "",
),
)
}
}

View File

@ -38,7 +38,6 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import logcat.LogPriority
@ -154,8 +153,8 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
*
* @param categoryId the ID of the category to update, or -1 if no category specified.
*/
private fun addMangaToQueue(categoryId: Long) {
val libraryManga = runBlocking { getLibraryManga.await() }
private suspend fun addMangaToQueue(categoryId: Long) {
val libraryManga = getLibraryManga.await()
val listToUpdate = if (categoryId != -1L) {
libraryManga.filter { it.category == categoryId }
@ -181,7 +180,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val restrictions = libraryPreferences.autoUpdateMangaRestrictions().get()
val skippedUpdates = mutableListOf<Pair<Manga, String?>>()
val fetchWindow = fetchInterval.getWindow(ZonedDateTime.now())
val (_, fetchWindowUpperBound) = fetchInterval.getWindow(ZonedDateTime.now())
mangaToUpdate = listToUpdate
.filter {
@ -208,7 +207,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
false
}
MANGA_OUTSIDE_RELEASE_PERIOD in restrictions && it.manga.nextUpdate > fetchWindow.second -> {
MANGA_OUTSIDE_RELEASE_PERIOD in restrictions && it.manga.nextUpdate > fetchWindowUpperBound -> {
skippedUpdates.add(
it.manga to context.stringResource(MR.strings.skipped_reason_not_in_release_period),
)
@ -220,14 +219,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
}
.sortedBy { it.manga.title }
// Warn when excessively checking a single source
val maxUpdatesFromSource = mangaToUpdate
.groupBy { it.manga.source }
.filterKeys { sourceManager.get(it) !is UnmeteredSource }
.maxOfOrNull { it.value.size } ?: 0
if (maxUpdatesFromSource > MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD) {
notifier.showQueueSizeWarningNotification()
}
notifier.showQueueSizeWarningNotificationIfNeeded(mangaToUpdate)
if (skippedUpdates.isNotEmpty()) {
// TODO: surface skipped reasons to user?

View File

@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.data.download.Downloader
import eu.kanade.tachiyomi.data.notification.NotificationHandler
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.source.UnmeteredSource
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.lang.chop
import eu.kanade.tachiyomi.util.system.cancelNotification
@ -30,15 +31,22 @@ import tachiyomi.core.i18n.pluralStringResource
import tachiyomi.core.i18n.stringResource
import tachiyomi.core.util.lang.launchUI
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.library.model.LibraryManga
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.i18n.MR
import uy.kohesive.injekt.injectLazy
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.math.RoundingMode
import java.text.NumberFormat
class LibraryUpdateNotifier(private val context: Context) {
class LibraryUpdateNotifier(
private val context: Context,
private val securityPreferences: SecurityPreferences = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(),
) {
private val preferences: SecurityPreferences by injectLazy()
private val percentFormatter = NumberFormat.getPercentInstance().apply {
roundingMode = RoundingMode.DOWN
maximumFractionDigits = 0
@ -88,7 +96,7 @@ class LibraryUpdateNotifier(private val context: Context) {
),
)
if (!preferences.hideNotificationContent().get()) {
if (!securityPreferences.hideNotificationContent().get()) {
val updatingText = manga.joinToString("\n") { it.title.chop(40) }
progressNotificationBuilder.setStyle(NotificationCompat.BigTextStyle().bigText(updatingText))
}
@ -101,7 +109,19 @@ class LibraryUpdateNotifier(private val context: Context) {
)
}
fun showQueueSizeWarningNotification() {
/**
* Warn when excessively checking any single source.
*/
fun showQueueSizeWarningNotificationIfNeeded(mangaToUpdate: List<LibraryManga>) {
val maxUpdatesFromSource = mangaToUpdate
.groupBy { it.manga.source }
.filterKeys { sourceManager.get(it) !is UnmeteredSource }
.maxOfOrNull { it.value.size } ?: 0
if (maxUpdatesFromSource <= MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD) {
return
}
context.notify(
Notifications.ID_LIBRARY_SIZE_WARNING,
Notifications.CHANNEL_LIBRARY_PROGRESS,
@ -151,7 +171,7 @@ class LibraryUpdateNotifier(private val context: Context) {
Notifications.CHANNEL_NEW_CHAPTERS,
) {
setContentTitle(context.stringResource(MR.strings.notification_new_chapters))
if (updates.size == 1 && !preferences.hideNotificationContent().get()) {
if (updates.size == 1 && !securityPreferences.hideNotificationContent().get()) {
setContentText(updates.first().first.title.chop(NOTIF_TITLE_MAX_LEN))
} else {
setContentText(
@ -162,7 +182,7 @@ class LibraryUpdateNotifier(private val context: Context) {
),
)
if (!preferences.hideNotificationContent().get()) {
if (!securityPreferences.hideNotificationContent().get()) {
setStyle(
NotificationCompat.BigTextStyle().bigText(
updates.joinToString("\n") {
@ -186,7 +206,7 @@ class LibraryUpdateNotifier(private val context: Context) {
}
// Per-manga notification
if (!preferences.hideNotificationContent().get()) {
if (!securityPreferences.hideNotificationContent().get()) {
launchUI {
context.notify(
updates.map { (manga, chapters) ->
@ -364,3 +384,4 @@ class LibraryUpdateNotifier(private val context: Context) {
private const val NOTIF_MAX_CHAPTERS = 5
private const val NOTIF_TITLE_MAX_LEN = 45
private const val NOTIF_ICON_SIZE = 192
private const val MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD = 60

View File

@ -15,7 +15,6 @@ import eu.kanade.domain.manga.model.copyFrom
import eu.kanade.domain.manga.model.toSManga
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.source.UnmeteredSource
import eu.kanade.tachiyomi.util.prepUpdateCover
import eu.kanade.tachiyomi.util.system.isRunning
import eu.kanade.tachiyomi.util.system.setForegroundSafely
@ -25,7 +24,6 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import logcat.LogPriority
@ -92,17 +90,9 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame
/**
* Adds list of manga to be updated.
*/
private fun addMangaToQueue() {
mangaToUpdate = runBlocking { getLibraryManga.await() }
// Warn when excessively checking a single source
val maxUpdatesFromSource = mangaToUpdate
.groupBy { it.manga.source }
.filterKeys { sourceManager.get(it) !is UnmeteredSource }
.maxOfOrNull { it.value.size } ?: 0
if (maxUpdatesFromSource > MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD) {
notifier.showQueueSizeWarningNotification()
}
private suspend fun addMangaToQueue() {
mangaToUpdate = getLibraryManga.await()
notifier.showQueueSizeWarningNotificationIfNeeded(mangaToUpdate)
}
private suspend fun updateMetadata() {

View File

@ -48,7 +48,7 @@ fun extensionsTab(
},
onClickItemCancel = extensionsScreenModel::cancelInstallUpdateExtension,
onClickUpdateAll = extensionsScreenModel::updateAllExtensions,
onClickItemWebView = { extension ->
onOpenWebView = { extension ->
extension.sources.getOrNull(0)?.let {
navigator.push(
WebViewScreen(

View File

@ -19,7 +19,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.model.StateScreenModel
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
import eu.kanade.domain.manga.interactor.UpdateManga
@ -49,6 +48,7 @@ import tachiyomi.domain.track.interactor.GetTracks
import tachiyomi.domain.track.interactor.InsertTrack
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.LoadingScreen
import uy.kohesive.injekt.Injekt
@ -96,7 +96,7 @@ internal fun MigrateDialog(
},
confirmButton = {
FlowRow(
horizontalArrangement = Arrangement.spacedBy(4.dp),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
) {
TextButton(
onClick = {

View File

@ -104,8 +104,6 @@ class MangaScreen(
MangaScreen(
state = successState,
snackbarHostState = screenModel.snackbarHostState,
dateRelativeTime = screenModel.relativeTime,
dateFormat = screenModel.dateFormat,
fetchInterval = successState.manga.fetchInterval,
isTabletUi = isTabletUi(),
chapterSwipeStartAction = screenModel.chapterSwipeStartAction,
@ -245,6 +243,7 @@ class MangaScreen(
is MangaScreenModel.Dialog.SetFetchInterval -> {
SetIntervalDialog(
interval = dialog.manga.fetchInterval,
nextUpdate = dialog.manga.nextUpdate,
onDismissRequest = onDismissRequest,
onValueChanged = { screenModel.setFetchInterval(dialog.manga, it) },
)

View File

@ -5,7 +5,6 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.util.fastAny
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
@ -22,7 +21,6 @@ import eu.kanade.domain.manga.model.chaptersFiltered
import eu.kanade.domain.manga.model.downloadedFilter
import eu.kanade.domain.manga.model.toSManga
import eu.kanade.domain.track.interactor.AddTracks
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.manga.DownloadAction
import eu.kanade.presentation.manga.components.ChapterDownloadAction
import eu.kanade.presentation.util.formattedMessage
@ -92,7 +90,6 @@ class MangaScreenModel(
private val downloadPreferences: DownloadPreferences = Injekt.get(),
private val libraryPreferences: LibraryPreferences = Injekt.get(),
readerPreferences: ReaderPreferences = Injekt.get(),
uiPreferences: UiPreferences = Injekt.get(),
private val trackerManager: TrackerManager = Injekt.get(),
private val downloadManager: DownloadManager = Injekt.get(),
private val downloadCache: DownloadCache = Injekt.get(),
@ -138,8 +135,6 @@ class MangaScreenModel(
val chapterSwipeStartAction = libraryPreferences.swipeToEndAction().get()
val chapterSwipeEndAction = libraryPreferences.swipeToStartAction().get()
val relativeTime by uiPreferences.relativeTime().asState(screenModelScope)
val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get()))
private val skipFiltered by readerPreferences.skipFiltered().asState(screenModelScope)
val isUpdateIntervalEnabled =

View File

@ -30,7 +30,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.rememberScreenModel
@ -759,7 +758,7 @@ private data class TrackerRemoveScreen(
},
text = {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
Text(
text = stringResource(MR.strings.track_delete_text, serviceName),

View File

@ -1,18 +1,15 @@
package eu.kanade.tachiyomi.ui.updates
import android.app.Application
import android.content.Context
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.core.preference.asState
import eu.kanade.core.util.addOrRemove
import eu.kanade.core.util.insertSeparators
import eu.kanade.domain.chapter.interactor.SetReadStatus
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.manga.components.ChapterDownloadAction
import eu.kanade.presentation.updates.UpdatesUiModel
import eu.kanade.tachiyomi.data.download.DownloadCache
@ -20,7 +17,6 @@ import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.util.lang.toDateKey
import eu.kanade.tachiyomi.util.lang.toRelativeString
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.mutate
import kotlinx.collections.immutable.persistentListOf
@ -63,14 +59,12 @@ class UpdatesScreenModel(
private val getChapter: GetChapter = Injekt.get(),
private val libraryPreferences: LibraryPreferences = Injekt.get(),
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
uiPreferences: UiPreferences = Injekt.get(),
) : StateScreenModel<UpdatesScreenModel.State>(State()) {
private val _events: Channel<Event> = Channel(Int.MAX_VALUE)
val events: Flow<Event> = _events.receiveAsFlow()
val lastUpdated by libraryPreferences.lastUpdatedTimestamp().asState(screenModelScope)
val relativeTime by uiPreferences.relativeTime().asState(screenModelScope)
// First and last selected index in list
private val selectedPositions: Array<Int> = arrayOf(-1, -1)
@ -376,9 +370,7 @@ class UpdatesScreenModel(
val selected = items.filter { it.selected }
val selectionMode = selected.isNotEmpty()
fun getUiModel(context: Context, relativeTime: Boolean): List<UpdatesUiModel> {
val dateFormat by mutableStateOf(UiPreferences.dateFormat(Injekt.get<UiPreferences>().dateFormat().get()))
fun getUiModel(): List<UpdatesUiModel> {
return items
.map { UpdatesUiModel.Item(it) }
.insertSeparators { before, after ->
@ -386,12 +378,7 @@ class UpdatesScreenModel(
val afterDate = after?.item?.update?.dateFetch?.toDateKey() ?: Date(0)
when {
beforeDate.time != afterDate.time && afterDate.time != 0L -> {
val text = afterDate.toRelativeString(
context = context,
relative = relativeTime,
dateFormat = dateFormat,
)
UpdatesUiModel.Header(text)
UpdatesUiModel.Header(afterDate)
}
// Return null to avoid adding a separator between two items.
else -> null

View File

@ -59,7 +59,6 @@ object UpdatesTab : Tab {
state = state,
snackbarHostState = screenModel.snackbarHostState,
lastUpdated = screenModel.lastUpdated,
relativeTime = screenModel.relativeTime,
onClickCover = { item -> navigator.push(MangaScreen(item.update.mangaId)) },
onSelectAll = screenModel::toggleAllSelection,
onInvertSelection = screenModel::invertSelection,

View File

@ -1,18 +0,0 @@
package eu.kanade.tachiyomi.util.lang
import kotlin.reflect.KProperty1
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.primaryConstructor
fun <T : Any> T.asBooleanArray(): BooleanArray {
return this::class.declaredMemberProperties
.filterIsInstance<KProperty1<T, Boolean>>()
.map { it.get(this) }
.toBooleanArray()
}
inline fun <reified T : Any> BooleanArray.asDataClass(): T {
val properties = T::class.declaredMemberProperties.filterIsInstance<KProperty1<T, Boolean>>()
require(properties.size == this.size) { "Boolean array size does not match data class property count" }
return T::class.primaryConstructor!!.call(*this.toTypedArray())
}

View File

@ -46,4 +46,6 @@ dependencies {
// JavaScript engine
implementation(libs.bundles.js.engine)
testImplementation(libs.bundles.test)
}

View File

@ -13,6 +13,9 @@ val UniFile.extension: String?
val UniFile.nameWithoutExtension: String?
get() = name?.substringBeforeLast('.')
val UniFile.displayablePath: String
get() = filePath ?: uri.toString()
fun UniFile.toTempFile(context: Context): File {
val inputStream = context.contentResolver.openInputStream(uri)!!
val tempFile = File.createTempFile(

View File

@ -19,16 +19,15 @@ class FetchInterval(
dateTime: ZonedDateTime,
window: Pair<Long, Long>,
): MangaUpdate? {
val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval(
chapters = getChaptersByMangaId.await(manga.id, applyScanlatorFilter = true),
zone = dateTime.zone,
)
val currentWindow = if (window.first == 0L && window.second == 0L) {
getWindow(ZonedDateTime.now())
} else {
window
}
val chapters = getChaptersByMangaId.await(manga.id, applyScanlatorFilter = true)
val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval(
chapters,
dateTime.zone,
)
val nextUpdate = calculateNextUpdate(manga, interval, dateTime, currentWindow)
return if (manga.nextUpdate == nextUpdate && manga.fetchInterval == interval) {
@ -102,7 +101,7 @@ class FetchInterval(
manga.fetchInterval == 0
) {
val latestDate = ZonedDateTime.ofInstant(
Instant.ofEpochMilli(manga.lastUpdate),
if (manga.lastUpdate > 0) Instant.ofEpochMilli(manga.lastUpdate) else Instant.now(),
dateTime.zone,
)
.toLocalDate()

View File

@ -1,5 +1,6 @@
[versions]
aboutlib_version = "10.9.2"
aboutlib_version = "10.10.0"
acra = "5.11.3"
leakcanary = "2.12"
moko = "0.23.0"
okhttp_version = "5.0.0-alpha.12"
@ -26,7 +27,7 @@ conscrypt-android = "org.conscrypt:conscrypt-android:2.5.2"
quickjs-android = "app.cash.quickjs:quickjs-android:0.9.2"
jsoup = "org.jsoup:jsoup:1.17.1"
jsoup = "org.jsoup:jsoup:1.17.2"
disklrucache = "com.jakewharton:disklrucache:2.0.2"
unifile = "com.github.tachiyomiorg:unifile:7c257e1c64"
@ -67,11 +68,12 @@ moko-gradle = { module = "dev.icerock.moko:resources-generator", version.ref = "
logcat = "com.squareup.logcat:logcat:0.1"
acra-http = "ch.acra:acra-http:5.11.3"
acra-http = { module = "ch.acra:acra-http", version.ref = "acra" }
acra-scheduler = { module = "ch.acra:acra-advanced-scheduler", version.ref = "acra" }
firebase-analytics = "com.google.firebase:firebase-analytics-ktx:21.5.0"
aboutLibraries-gradle = { module = "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin", version.ref = "aboutlib_version" }
aboutLibraries-compose = { module = "com.mikepenz:aboutlibraries-compose", version.ref = "aboutlib_version" }
aboutLibraries-compose = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "aboutlib_version" }
shizuku-api = { module = "dev.rikka.shizuku:api", version.ref = "shizuku_version" }
shizuku-provider = { module = "dev.rikka.shizuku:provider", version.ref = "shizuku_version" }
@ -99,6 +101,7 @@ google-api-services-drive = "com.google.apis:google-api-services-drive:v3-rev197
google-api-client-oauth = "com.google.oauth-client:google-oauth-client:1.34.1"
[bundles]
acra = ["acra-http", "acra-scheduler"]
okhttp = ["okhttp-core", "okhttp-logging", "okhttp-brotli", "okhttp-dnsoverhttps"]
js-engine = ["quickjs-android"]
sqlite = ["sqlite-framework", "sqlite-ktx", "sqlite-android"]

View File

@ -498,11 +498,12 @@
<string name="pref_backup_interval">Automatic backup frequency</string>
<string name="action_create">Create</string>
<string name="backup_created">Backup created</string>
<string name="invalid_backup_file">Invalid backup file</string>
<string name="invalid_backup_file">Invalid backup file:</string>
<string name="invalid_backup_file_error">Full error:</string>
<string name="invalid_backup_file_missing_manga">Backup does not contain any library entries.</string>
<string name="backup_restore_missing_sources">Missing sources:</string>
<string name="backup_restore_missing_trackers">Trackers not logged into:</string>
<string name="backup_restore_content_full">Data from the backup file will be restored.\n\nYou will need to install any missing extensions and log in to tracking services afterwards to use them.</string>
<string name="backup_restore_content_full">You may need to install any missing extensions and log in to tracking services afterwards to use them.</string>
<string name="restore_completed">Restore completed</string>
<string name="restore_duration">%02d min, %02d sec</string>
<string name="backup_in_progress">Backup is already in progress</string>
@ -713,6 +714,8 @@
<string name="display_mode_chapter">Chapter %1$s</string>
<string name="manga_display_interval_title">Estimate every</string>
<string name="manga_display_modified_interval_title">Set to update every</string>
<!-- "... around 2 days" -->
<string name="manga_interval_expected_update">Next update expected in around %s</string>
<string name="manga_modify_calculated_interval_title">Customize interval</string>
<string name="chapter_downloading_progress">Downloading (%1$d/%2$d)</string>
<string name="chapter_error">Error</string>

View File

@ -3,6 +3,7 @@ package tachiyomi.presentation.core.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@ -10,7 +11,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import tachiyomi.presentation.core.components.material.padding
@Composable
fun ActionButton(
@ -24,7 +25,7 @@ fun ActionButton(
onClick = onClick,
) {
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(

View File

@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import tachiyomi.presentation.core.components.material.padding
@Composable
fun LabeledCheckbox(
@ -30,10 +31,14 @@ fun LabeledCheckbox(
.heightIn(min = 48.dp)
.clickable(
role = Role.Checkbox,
onClick = { onCheckedChange(!checked) },
onClick = {
if (enabled) {
onCheckedChange(!checked)
}
},
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
Checkbox(
checked = checked,

View File

@ -0,0 +1,52 @@
package tachiyomi.presentation.core.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material3.Button
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun LazyColumnWithAction(
contentPadding: PaddingValues,
actionLabel: String,
onClickAction: () -> Unit,
modifier: Modifier = Modifier,
actionEnabled: Boolean = true,
content: LazyListScope.() -> Unit,
) {
Column(
modifier = modifier
.padding(contentPadding)
.fillMaxSize(),
) {
LazyColumn(
modifier = Modifier.weight(1f),
content = content,
)
HorizontalDivider()
Button(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
.fillMaxWidth(),
enabled = actionEnabled,
onClick = onClickAction,
) {
Text(
text = actionLabel,
color = MaterialTheme.colorScheme.onPrimary,
)
}
}
}

View File

@ -1,8 +1,10 @@
package eu.kanade.presentation.more.stats.components
package tachiyomi.presentation.core.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@ -13,15 +15,18 @@ import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
@Composable
fun StatsSection(
titleRes: StringResource,
content: @Composable () -> Unit,
fun LazyItemScope.SectionCard(
titleRes: StringResource? = null,
content: @Composable ColumnScope.() -> Unit,
) {
Text(
modifier = Modifier.padding(horizontal = MaterialTheme.padding.extraLarge),
text = stringResource(titleRes),
style = MaterialTheme.typography.titleSmall,
)
if (titleRes != null) {
Text(
modifier = Modifier.padding(horizontal = MaterialTheme.padding.extraLarge),
text = stringResource(titleRes),
style = MaterialTheme.typography.titleSmall,
)
}
ElevatedCard(
modifier = Modifier
.fillMaxWidth()

View File

@ -345,7 +345,7 @@ fun SettingsIconGrid(labelRes: StringResource, content: LazyGridScope.() -> Unit
end = SettingsItemsPaddings.Horizontal,
bottom = SettingsItemsPaddings.Vertical,
),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
content = content,
)

View File

@ -19,7 +19,7 @@ class Padding {
val small = 8.dp
val tiny = 4.dp
val extraSmall = 4.dp
}
val MaterialTheme.padding: Padding

View File

@ -45,11 +45,11 @@ fun NavigationRail(
.fillMaxHeight()
.windowInsetsPadding(windowInsets)
.widthIn(min = 80.dp)
.padding(vertical = MaterialTheme.padding.tiny)
.padding(vertical = MaterialTheme.padding.extraSmall)
.selectableGroup(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(
MaterialTheme.padding.tiny,
MaterialTheme.padding.extraSmall,
alignment = Alignment.CenterVertically,
),
) {