mirror of
https://github.com/mihonapp/mihon.git
synced 2024-11-15 15:02:49 +01:00
chore: merge upstream
Signed-off-by: KaiserBh <kaiserbh@proton.me>
This commit is contained in:
commit
227d723622
@ -246,7 +246,7 @@ dependencies {
|
||||
implementation(libs.logcat)
|
||||
|
||||
// Crash reports/analytics
|
||||
implementation(libs.acra.http)
|
||||
implementation(libs.bundles.acra)
|
||||
"standardImplementation"(libs.firebase.analytics)
|
||||
|
||||
// Shizuku
|
||||
|
@ -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)
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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),
|
||||
|
@ -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)
|
||||
}
|
@ -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,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
|
||||
|
@ -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 = {
|
||||
|
@ -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 {
|
||||
|
@ -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) },
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,15 +59,37 @@ 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 = {
|
||||
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,
|
||||
@ -84,6 +111,7 @@ fun SetIntervalDialog(
|
||||
onSelectionChanged = { selectedInterval = it },
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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))
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,13 +128,11 @@ class ClearDatabaseScreen : Screen() {
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
)
|
||||
} else {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(contentPadding)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(1f),
|
||||
LazyColumnWithAction(
|
||||
contentPadding = contentPadding,
|
||||
actionLabel = stringResource(MR.strings.action_delete),
|
||||
actionEnabled = s.selection.isNotEmpty(),
|
||||
onClickAction = model::showConfirmation,
|
||||
) {
|
||||
items(s.items) { sourceWithCount ->
|
||||
ClearDatabaseItem(
|
||||
@ -149,22 +143,6 @@ class ClearDatabaseScreen : Screen() {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,50 +65,11 @@ class CreateBackupScreen : Screen() {
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(contentPadding)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
|
||||
item {
|
||||
WarningBanner(MR.strings.restore_miui_warning)
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
Button(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
.fillMaxWidth(),
|
||||
onClick = {
|
||||
LazyColumnWithAction(
|
||||
contentPadding = contentPadding,
|
||||
actionLabel = stringResource(MR.strings.action_create),
|
||||
actionEnabled = state.options.anyEnabled(),
|
||||
onClickAction = {
|
||||
if (!BackupCreateJob.isManualJobRunning(context)) {
|
||||
try {
|
||||
chooseBackupDir.launch(BackupCreator.getFilename())
|
||||
@ -128,15 +81,45 @@ class CreateBackupScreen : Screen() {
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(MR.strings.action_create),
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
|
||||
item {
|
||||
WarningBanner(MR.strings.restore_miui_warning)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
SectionCard(MR.strings.label_library) {
|
||||
Options(BackupOptions.libraryOptions, state, model)
|
||||
}
|
||||
}
|
||||
|
||||
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()) {
|
||||
|
||||
|
@ -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()
|
||||
LazyColumnWithAction(
|
||||
contentPadding = contentPadding,
|
||||
actionLabel = stringResource(MR.strings.action_restore),
|
||||
actionEnabled = state.canRestore && state.options.anyEnabled(),
|
||||
onClickAction = {
|
||||
model.startRestore()
|
||||
navigator.pop()
|
||||
},
|
||||
) {
|
||||
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))
|
||||
}
|
||||
},
|
||||
) {
|
||||
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() {
|
||||
}
|
||||
}
|
||||
|
||||
if (state.canRestore) {
|
||||
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)
|
||||
}
|
||||
SectionCard {
|
||||
RestoreOptions.options.forEach { option ->
|
||||
LabeledCheckbox(
|
||||
label = stringResource(option.label),
|
||||
checked = option.getter(state.options),
|
||||
onCheckedChange = {
|
||||
model.toggle(option.setter, it)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.error != null) {
|
||||
errorMessageItem(state.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.errorMessageItem(
|
||||
error: Any?,
|
||||
) {
|
||||
Text(stringResource(MR.strings.pref_restore_backup))
|
||||
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 = "- ",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: show validation errors inline
|
||||
// TODO: show options for what to restore
|
||||
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(),
|
||||
)
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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(),
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 -> {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 }
|
||||
|
@ -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(
|
||||
|
@ -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 {
|
||||
|
@ -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 },
|
||||
)
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ class PreferenceBackupCreator(
|
||||
.withPrivatePreferences(includePrivatePreferences),
|
||||
)
|
||||
}
|
||||
.filter { it.prefs.isNotEmpty() }
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
|
@ -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()
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
@ -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 ?: "",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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?
|
||||
|
@ -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
|
||||
|
@ -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() {
|
||||
|
@ -48,7 +48,7 @@ fun extensionsTab(
|
||||
},
|
||||
onClickItemCancel = extensionsScreenModel::cancelInstallUpdateExtension,
|
||||
onClickUpdateAll = extensionsScreenModel::updateAllExtensions,
|
||||
onClickItemWebView = { extension ->
|
||||
onOpenWebView = { extension ->
|
||||
extension.sources.getOrNull(0)?.let {
|
||||
navigator.push(
|
||||
WebViewScreen(
|
||||
|
@ -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 = {
|
||||
|
@ -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) },
|
||||
)
|
||||
|
@ -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 =
|
||||
|
@ -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),
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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())
|
||||
}
|
@ -46,4 +46,6 @@ dependencies {
|
||||
|
||||
// JavaScript engine
|
||||
implementation(libs.bundles.js.engine)
|
||||
|
||||
testImplementation(libs.bundles.test)
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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()
|
||||
|
@ -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"]
|
||||
|
@ -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>
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
) {
|
||||
if (titleRes != null) {
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = MaterialTheme.padding.extraLarge),
|
||||
text = stringResource(titleRes),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
}
|
||||
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
@ -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,
|
||||
)
|
||||
|
@ -19,7 +19,7 @@ class Padding {
|
||||
|
||||
val small = 8.dp
|
||||
|
||||
val tiny = 4.dp
|
||||
val extraSmall = 4.dp
|
||||
}
|
||||
|
||||
val MaterialTheme.padding: Padding
|
||||
|
@ -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,
|
||||
),
|
||||
) {
|
||||
|
Loading…
Reference in New Issue
Block a user