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)
|
implementation(libs.logcat)
|
||||||
|
|
||||||
// Crash reports/analytics
|
// Crash reports/analytics
|
||||||
implementation(libs.acra.http)
|
implementation(libs.bundles.acra)
|
||||||
"standardImplementation"(libs.firebase.analytics)
|
"standardImplementation"(libs.firebase.analytics)
|
||||||
|
|
||||||
// Shizuku
|
// Shizuku
|
||||||
|
@ -22,7 +22,6 @@ import tachiyomi.domain.chapter.service.ChapterRecognition
|
|||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.source.local.isLocal
|
import tachiyomi.source.local.isLocal
|
||||||
import java.lang.Long.max
|
import java.lang.Long.max
|
||||||
import java.time.Instant
|
|
||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
import java.util.TreeSet
|
import java.util.TreeSet
|
||||||
|
|
||||||
@ -57,6 +56,7 @@ class SyncChaptersWithSource(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val now = ZonedDateTime.now()
|
val now = ZonedDateTime.now()
|
||||||
|
val nowMillis = now.toInstant().toEpochMilli()
|
||||||
|
|
||||||
val sourceChapters = rawSourceChapters
|
val sourceChapters = rawSourceChapters
|
||||||
.distinctBy { it.url }
|
.distinctBy { it.url }
|
||||||
@ -67,36 +67,27 @@ class SyncChaptersWithSource(
|
|||||||
.copy(mangaId = manga.id, sourceOrder = i.toLong())
|
.copy(mangaId = manga.id, sourceOrder = i.toLong())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chapters from db.
|
|
||||||
val dbChapters = getChaptersByMangaId.await(manga.id)
|
val dbChapters = getChaptersByMangaId.await(manga.id)
|
||||||
|
|
||||||
// Chapters from the source not in db.
|
val newChapters = mutableListOf<Chapter>()
|
||||||
val toAdd = mutableListOf<Chapter>()
|
val updatedChapters = mutableListOf<Chapter>()
|
||||||
|
val removedChapters = dbChapters.filterNot { dbChapter ->
|
||||||
// Chapters whose metadata have changed.
|
|
||||||
val toChange = mutableListOf<Chapter>()
|
|
||||||
|
|
||||||
// Chapters from the db not in source.
|
|
||||||
val toDelete = dbChapters.filterNot { dbChapter ->
|
|
||||||
sourceChapters.any { sourceChapter ->
|
sourceChapters.any { sourceChapter ->
|
||||||
dbChapter.url == sourceChapter.url
|
dbChapter.url == sourceChapter.url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val rightNow = Instant.now().toEpochMilli()
|
|
||||||
|
|
||||||
// Used to not set upload date of older chapters
|
// Used to not set upload date of older chapters
|
||||||
// to a higher value than newer chapters
|
// to a higher value than newer chapters
|
||||||
var maxSeenUploadDate = 0L
|
var maxSeenUploadDate = 0L
|
||||||
|
|
||||||
val sManga = manga.toSManga()
|
|
||||||
for (sourceChapter in sourceChapters) {
|
for (sourceChapter in sourceChapters) {
|
||||||
var chapter = sourceChapter
|
var chapter = sourceChapter
|
||||||
|
|
||||||
// Update metadata from source if necessary.
|
// Update metadata from source if necessary.
|
||||||
if (source is HttpSource) {
|
if (source is HttpSource) {
|
||||||
val sChapter = chapter.toSChapter()
|
val sChapter = chapter.toSChapter()
|
||||||
source.prepareNewChapter(sChapter, sManga)
|
source.prepareNewChapter(sChapter, manga.toSManga())
|
||||||
chapter = chapter.copyFromSChapter(sChapter)
|
chapter = chapter.copyFromSChapter(sChapter)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,13 +99,13 @@ class SyncChaptersWithSource(
|
|||||||
|
|
||||||
if (dbChapter == null) {
|
if (dbChapter == null) {
|
||||||
val toAddChapter = if (chapter.dateUpload == 0L) {
|
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)
|
chapter.copy(dateUpload = altDateUpload)
|
||||||
} else {
|
} else {
|
||||||
maxSeenUploadDate = max(maxSeenUploadDate, sourceChapter.dateUpload)
|
maxSeenUploadDate = max(maxSeenUploadDate, sourceChapter.dateUpload)
|
||||||
chapter
|
chapter
|
||||||
}
|
}
|
||||||
toAdd.add(toAddChapter)
|
newChapters.add(toAddChapter)
|
||||||
} else {
|
} else {
|
||||||
if (shouldUpdateDbChapter.await(dbChapter, chapter)) {
|
if (shouldUpdateDbChapter.await(dbChapter, chapter)) {
|
||||||
val shouldRenameChapter = downloadProvider.isChapterDirNameChanged(dbChapter, chapter) &&
|
val shouldRenameChapter = downloadProvider.isChapterDirNameChanged(dbChapter, chapter) &&
|
||||||
@ -134,13 +125,13 @@ class SyncChaptersWithSource(
|
|||||||
if (chapter.dateUpload != 0L) {
|
if (chapter.dateUpload != 0L) {
|
||||||
toChangeChapter = toChangeChapter.copy(dateUpload = chapter.dateUpload)
|
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.
|
// Return if there's nothing to add, delete, or update to avoid unnecessary db transactions.
|
||||||
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
|
if (newChapters.isEmpty() && removedChapters.isEmpty() && updatedChapters.isEmpty()) {
|
||||||
if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchWindow.first) {
|
if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchWindow.first) {
|
||||||
updateManga.awaitUpdateFetchInterval(
|
updateManga.awaitUpdateFetchInterval(
|
||||||
manga,
|
manga,
|
||||||
@ -157,20 +148,20 @@ class SyncChaptersWithSource(
|
|||||||
val deletedReadChapterNumbers = TreeSet<Double>()
|
val deletedReadChapterNumbers = TreeSet<Double>()
|
||||||
val deletedBookmarkedChapterNumbers = TreeSet<Double>()
|
val deletedBookmarkedChapterNumbers = TreeSet<Double>()
|
||||||
|
|
||||||
toDelete.forEach { chapter ->
|
removedChapters.forEach { chapter ->
|
||||||
if (chapter.read) deletedReadChapterNumbers.add(chapter.chapterNumber)
|
if (chapter.read) deletedReadChapterNumbers.add(chapter.chapterNumber)
|
||||||
if (chapter.bookmark) deletedBookmarkedChapterNumbers.add(chapter.chapterNumber)
|
if (chapter.bookmark) deletedBookmarkedChapterNumbers.add(chapter.chapterNumber)
|
||||||
deletedChapterNumbers.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 }
|
.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
|
// 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.
|
// Sources MUST return the chapters from most to less recent, which is common.
|
||||||
var itemCount = toAdd.size
|
var itemCount = newChapters.size
|
||||||
var updatedToAdd = toAdd.map { toAddItem ->
|
var updatedToAdd = newChapters.map { toAddItem ->
|
||||||
var chapter = toAddItem.copy(dateFetch = rightNow + itemCount--)
|
var chapter = toAddItem.copy(dateFetch = nowMillis + itemCount--)
|
||||||
|
|
||||||
if (chapter.isRecognizedNumber.not() || chapter.chapterNumber !in deletedChapterNumbers) return@map chapter
|
if (chapter.isRecognizedNumber.not() || chapter.chapterNumber !in deletedChapterNumbers) return@map chapter
|
||||||
|
|
||||||
@ -189,8 +180,8 @@ class SyncChaptersWithSource(
|
|||||||
chapter
|
chapter
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toDelete.isNotEmpty()) {
|
if (removedChapters.isNotEmpty()) {
|
||||||
val toDeleteIds = toDelete.map { it.id }
|
val toDeleteIds = removedChapters.map { it.id }
|
||||||
chapterRepository.removeChaptersWithIds(toDeleteIds)
|
chapterRepository.removeChaptersWithIds(toDeleteIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,8 +189,8 @@ class SyncChaptersWithSource(
|
|||||||
updatedToAdd = chapterRepository.addAll(updatedToAdd)
|
updatedToAdd = chapterRepository.addAll(updatedToAdd)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toChange.isNotEmpty()) {
|
if (updatedChapters.isNotEmpty()) {
|
||||||
val chapterUpdates = toChange.map { it.toChapterUpdate() }
|
val chapterUpdates = updatedChapters.map { it.toChapterUpdate() }
|
||||||
updateChapter.awaitAll(chapterUpdates)
|
updateChapter.awaitAll(chapterUpdates)
|
||||||
}
|
}
|
||||||
updateManga.awaitUpdateFetchInterval(manga, now, fetchWindow)
|
updateManga.awaitUpdateFetchInterval(manga, now, fetchWindow)
|
||||||
|
@ -295,7 +295,7 @@ private fun DetailsHeader(
|
|||||||
top = MaterialTheme.padding.small,
|
top = MaterialTheme.padding.small,
|
||||||
bottom = MaterialTheme.padding.medium,
|
bottom = MaterialTheme.padding.medium,
|
||||||
),
|
),
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
|
||||||
) {
|
) {
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
|
@ -71,7 +71,7 @@ fun ExtensionScreen(
|
|||||||
searchQuery: String?,
|
searchQuery: String?,
|
||||||
onLongClickItem: (Extension) -> Unit,
|
onLongClickItem: (Extension) -> Unit,
|
||||||
onClickItemCancel: (Extension) -> Unit,
|
onClickItemCancel: (Extension) -> Unit,
|
||||||
onClickItemWebView: (Extension.Available) -> Unit,
|
onOpenWebView: (Extension.Available) -> Unit,
|
||||||
onInstallExtension: (Extension.Available) -> Unit,
|
onInstallExtension: (Extension.Available) -> Unit,
|
||||||
onUninstallExtension: (Extension) -> Unit,
|
onUninstallExtension: (Extension) -> Unit,
|
||||||
onUpdateExtension: (Extension.Installed) -> Unit,
|
onUpdateExtension: (Extension.Installed) -> Unit,
|
||||||
@ -104,7 +104,7 @@ fun ExtensionScreen(
|
|||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
onLongClickItem = onLongClickItem,
|
onLongClickItem = onLongClickItem,
|
||||||
onClickItemCancel = onClickItemCancel,
|
onClickItemCancel = onClickItemCancel,
|
||||||
onClickItemWebView = onClickItemWebView,
|
onOpenWebView = onOpenWebView,
|
||||||
onInstallExtension = onInstallExtension,
|
onInstallExtension = onInstallExtension,
|
||||||
onUninstallExtension = onUninstallExtension,
|
onUninstallExtension = onUninstallExtension,
|
||||||
onUpdateExtension = onUpdateExtension,
|
onUpdateExtension = onUpdateExtension,
|
||||||
@ -122,8 +122,8 @@ private fun ExtensionContent(
|
|||||||
state: ExtensionsScreenModel.State,
|
state: ExtensionsScreenModel.State,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
onLongClickItem: (Extension) -> Unit,
|
onLongClickItem: (Extension) -> Unit,
|
||||||
onClickItemWebView: (Extension.Available) -> Unit,
|
|
||||||
onClickItemCancel: (Extension) -> Unit,
|
onClickItemCancel: (Extension) -> Unit,
|
||||||
|
onOpenWebView: (Extension.Available) -> Unit,
|
||||||
onInstallExtension: (Extension.Available) -> Unit,
|
onInstallExtension: (Extension.Available) -> Unit,
|
||||||
onUninstallExtension: (Extension) -> Unit,
|
onUninstallExtension: (Extension) -> Unit,
|
||||||
onUpdateExtension: (Extension.Installed) -> Unit,
|
onUpdateExtension: (Extension.Installed) -> Unit,
|
||||||
@ -202,7 +202,13 @@ private fun ExtensionContent(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLongClickItem = onLongClickItem,
|
onLongClickItem = onLongClickItem,
|
||||||
onClickItemWebView = onClickItemWebView,
|
onClickItemSecondaryAction = {
|
||||||
|
when (it) {
|
||||||
|
is Extension.Available -> onOpenWebView(it)
|
||||||
|
is Extension.Installed -> onOpenExtension(it)
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
},
|
||||||
onClickItemCancel = onClickItemCancel,
|
onClickItemCancel = onClickItemCancel,
|
||||||
onClickItemAction = {
|
onClickItemAction = {
|
||||||
when (it) {
|
when (it) {
|
||||||
@ -243,9 +249,9 @@ private fun ExtensionItem(
|
|||||||
item: ExtensionUiModel.Item,
|
item: ExtensionUiModel.Item,
|
||||||
onClickItem: (Extension) -> Unit,
|
onClickItem: (Extension) -> Unit,
|
||||||
onLongClickItem: (Extension) -> Unit,
|
onLongClickItem: (Extension) -> Unit,
|
||||||
onClickItemWebView: (Extension.Available) -> Unit,
|
|
||||||
onClickItemCancel: (Extension) -> Unit,
|
onClickItemCancel: (Extension) -> Unit,
|
||||||
onClickItemAction: (Extension) -> Unit,
|
onClickItemAction: (Extension) -> Unit,
|
||||||
|
onClickItemSecondaryAction: (Extension) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val (extension, installStep) = item
|
val (extension, installStep) = item
|
||||||
@ -287,9 +293,9 @@ private fun ExtensionItem(
|
|||||||
ExtensionItemActions(
|
ExtensionItemActions(
|
||||||
extension = extension,
|
extension = extension,
|
||||||
installStep = installStep,
|
installStep = installStep,
|
||||||
onClickItemWebView = onClickItemWebView,
|
|
||||||
onClickItemCancel = onClickItemCancel,
|
onClickItemCancel = onClickItemCancel,
|
||||||
onClickItemAction = onClickItemAction,
|
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
|
// Won't look good but it's not like we can ellipsize overflowing content
|
||||||
FlowRow(
|
FlowRow(
|
||||||
modifier = Modifier.secondaryItemAlpha(),
|
modifier = Modifier.secondaryItemAlpha(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
) {
|
) {
|
||||||
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
|
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
|
||||||
if (extension is Extension.Installed && extension.lang.isNotEmpty()) {
|
if (extension is Extension.Installed && extension.lang.isNotEmpty()) {
|
||||||
@ -371,15 +377,15 @@ private fun ExtensionItemActions(
|
|||||||
extension: Extension,
|
extension: Extension,
|
||||||
installStep: InstallStep,
|
installStep: InstallStep,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onClickItemWebView: (Extension.Available) -> Unit = {},
|
|
||||||
onClickItemCancel: (Extension) -> Unit = {},
|
onClickItemCancel: (Extension) -> Unit = {},
|
||||||
onClickItemAction: (Extension) -> Unit = {},
|
onClickItemAction: (Extension) -> Unit = {},
|
||||||
|
onClickItemSecondaryAction: (Extension) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val isIdle = installStep.isCompleted()
|
val isIdle = installStep.isCompleted()
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
!isIdle -> {
|
!isIdle -> {
|
||||||
@ -401,7 +407,7 @@ private fun ExtensionItemActions(
|
|||||||
installStep == InstallStep.Idle -> {
|
installStep == InstallStep.Idle -> {
|
||||||
when (extension) {
|
when (extension) {
|
||||||
is Extension.Installed -> {
|
is Extension.Installed -> {
|
||||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
IconButton(onClick = { onClickItemSecondaryAction(extension) }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Settings,
|
imageVector = Icons.Outlined.Settings,
|
||||||
contentDescription = stringResource(MR.strings.action_settings),
|
contentDescription = stringResource(MR.strings.action_settings),
|
||||||
@ -428,7 +434,7 @@ private fun ExtensionItemActions(
|
|||||||
is Extension.Available -> {
|
is Extension.Available -> {
|
||||||
if (extension.sources.isNotEmpty()) {
|
if (extension.sources.isNotEmpty()) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { onClickItemWebView(extension) },
|
onClick = { onClickItemSecondaryAction(extension) },
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Public,
|
imageVector = Icons.Outlined.Public,
|
||||||
|
@ -37,7 +37,7 @@ fun GlobalSearchCardRow(
|
|||||||
|
|
||||||
LazyRow(
|
LazyRow(
|
||||||
contentPadding = PaddingValues(MaterialTheme.padding.small),
|
contentPadding = PaddingValues(MaterialTheme.padding.small),
|
||||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
) {
|
) {
|
||||||
items(titles) {
|
items(titles) {
|
||||||
val title by getManga(it)
|
val title by getManga(it)
|
||||||
|
@ -39,7 +39,7 @@ fun GlobalSearchResultItem(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(
|
.padding(
|
||||||
start = MaterialTheme.padding.medium,
|
start = MaterialTheme.padding.medium,
|
||||||
end = MaterialTheme.padding.tiny,
|
end = MaterialTheme.padding.extraSmall,
|
||||||
)
|
)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable(onClick = onClick),
|
.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.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import eu.kanade.domain.ui.UiPreferences
|
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.components.AppBarActions
|
import eu.kanade.presentation.components.AppBarActions
|
||||||
import eu.kanade.presentation.components.AppBarTitle
|
import eu.kanade.presentation.components.AppBarTitle
|
||||||
import eu.kanade.presentation.components.RelativeDateHeader
|
|
||||||
import eu.kanade.presentation.components.SearchToolbar
|
import eu.kanade.presentation.components.SearchToolbar
|
||||||
|
import eu.kanade.presentation.components.relativeDateText
|
||||||
import eu.kanade.presentation.history.components.HistoryItem
|
import eu.kanade.presentation.history.components.HistoryItem
|
||||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||||
import eu.kanade.tachiyomi.ui.history.HistoryScreenModel
|
import eu.kanade.tachiyomi.ui.history.HistoryScreenModel
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import tachiyomi.core.preference.InMemoryPreferenceStore
|
|
||||||
import tachiyomi.domain.history.model.HistoryWithRelations
|
import tachiyomi.domain.history.model.HistoryWithRelations
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||||
|
import tachiyomi.presentation.core.components.ListGroupHeader
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -42,7 +38,6 @@ fun HistoryScreen(
|
|||||||
onClickCover: (mangaId: Long) -> Unit,
|
onClickCover: (mangaId: Long) -> Unit,
|
||||||
onClickResume: (mangaId: Long, chapterId: Long) -> Unit,
|
onClickResume: (mangaId: Long, chapterId: Long) -> Unit,
|
||||||
onDialogChange: (HistoryScreenModel.Dialog?) -> Unit,
|
onDialogChange: (HistoryScreenModel.Dialog?) -> Unit,
|
||||||
preferences: UiPreferences = Injekt.get(),
|
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = { scrollBehavior ->
|
topBar = { scrollBehavior ->
|
||||||
@ -88,7 +83,6 @@ fun HistoryScreen(
|
|||||||
onClickCover = { history -> onClickCover(history.mangaId) },
|
onClickCover = { history -> onClickCover(history.mangaId) },
|
||||||
onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) },
|
onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) },
|
||||||
onClickDelete = { item -> onDialogChange(HistoryScreenModel.Dialog.Delete(item)) },
|
onClickDelete = { item -> onDialogChange(HistoryScreenModel.Dialog.Delete(item)) },
|
||||||
preferences = preferences,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -102,11 +96,7 @@ private fun HistoryScreenContent(
|
|||||||
onClickCover: (HistoryWithRelations) -> Unit,
|
onClickCover: (HistoryWithRelations) -> Unit,
|
||||||
onClickResume: (HistoryWithRelations) -> Unit,
|
onClickResume: (HistoryWithRelations) -> Unit,
|
||||||
onClickDelete: (HistoryWithRelations) -> Unit,
|
onClickDelete: (HistoryWithRelations) -> Unit,
|
||||||
preferences: UiPreferences,
|
|
||||||
) {
|
) {
|
||||||
val relativeTime = remember { preferences.relativeTime().get() }
|
|
||||||
val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
|
|
||||||
|
|
||||||
FastScrollLazyColumn(
|
FastScrollLazyColumn(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
) {
|
) {
|
||||||
@ -122,11 +112,9 @@ private fun HistoryScreenContent(
|
|||||||
) { item ->
|
) { item ->
|
||||||
when (item) {
|
when (item) {
|
||||||
is HistoryUiModel.Header -> {
|
is HistoryUiModel.Header -> {
|
||||||
RelativeDateHeader(
|
ListGroupHeader(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItemPlacement(),
|
||||||
date = item.date,
|
text = relativeDateText(item.date),
|
||||||
relativeTime = relativeTime,
|
|
||||||
dateFormat = dateFormat,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is HistoryUiModel.Item -> {
|
is HistoryUiModel.Item -> {
|
||||||
@ -163,17 +151,6 @@ internal fun HistoryScreenPreviews(
|
|||||||
onClickCover = {},
|
onClickCover = {},
|
||||||
onClickResume = { _, _ -> run {} },
|
onClickResume = { _, _ -> run {} },
|
||||||
onDialogChange = {},
|
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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@ -11,10 +12,10 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.LabeledCheckbox
|
import tachiyomi.presentation.core.components.LabeledCheckbox
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -30,7 +31,7 @@ fun HistoryDeleteDialog(
|
|||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(MR.strings.dialog_with_checkbox_remove_description))
|
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.FlowRow
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -28,7 +29,7 @@ fun DuplicateMangaDialog(
|
|||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
FlowRow(
|
FlowRow(
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
) {
|
) {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
|
@ -47,6 +47,7 @@ import androidx.compose.ui.platform.LocalLayoutDirection
|
|||||||
import androidx.compose.ui.util.fastAll
|
import androidx.compose.ui.util.fastAll
|
||||||
import androidx.compose.ui.util.fastAny
|
import androidx.compose.ui.util.fastAny
|
||||||
import androidx.compose.ui.util.fastMap
|
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.ChapterDownloadAction
|
||||||
import eu.kanade.presentation.manga.components.ChapterHeader
|
import eu.kanade.presentation.manga.components.ChapterHeader
|
||||||
import eu.kanade.presentation.manga.components.ExpandableMangaDescription
|
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.source.getNameForMangaInfo
|
||||||
import eu.kanade.tachiyomi.ui.manga.ChapterList
|
import eu.kanade.tachiyomi.ui.manga.ChapterList
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaScreenModel
|
import eu.kanade.tachiyomi.ui.manga.MangaScreenModel
|
||||||
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
|
||||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
import tachiyomi.domain.chapter.service.missingChaptersCount
|
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.isScrolledToEnd
|
||||||
import tachiyomi.presentation.core.util.isScrollingUp
|
import tachiyomi.presentation.core.util.isScrollingUp
|
||||||
import tachiyomi.source.local.isLocal
|
import tachiyomi.source.local.isLocal
|
||||||
import java.text.DateFormat
|
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MangaScreen(
|
fun MangaScreen(
|
||||||
state: MangaScreenModel.State.Success,
|
state: MangaScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
fetchInterval: Int?,
|
fetchInterval: Int?,
|
||||||
dateRelativeTime: Boolean,
|
|
||||||
dateFormat: DateFormat,
|
|
||||||
isTabletUi: Boolean,
|
isTabletUi: Boolean,
|
||||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
@ -142,8 +138,6 @@ fun MangaScreen(
|
|||||||
MangaScreenSmallImpl(
|
MangaScreenSmallImpl(
|
||||||
state = state,
|
state = state,
|
||||||
snackbarHostState = snackbarHostState,
|
snackbarHostState = snackbarHostState,
|
||||||
dateRelativeTime = dateRelativeTime,
|
|
||||||
dateFormat = dateFormat,
|
|
||||||
fetchInterval = fetchInterval,
|
fetchInterval = fetchInterval,
|
||||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||||
@ -179,10 +173,8 @@ fun MangaScreen(
|
|||||||
MangaScreenLargeImpl(
|
MangaScreenLargeImpl(
|
||||||
state = state,
|
state = state,
|
||||||
snackbarHostState = snackbarHostState,
|
snackbarHostState = snackbarHostState,
|
||||||
dateRelativeTime = dateRelativeTime,
|
|
||||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||||
dateFormat = dateFormat,
|
|
||||||
fetchInterval = fetchInterval,
|
fetchInterval = fetchInterval,
|
||||||
onBackClicked = onBackClicked,
|
onBackClicked = onBackClicked,
|
||||||
onChapterClicked = onChapterClicked,
|
onChapterClicked = onChapterClicked,
|
||||||
@ -219,8 +211,6 @@ fun MangaScreen(
|
|||||||
private fun MangaScreenSmallImpl(
|
private fun MangaScreenSmallImpl(
|
||||||
state: MangaScreenModel.State.Success,
|
state: MangaScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
dateRelativeTime: Boolean,
|
|
||||||
dateFormat: DateFormat,
|
|
||||||
fetchInterval: Int?,
|
fetchInterval: Int?,
|
||||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
@ -455,8 +445,6 @@ private fun MangaScreenSmallImpl(
|
|||||||
manga = state.manga,
|
manga = state.manga,
|
||||||
chapters = listItem,
|
chapters = listItem,
|
||||||
isAnyChapterSelected = chapters.fastAny { it.selected },
|
isAnyChapterSelected = chapters.fastAny { it.selected },
|
||||||
dateRelativeTime = dateRelativeTime,
|
|
||||||
dateFormat = dateFormat,
|
|
||||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||||
onChapterClicked = onChapterClicked,
|
onChapterClicked = onChapterClicked,
|
||||||
@ -474,8 +462,6 @@ private fun MangaScreenSmallImpl(
|
|||||||
fun MangaScreenLargeImpl(
|
fun MangaScreenLargeImpl(
|
||||||
state: MangaScreenModel.State.Success,
|
state: MangaScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
dateRelativeTime: Boolean,
|
|
||||||
dateFormat: DateFormat,
|
|
||||||
fetchInterval: Int?,
|
fetchInterval: Int?,
|
||||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
@ -705,8 +691,6 @@ fun MangaScreenLargeImpl(
|
|||||||
manga = state.manga,
|
manga = state.manga,
|
||||||
chapters = listItem,
|
chapters = listItem,
|
||||||
isAnyChapterSelected = chapters.fastAny { it.selected },
|
isAnyChapterSelected = chapters.fastAny { it.selected },
|
||||||
dateRelativeTime = dateRelativeTime,
|
|
||||||
dateFormat = dateFormat,
|
|
||||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||||
onChapterClicked = onChapterClicked,
|
onChapterClicked = onChapterClicked,
|
||||||
@ -768,8 +752,6 @@ private fun LazyListScope.sharedChapterItems(
|
|||||||
manga: Manga,
|
manga: Manga,
|
||||||
chapters: List<ChapterList>,
|
chapters: List<ChapterList>,
|
||||||
isAnyChapterSelected: Boolean,
|
isAnyChapterSelected: Boolean,
|
||||||
dateRelativeTime: Boolean,
|
|
||||||
dateFormat: DateFormat,
|
|
||||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
onChapterClicked: (Chapter) -> Unit,
|
onChapterClicked: (Chapter) -> Unit,
|
||||||
@ -788,7 +770,6 @@ private fun LazyListScope.sharedChapterItems(
|
|||||||
contentType = { MangaScreenItem.CHAPTER },
|
contentType = { MangaScreenItem.CHAPTER },
|
||||||
) { item ->
|
) { item ->
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
when (item) {
|
when (item) {
|
||||||
is ChapterList.MissingCount -> {
|
is ChapterList.MissingCount -> {
|
||||||
@ -804,15 +785,7 @@ private fun LazyListScope.sharedChapterItems(
|
|||||||
} else {
|
} else {
|
||||||
item.chapter.name
|
item.chapter.name
|
||||||
},
|
},
|
||||||
date = item.chapter.dateUpload
|
date = relativeDateText(item.chapter.dateUpload),
|
||||||
.takeIf { it > 0L }
|
|
||||||
?.let {
|
|
||||||
Date(it).toRelativeString(
|
|
||||||
context,
|
|
||||||
dateRelativeTime,
|
|
||||||
dateFormat,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
readProgress = item.chapter.lastPageRead
|
readProgress = item.chapter.lastPageRead
|
||||||
.takeIf { !item.chapter.read && it > 0L }
|
.takeIf { !item.chapter.read && it > 0L }
|
||||||
?.let {
|
?.let {
|
||||||
|
@ -19,8 +19,8 @@ import tachiyomi.presentation.core.components.material.padding
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BaseMangaListItem(
|
fun BaseMangaListItem(
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
onClickItem: () -> Unit = {},
|
onClickItem: () -> Unit = {},
|
||||||
onClickCover: () -> Unit = onClickItem,
|
onClickCover: () -> Unit = onClickItem,
|
||||||
cover: @Composable RowScope.() -> Unit = { defaultCover(manga, onClickCover) },
|
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 androidx.compose.ui.unit.dp
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha
|
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.pluralStringResource
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
|
||||||
@ -22,16 +23,17 @@ fun ChapterHeader(
|
|||||||
chapterCount: Int?,
|
chapterCount: Int?,
|
||||||
missingChapterCount: Int,
|
missingChapterCount: Int,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable(
|
.clickable(
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
)
|
)
|
||||||
.padding(horizontal = 16.dp, vertical = 4.dp),
|
.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = if (chapterCount == null) {
|
text = if (chapterCount == null) {
|
||||||
|
@ -2,13 +2,24 @@ package eu.kanade.presentation.manga.components
|
|||||||
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DotSeparatorText() {
|
fun DotSeparatorText(
|
||||||
Text(text = " • ")
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = " • ",
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DotSeparatorNoSpaceText() {
|
fun DotSeparatorNoSpaceText(
|
||||||
Text(text = "•")
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "•",
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -222,12 +222,12 @@ private fun RowScope.Button(
|
|||||||
@Composable
|
@Composable
|
||||||
fun LibraryBottomActionMenu(
|
fun LibraryBottomActionMenu(
|
||||||
visible: Boolean,
|
visible: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
onChangeCategoryClicked: () -> Unit,
|
onChangeCategoryClicked: () -> Unit,
|
||||||
onMarkAsReadClicked: () -> Unit,
|
onMarkAsReadClicked: () -> Unit,
|
||||||
onMarkAsUnreadClicked: () -> Unit,
|
onMarkAsUnreadClicked: () -> Unit,
|
||||||
onDownloadClicked: ((DownloadAction) -> Unit)?,
|
onDownloadClicked: ((DownloadAction) -> Unit)?,
|
||||||
onDeleteClicked: () -> Unit,
|
onDeleteClicked: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = visible,
|
visible = visible,
|
||||||
|
@ -22,8 +22,8 @@ enum class MangaCover(val ratio: Float) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
operator fun invoke(
|
operator fun invoke(
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
data: Any?,
|
data: Any?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
contentDescription: String = "",
|
contentDescription: String = "",
|
||||||
shape: Shape = MaterialTheme.shapes.extraSmall,
|
shape: Shape = MaterialTheme.shapes.extraSmall,
|
||||||
onClick: (() -> Unit)? = null,
|
onClick: (() -> Unit)? = null,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package eu.kanade.presentation.manga.components
|
package eu.kanade.presentation.manga.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@ -8,6 +9,7 @@ import androidx.compose.material3.TextButton
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@ -18,7 +20,10 @@ import kotlinx.collections.immutable.toImmutableList
|
|||||||
import tachiyomi.domain.manga.interactor.FetchInterval
|
import tachiyomi.domain.manga.interactor.FetchInterval
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.WheelTextPicker
|
import tachiyomi.presentation.core.components.WheelTextPicker
|
||||||
|
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.temporal.ChronoUnit
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DeleteChaptersDialog(
|
fun DeleteChaptersDialog(
|
||||||
@ -54,15 +59,37 @@ fun DeleteChaptersDialog(
|
|||||||
@Composable
|
@Composable
|
||||||
fun SetIntervalDialog(
|
fun SetIntervalDialog(
|
||||||
interval: Int,
|
interval: Int,
|
||||||
|
nextUpdate: Long,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onValueChanged: (Int) -> Unit,
|
onValueChanged: (Int) -> Unit,
|
||||||
) {
|
) {
|
||||||
var selectedInterval by rememberSaveable { mutableIntStateOf(if (interval < 0) -interval else 0) }
|
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(
|
AlertDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
title = { Text(text = stringResource(MR.strings.manga_modify_calculated_interval_title)) },
|
title = { Text(stringResource(MR.strings.manga_modify_calculated_interval_title)) },
|
||||||
text = {
|
text = {
|
||||||
|
Column {
|
||||||
|
if (nextUpdateDays >= 0) {
|
||||||
|
Text(
|
||||||
|
stringResource(
|
||||||
|
MR.strings.manga_interval_expected_update,
|
||||||
|
pluralStringResource(
|
||||||
|
MR.plurals.day,
|
||||||
|
count = nextUpdateDays.toInt(),
|
||||||
|
nextUpdateDays,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
BoxWithConstraints(
|
BoxWithConstraints(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
@ -84,6 +111,7 @@ fun SetIntervalDialog(
|
|||||||
onSelectionChanged = { selectedInterval = it },
|
onSelectionChanged = { selectedInterval = it },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = onDismissRequest) {
|
TextButton(onClick = onDismissRequest) {
|
||||||
|
@ -9,6 +9,7 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@ -283,7 +284,7 @@ fun ExpandableMangaDescription(
|
|||||||
if (expanded) {
|
if (expanded) {
|
||||||
FlowRow(
|
FlowRow(
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
) {
|
) {
|
||||||
tags.forEach {
|
tags.forEach {
|
||||||
TagsChip(
|
TagsChip(
|
||||||
@ -299,7 +300,7 @@ fun ExpandableMangaDescription(
|
|||||||
} else {
|
} else {
|
||||||
LazyRow(
|
LazyRow(
|
||||||
contentPadding = PaddingValues(horizontal = MaterialTheme.padding.medium),
|
contentPadding = PaddingValues(horizontal = MaterialTheme.padding.medium),
|
||||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
) {
|
) {
|
||||||
items(items = tags) {
|
items(items = tags) {
|
||||||
TagsChip(
|
TagsChip(
|
||||||
@ -402,7 +403,7 @@ private fun MangaAndSourceTitlesSmall(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MangaContentInfo(
|
private fun ColumnScope.MangaContentInfo(
|
||||||
title: String,
|
title: String,
|
||||||
doSearch: (query: String, global: Boolean) -> Unit,
|
doSearch: (query: String, global: Boolean) -> Unit,
|
||||||
author: String?,
|
author: String?,
|
||||||
@ -434,7 +435,7 @@ private fun MangaContentInfo(
|
|||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.secondaryItemAlpha(),
|
modifier = Modifier.secondaryItemAlpha(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
@ -465,7 +466,7 @@ private fun MangaContentInfo(
|
|||||||
if (!artist.isNullOrBlank() && author != artist) {
|
if (!artist.isNullOrBlank() && author != artist) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.secondaryItemAlpha(),
|
modifier = Modifier.secondaryItemAlpha(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
|
@ -59,7 +59,7 @@ fun NewUpdateScreen(
|
|||||||
modifier = Modifier.padding(top = MaterialTheme.padding.small),
|
modifier = Modifier.padding(top = MaterialTheme.padding.small),
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(MR.strings.update_check_open))
|
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)
|
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 androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
|
||||||
internal class GuidesStep(
|
internal class GuidesStep(
|
||||||
@ -29,7 +30,7 @@ internal class GuidesStep(
|
|||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(16.dp),
|
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)))
|
Text(stringResource(MR.strings.onboarding_guides_new_user, stringResource(MR.strings.app_name)))
|
||||||
Button(
|
Button(
|
||||||
|
@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Arrangement
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@ -20,6 +21,7 @@ import kotlinx.coroutines.flow.collectLatest
|
|||||||
import tachiyomi.domain.storage.service.StoragePreferences
|
import tachiyomi.domain.storage.service.StoragePreferences
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.material.Button
|
import tachiyomi.presentation.core.components.material.Button
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
@ -40,7 +42,7 @@ internal class StorageStep : OnboardingStep {
|
|||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(16.dp),
|
modifier = Modifier.padding(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
stringResource(
|
stringResource(
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package eu.kanade.presentation.more.settings.screen
|
package eu.kanade.presentation.more.settings.screen
|
||||||
|
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.activity.compose.ManagedActivityResultLauncher
|
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.more.settings.widget.PrefsHorizontalPadding
|
||||||
import eu.kanade.presentation.util.relativeTimeSpanString
|
import eu.kanade.presentation.util.relativeTimeSpanString
|
||||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
|
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.cache.ChapterCache
|
||||||
import eu.kanade.tachiyomi.data.sync.SyncDataJob
|
import eu.kanade.tachiyomi.data.sync.SyncDataJob
|
||||||
import eu.kanade.tachiyomi.data.sync.SyncManager
|
import eu.kanade.tachiyomi.data.sync.SyncManager
|
||||||
import eu.kanade.tachiyomi.data.sync.service.GoogleDriveService
|
import eu.kanade.tachiyomi.data.sync.service.GoogleDriveService
|
||||||
import eu.kanade.tachiyomi.data.sync.service.GoogleDriveSyncService
|
import eu.kanade.tachiyomi.data.sync.service.GoogleDriveSyncService
|
||||||
|
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.persistentMapOf
|
import kotlinx.collections.immutable.persistentMapOf
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.i18n.stringResource
|
import tachiyomi.core.i18n.stringResource
|
||||||
|
import tachiyomi.core.storage.displayablePath
|
||||||
import tachiyomi.core.util.lang.launchNonCancellable
|
import tachiyomi.core.util.lang.launchNonCancellable
|
||||||
import tachiyomi.core.util.lang.withUIContext
|
import tachiyomi.core.util.lang.withUIContext
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
@ -121,7 +125,7 @@ object SettingsDataScreen : SearchableSettings {
|
|||||||
|
|
||||||
return remember(storageDir) {
|
return remember(storageDir) {
|
||||||
val file = UniFile.fromUri(context, storageDir.toUri())
|
val file = UniFile.fromUri(context, storageDir.toUri())
|
||||||
file?.filePath ?: file?.uri?.toString()
|
file?.displayablePath
|
||||||
} ?: stringResource(MR.strings.invalid_location, storageDir)
|
} ?: stringResource(MR.strings.invalid_location, storageDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,6 +156,22 @@ object SettingsDataScreen : SearchableSettings {
|
|||||||
|
|
||||||
val lastAutoBackup by backupPreferences.lastAutoBackupTimestamp().collectAsState()
|
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(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.label_backup),
|
title = stringResource(MR.strings.label_backup),
|
||||||
preferenceItems = persistentListOf(
|
preferenceItems = persistentListOf(
|
||||||
@ -175,7 +195,18 @@ object SettingsDataScreen : SearchableSettings {
|
|||||||
}
|
}
|
||||||
SegmentedButton(
|
SegmentedButton(
|
||||||
checked = false,
|
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),
|
shape = SegmentedButtonDefaults.itemShape(1, 2),
|
||||||
) {
|
) {
|
||||||
Text(stringResource(MR.strings.pref_restore_backup))
|
Text(stringResource(MR.strings.pref_restore_backup))
|
||||||
|
@ -317,7 +317,7 @@ object SettingsTrackingScreen : SearchableSettings {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall)) {
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
onClick = onDismissRequest,
|
onClick = onDismissRequest,
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
package eu.kanade.presentation.more.settings.screen.about
|
package eu.kanade.presentation.more.settings.screen.about
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import com.mikepenz.aboutlibraries.ui.compose.LibrariesContainer
|
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
|
||||||
import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults
|
import com.mikepenz.aboutlibraries.ui.compose.m3.util.htmlReadyLicenseContent
|
||||||
import com.mikepenz.aboutlibraries.ui.compose.util.htmlReadyLicenseContent
|
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.util.Screen
|
import eu.kanade.presentation.util.Screen
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
@ -33,12 +31,6 @@ class OpenSourceLicensesScreen : Screen() {
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
colors = LibraryDefaults.libraryColors(
|
|
||||||
backgroundColor = MaterialTheme.colorScheme.background,
|
|
||||||
contentColor = MaterialTheme.colorScheme.onBackground,
|
|
||||||
badgeBackgroundColor = MaterialTheme.colorScheme.primary,
|
|
||||||
badgeContentColor = MaterialTheme.colorScheme.onPrimary,
|
|
||||||
),
|
|
||||||
onLibraryClick = {
|
onLibraryClick = {
|
||||||
val libraryLicenseScreen = OpenSourceLibraryLicenseScreen(
|
val libraryLicenseScreen = OpenSourceLibraryLicenseScreen(
|
||||||
name = it.library.name,
|
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.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
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.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.FlipToBack
|
import androidx.compose.material.icons.outlined.FlipToBack
|
||||||
import androidx.compose.material.icons.outlined.SelectAll
|
import androidx.compose.material.icons.outlined.SelectAll
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
@ -50,6 +45,7 @@ import tachiyomi.domain.source.interactor.GetSourcesWithNonLibraryManga
|
|||||||
import tachiyomi.domain.source.model.Source
|
import tachiyomi.domain.source.model.Source
|
||||||
import tachiyomi.domain.source.model.SourceWithCount
|
import tachiyomi.domain.source.model.SourceWithCount
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
|
import tachiyomi.presentation.core.components.LazyColumnWithAction
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||||
@ -114,7 +110,7 @@ class ClearDatabaseScreen : Screen() {
|
|||||||
onClick = model::selectAll,
|
onClick = model::selectAll,
|
||||||
),
|
),
|
||||||
AppBar.Action(
|
AppBar.Action(
|
||||||
title = stringResource(MR.strings.action_select_all),
|
title = stringResource(MR.strings.action_select_inverse),
|
||||||
icon = Icons.Outlined.FlipToBack,
|
icon = Icons.Outlined.FlipToBack,
|
||||||
onClick = model::invertSelection,
|
onClick = model::invertSelection,
|
||||||
),
|
),
|
||||||
@ -132,13 +128,11 @@ class ClearDatabaseScreen : Screen() {
|
|||||||
modifier = Modifier.padding(contentPadding),
|
modifier = Modifier.padding(contentPadding),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Column(
|
LazyColumnWithAction(
|
||||||
modifier = Modifier
|
contentPadding = contentPadding,
|
||||||
.padding(contentPadding)
|
actionLabel = stringResource(MR.strings.action_delete),
|
||||||
.fillMaxSize(),
|
actionEnabled = s.selection.isNotEmpty(),
|
||||||
) {
|
onClickAction = model::showConfirmation,
|
||||||
LazyColumn(
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
) {
|
) {
|
||||||
items(s.items) { sourceWithCount ->
|
items(s.items) { sourceWithCount ->
|
||||||
ClearDatabaseItem(
|
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 android.net.Uri
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
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.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
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.StateScreenModel
|
||||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
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.data.backup.create.BackupOptions
|
||||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.LabeledCheckbox
|
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.Scaffold
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
|
||||||
class CreateBackupScreen : Screen() {
|
class CreateBackupScreen : Screen() {
|
||||||
@ -73,50 +65,11 @@ class CreateBackupScreen : Screen() {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
Column(
|
LazyColumnWithAction(
|
||||||
modifier = Modifier
|
contentPadding = contentPadding,
|
||||||
.padding(contentPadding)
|
actionLabel = stringResource(MR.strings.action_create),
|
||||||
.fillMaxSize(),
|
actionEnabled = state.options.anyEnabled(),
|
||||||
) {
|
onClickAction = {
|
||||||
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 = {
|
|
||||||
if (!BackupCreateJob.isManualJobRunning(context)) {
|
if (!BackupCreateJob.isManualJobRunning(context)) {
|
||||||
try {
|
try {
|
||||||
chooseBackupDir.launch(BackupCreator.getFilename())
|
chooseBackupDir.launch(BackupCreator.getFilename())
|
||||||
@ -128,14 +81,44 @@ class CreateBackupScreen : Screen() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Text(
|
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
|
||||||
text = stringResource(MR.strings.action_create),
|
item {
|
||||||
color = MaterialTheme.colorScheme.onPrimary,
|
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()) {
|
private class CreateBackupScreenModel : StateScreenModel<CreateBackupScreenModel.State>(State()) {
|
||||||
|
@ -1,28 +1,25 @@
|
|||||||
package eu.kanade.presentation.more.settings.screen.data
|
package eu.kanade.presentation.more.settings.screen.data
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.compose.foundation.layout.Column
|
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.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
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.StateScreenModel
|
||||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
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.BackupRestoreJob
|
||||||
import eu.kanade.tachiyomi.data.backup.restore.RestoreOptions
|
import eu.kanade.tachiyomi.data.backup.restore.RestoreOptions
|
||||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
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 kotlinx.coroutines.flow.update
|
||||||
import tachiyomi.core.i18n.stringResource
|
|
||||||
import tachiyomi.i18n.MR
|
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.Scaffold
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
|
||||||
class RestoreBackupScreen : Screen() {
|
class RestoreBackupScreen(
|
||||||
|
private val uri: String,
|
||||||
|
) : Screen() {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
val model = rememberScreenModel { RestoreBackupScreenModel() }
|
val model = rememberScreenModel { RestoreBackupScreenModel(context, uri) }
|
||||||
val state by model.state.collectAsState()
|
val state by model.state.collectAsState()
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@ -61,121 +60,14 @@ class RestoreBackupScreen : Screen() {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
if (state.error != null) {
|
LazyColumnWithAction(
|
||||||
val onDismissRequest = model::clearError
|
contentPadding = contentPadding,
|
||||||
when (val err = state.error) {
|
actionLabel = stringResource(MR.strings.action_restore),
|
||||||
is InvalidRestore -> {
|
actionEnabled = state.canRestore && state.options.anyEnabled(),
|
||||||
AlertDialog(
|
onClickAction = {
|
||||||
onDismissRequest = onDismissRequest,
|
model.startRestore()
|
||||||
title = { Text(text = stringResource(MR.strings.invalid_backup_file)) },
|
navigator.pop()
|
||||||
text = { Text(text = listOfNotNull(err.uri, err.message).joinToString("\n\n")) },
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
context.copyToClipboard(err.message, err.message)
|
|
||||||
onDismissRequest()
|
|
||||||
},
|
},
|
||||||
) {
|
|
||||||
Text(text = stringResource(MR.strings.action_copy_to_clipboard))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = onDismissRequest) {
|
|
||||||
Text(text = stringResource(MR.strings.action_ok))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is MissingRestoreComponents -> {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
title = { Text(text = stringResource(MR.strings.pref_restore_backup)) },
|
|
||||||
text = {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
|
||||||
) {
|
|
||||||
val msg = buildString {
|
|
||||||
append(stringResource(MR.strings.backup_restore_content_full))
|
|
||||||
if (err.sources.isNotEmpty()) {
|
|
||||||
append(
|
|
||||||
"\n\n",
|
|
||||||
).append(stringResource(MR.strings.backup_restore_missing_sources))
|
|
||||||
err.sources.joinTo(
|
|
||||||
this,
|
|
||||||
separator = "\n- ",
|
|
||||||
prefix = "\n- ",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (err.trackers.isNotEmpty()) {
|
|
||||||
append(
|
|
||||||
"\n\n",
|
|
||||||
).append(stringResource(MR.strings.backup_restore_missing_trackers))
|
|
||||||
err.trackers.joinTo(
|
|
||||||
this,
|
|
||||||
separator = "\n- ",
|
|
||||||
prefix = "\n- ",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Text(text = msg)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
BackupRestoreJob.start(
|
|
||||||
context = context,
|
|
||||||
uri = err.uri,
|
|
||||||
options = state.options,
|
|
||||||
)
|
|
||||||
onDismissRequest()
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Text(text = stringResource(MR.strings.action_restore))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> onDismissRequest() // Unknown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val chooseBackup = rememberLauncherForActivityResult(
|
|
||||||
object : ActivityResultContracts.GetContent() {
|
|
||||||
override fun createIntent(context: Context, input: String): Intent {
|
|
||||||
val intent = super.createIntent(context, input)
|
|
||||||
return Intent.createChooser(intent, context.stringResource(MR.strings.file_select_backup))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
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()) {
|
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
|
||||||
item {
|
item {
|
||||||
@ -183,49 +75,155 @@ class RestoreBackupScreen : Screen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.canRestore) {
|
||||||
item {
|
item {
|
||||||
Button(
|
SectionCard {
|
||||||
modifier = Modifier
|
RestoreOptions.options.forEach { option ->
|
||||||
.padding(horizontal = MaterialTheme.padding.medium)
|
LabeledCheckbox(
|
||||||
.fillMaxWidth(),
|
label = stringResource(option.label),
|
||||||
onClick = {
|
checked = option.getter(state.options),
|
||||||
if (!BackupRestoreJob.isRunning(context)) {
|
onCheckedChange = {
|
||||||
// no need to catch because it's wrapped with a chooser
|
model.toggle(option.setter, it)
|
||||||
chooseBackup.launch("*/*")
|
|
||||||
} else {
|
|
||||||
context.toast(MR.strings.restore_in_progress)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
) {
|
)
|
||||||
Text(stringResource(MR.strings.pref_restore_backup))
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: show validation errors inline
|
if (state.error != null) {
|
||||||
// TODO: show options for what to restore
|
errorMessageItem(state.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun LazyListScope.errorMessageItem(
|
||||||
|
error: Any?,
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
SectionCard {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||||
|
) {
|
||||||
|
val msg = buildAnnotatedString {
|
||||||
|
when (error) {
|
||||||
|
is MissingRestoreComponents -> {
|
||||||
|
appendLine(stringResource(MR.strings.backup_restore_content_full))
|
||||||
|
if (error.sources.isNotEmpty()) {
|
||||||
|
appendLine()
|
||||||
|
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||||
|
appendLine(stringResource(MR.strings.backup_restore_missing_sources))
|
||||||
|
}
|
||||||
|
error.sources.joinTo(
|
||||||
|
this,
|
||||||
|
separator = "\n- ",
|
||||||
|
prefix = "- ",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (error.trackers.isNotEmpty()) {
|
||||||
|
appendLine()
|
||||||
|
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||||
|
appendLine(stringResource(MR.strings.backup_restore_missing_trackers))
|
||||||
|
}
|
||||||
|
error.trackers.joinTo(
|
||||||
|
this,
|
||||||
|
separator = "\n- ",
|
||||||
|
prefix = "- ",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is InvalidRestore -> {
|
||||||
|
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||||
|
appendLine(stringResource(MR.strings.invalid_backup_file))
|
||||||
|
}
|
||||||
|
appendLine(error.uri.toString())
|
||||||
|
|
||||||
|
appendLine()
|
||||||
|
|
||||||
|
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||||
|
appendLine(stringResource(MR.strings.invalid_backup_file_error))
|
||||||
|
}
|
||||||
|
appendLine(error.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
appendLine(error.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectionContainer {
|
||||||
|
Text(text = msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class RestoreBackupScreenModel : StateScreenModel<RestoreBackupScreenModel.State>(State()) {
|
private class RestoreBackupScreenModel(
|
||||||
|
private val context: Context,
|
||||||
|
private val uri: String,
|
||||||
|
) : StateScreenModel<RestoreBackupScreenModel.State>(State()) {
|
||||||
|
|
||||||
fun setError(error: Any) {
|
init {
|
||||||
|
validate(uri.toUri())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggle(setter: (RestoreOptions, Boolean) -> RestoreOptions, enabled: Boolean) {
|
||||||
mutableState.update {
|
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 {
|
mutableState.update {
|
||||||
it.copy(error = null)
|
it.copy(
|
||||||
|
error = error,
|
||||||
|
canRestore = canRestore,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class State(
|
data class State(
|
||||||
val error: Any? = null,
|
val error: Any? = null,
|
||||||
// TODO: allow user-selectable restore options
|
val canRestore: Boolean = false,
|
||||||
val options: RestoreOptions = RestoreOptions(),
|
val options: RestoreOptions = RestoreOptions(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.theme.header
|
import tachiyomi.presentation.core.theme.header
|
||||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||||
@ -30,7 +31,7 @@ fun StorageInfo(
|
|||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||||
) {
|
) {
|
||||||
storages.forEach {
|
storages.forEach {
|
||||||
StorageInfo(it)
|
StorageInfo(it)
|
||||||
@ -50,7 +51,7 @@ private fun StorageInfo(
|
|||||||
val totalText = remember(total) { Formatter.formatFileSize(context, total) }
|
val totalText = remember(total) { Formatter.formatFileSize(context, total) }
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = file.absolutePath,
|
text = file.absolutePath,
|
||||||
|
@ -6,7 +6,7 @@ import androidx.compose.foundation.layout.PaddingValues
|
|||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
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.Icons
|
||||||
import androidx.compose.material.icons.outlined.CollectionsBookmark
|
import androidx.compose.material.icons.outlined.CollectionsBookmark
|
||||||
import androidx.compose.material.icons.outlined.LocalLibrary
|
import androidx.compose.material.icons.outlined.LocalLibrary
|
||||||
@ -18,10 +18,10 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import eu.kanade.presentation.more.stats.components.StatsItem
|
import eu.kanade.presentation.more.stats.components.StatsItem
|
||||||
import eu.kanade.presentation.more.stats.components.StatsOverviewItem
|
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.more.stats.data.StatsData
|
||||||
import eu.kanade.presentation.util.toDurationString
|
import eu.kanade.presentation.util.toDurationString
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
|
import tachiyomi.presentation.core.components.SectionCard
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@ -33,9 +33,7 @@ fun StatsScreenContent(
|
|||||||
state: StatsScreenState.Success,
|
state: StatsScreenState.Success,
|
||||||
paddingValues: PaddingValues,
|
paddingValues: PaddingValues,
|
||||||
) {
|
) {
|
||||||
val statListState = rememberLazyListState()
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = statListState,
|
|
||||||
contentPadding = paddingValues,
|
contentPadding = paddingValues,
|
||||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||||
) {
|
) {
|
||||||
@ -55,7 +53,7 @@ fun StatsScreenContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun OverviewSection(
|
private fun LazyItemScope.OverviewSection(
|
||||||
data: StatsData.Overview,
|
data: StatsData.Overview,
|
||||||
) {
|
) {
|
||||||
val none = stringResource(MR.strings.none)
|
val none = stringResource(MR.strings.none)
|
||||||
@ -65,7 +63,7 @@ private fun OverviewSection(
|
|||||||
.toDuration(DurationUnit.MILLISECONDS)
|
.toDuration(DurationUnit.MILLISECONDS)
|
||||||
.toDurationString(context, fallback = none)
|
.toDurationString(context, fallback = none)
|
||||||
}
|
}
|
||||||
StatsSection(MR.strings.label_overview_section) {
|
SectionCard(MR.strings.label_overview_section) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.height(IntrinsicSize.Min),
|
modifier = Modifier.height(IntrinsicSize.Min),
|
||||||
) {
|
) {
|
||||||
@ -89,10 +87,10 @@ private fun OverviewSection(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun TitlesStats(
|
private fun LazyItemScope.TitlesStats(
|
||||||
data: StatsData.Titles,
|
data: StatsData.Titles,
|
||||||
) {
|
) {
|
||||||
StatsSection(MR.strings.label_titles_section) {
|
SectionCard(MR.strings.label_titles_section) {
|
||||||
Row {
|
Row {
|
||||||
StatsItem(
|
StatsItem(
|
||||||
data.globalUpdateItemCount.toString(),
|
data.globalUpdateItemCount.toString(),
|
||||||
@ -111,10 +109,10 @@ private fun TitlesStats(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ChapterStats(
|
private fun LazyItemScope.ChapterStats(
|
||||||
data: StatsData.Chapters,
|
data: StatsData.Chapters,
|
||||||
) {
|
) {
|
||||||
StatsSection(MR.strings.chapters) {
|
SectionCard(MR.strings.chapters) {
|
||||||
Row {
|
Row {
|
||||||
StatsItem(
|
StatsItem(
|
||||||
data.totalChapterCount.toString(),
|
data.totalChapterCount.toString(),
|
||||||
@ -133,7 +131,7 @@ private fun ChapterStats(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun TrackerStats(
|
private fun LazyItemScope.TrackerStats(
|
||||||
data: StatsData.Trackers,
|
data: StatsData.Trackers,
|
||||||
) {
|
) {
|
||||||
val notApplicable = stringResource(MR.strings.not_applicable)
|
val notApplicable = stringResource(MR.strings.not_applicable)
|
||||||
@ -145,7 +143,7 @@ private fun TrackerStats(
|
|||||||
notApplicable
|
notApplicable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
StatsSection(MR.strings.label_tracker_section) {
|
SectionCard(MR.strings.label_tracker_section) {
|
||||||
Row {
|
Row {
|
||||||
StatsItem(
|
StatsItem(
|
||||||
data.trackedTitleCount.toString(),
|
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 eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
|
||||||
private val animationSpec = tween<IntOffset>(200)
|
private val animationSpec = tween<IntOffset>(200)
|
||||||
@ -156,7 +157,7 @@ fun ReaderAppBars(
|
|||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifierWithInsetsPadding,
|
modifier = modifierWithInsetsPadding,
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||||
) {
|
) {
|
||||||
ChapterNavigator(
|
ChapterNavigator(
|
||||||
isRtl = isRtl,
|
isRtl = isRtl,
|
||||||
|
@ -10,6 +10,7 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.outlined.Check
|
import androidx.compose.material.icons.outlined.Check
|
||||||
import androidx.compose.material3.FilledTonalButton
|
import androidx.compose.material3.FilledTonalButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@ -21,6 +22,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.SettingsItemsPaddings
|
import tachiyomi.presentation.core.components.SettingsItemsPaddings
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -50,7 +52,7 @@ fun ModeSelectionDialog(
|
|||||||
onClick = onApply,
|
onClick = onApply,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
|
@ -309,7 +309,7 @@ private fun SearchResultItemDetails(
|
|||||||
title: String,
|
title: String,
|
||||||
text: String,
|
text: String,
|
||||||
) {
|
) {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall)) {
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
|
@ -17,7 +17,6 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.util.fastAll
|
import androidx.compose.ui.util.fastAll
|
||||||
import androidx.compose.ui.util.fastAny
|
import androidx.compose.ui.util.fastAny
|
||||||
import eu.kanade.presentation.components.AppBar
|
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.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
|
import java.util.Date
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -44,7 +44,6 @@ fun UpdateScreen(
|
|||||||
state: UpdatesScreenModel.State,
|
state: UpdatesScreenModel.State,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
lastUpdated: Long,
|
lastUpdated: Long,
|
||||||
relativeTime: Boolean,
|
|
||||||
onClickCover: (UpdatesItem) -> Unit,
|
onClickCover: (UpdatesItem) -> Unit,
|
||||||
onSelectAll: (Boolean) -> Unit,
|
onSelectAll: (Boolean) -> Unit,
|
||||||
onInvertSelection: () -> Unit,
|
onInvertSelection: () -> Unit,
|
||||||
@ -58,8 +57,6 @@ fun UpdateScreen(
|
|||||||
) {
|
) {
|
||||||
BackHandler(enabled = state.selectionMode, onBack = { onSelectAll(false) })
|
BackHandler(enabled = state.selectionMode, onBack = { onSelectAll(false) })
|
||||||
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = { scrollBehavior ->
|
topBar = { scrollBehavior ->
|
||||||
UpdatesAppBar(
|
UpdatesAppBar(
|
||||||
@ -113,7 +110,7 @@ fun UpdateScreen(
|
|||||||
updatesLastUpdatedItem(lastUpdated)
|
updatesLastUpdatedItem(lastUpdated)
|
||||||
|
|
||||||
updatesUiItems(
|
updatesUiItems(
|
||||||
uiModels = state.getUiModel(context, relativeTime),
|
uiModels = state.getUiModel(),
|
||||||
selectionMode = state.selectionMode,
|
selectionMode = state.selectionMode,
|
||||||
onUpdateSelected = onUpdateSelected,
|
onUpdateSelected = onUpdateSelected,
|
||||||
onClickCover = onClickCover,
|
onClickCover = onClickCover,
|
||||||
@ -209,6 +206,6 @@ private fun UpdatesBottomBar(
|
|||||||
}
|
}
|
||||||
|
|
||||||
sealed interface UpdatesUiModel {
|
sealed interface UpdatesUiModel {
|
||||||
data class Header(val date: String) : UpdatesUiModel
|
data class Header(val date: Date) : UpdatesUiModel
|
||||||
data class Item(val item: UpdatesItem) : 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.font.FontStyle
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
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.ChapterDownloadAction
|
||||||
import eu.kanade.presentation.manga.components.ChapterDownloadIndicator
|
import eu.kanade.presentation.manga.components.ChapterDownloadIndicator
|
||||||
import eu.kanade.presentation.manga.components.DotSeparatorText
|
import eu.kanade.presentation.manga.components.DotSeparatorText
|
||||||
@ -91,7 +92,7 @@ internal fun LazyListScope.updatesUiItems(
|
|||||||
is UpdatesUiModel.Header -> {
|
is UpdatesUiModel.Header -> {
|
||||||
ListGroupHeader(
|
ListGroupHeader(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItemPlacement(),
|
||||||
text = item.date,
|
text = relativeDateText(item.date),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is UpdatesUiModel.Item -> {
|
is UpdatesUiModel.Item -> {
|
||||||
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
|
import android.app.job.JobInfo
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@ -52,10 +53,12 @@ import logcat.AndroidLogcatLogger
|
|||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import logcat.LogcatLogger
|
import logcat.LogcatLogger
|
||||||
import org.acra.config.httpSender
|
import org.acra.config.httpSender
|
||||||
|
import org.acra.config.scheduler
|
||||||
import org.acra.ktx.initAcra
|
import org.acra.ktx.initAcra
|
||||||
import org.acra.sender.HttpSender
|
import org.acra.sender.HttpSender
|
||||||
import org.conscrypt.Conscrypt
|
import org.conscrypt.Conscrypt
|
||||||
import tachiyomi.core.i18n.stringResource
|
import tachiyomi.core.i18n.stringResource
|
||||||
|
import tachiyomi.core.preference.Preference
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
import tachiyomi.domain.sync.SyncPreferences
|
import tachiyomi.domain.sync.SyncPreferences
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
@ -213,12 +216,20 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
|||||||
if (isPreviewBuildType || isReleaseBuildType) {
|
if (isPreviewBuildType || isReleaseBuildType) {
|
||||||
initAcra {
|
initAcra {
|
||||||
buildConfigClass = BuildConfig::class.java
|
buildConfigClass = BuildConfig::class.java
|
||||||
excludeMatchingSharedPreferencesKeys = listOf(".*username.*", ".*password.*", ".*token.*")
|
excludeMatchingSharedPreferencesKeys = listOf(
|
||||||
|
Preference.privateKey(".*"), ".*username.*", ".*password.*", ".*token.*",
|
||||||
|
)
|
||||||
|
|
||||||
httpSender {
|
httpSender {
|
||||||
uri = BuildConfig.ACRA_URI
|
uri = BuildConfig.ACRA_URI
|
||||||
httpMethod = HttpSender.Method.PUT
|
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.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.data.track.TrackerManager
|
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||||
import tachiyomi.core.i18n.stringResource
|
|
||||||
import tachiyomi.domain.source.service.SourceManager
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
import tachiyomi.i18n.MR
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
@ -19,7 +17,6 @@ class BackupFileValidator(
|
|||||||
/**
|
/**
|
||||||
* Checks for critical backup file data.
|
* Checks for critical backup file data.
|
||||||
*
|
*
|
||||||
* @throws Exception if manga cannot be found.
|
|
||||||
* @return List of missing sources or missing trackers.
|
* @return List of missing sources or missing trackers.
|
||||||
*/
|
*/
|
||||||
fun validate(uri: Uri): Results {
|
fun validate(uri: Uri): Results {
|
||||||
@ -29,10 +26,6 @@ class BackupFileValidator(
|
|||||||
throw IllegalStateException(e)
|
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 sources = backup.backupSources.associate { it.sourceId to it.name }
|
||||||
val missingSources = sources
|
val missingSources = sources
|
||||||
.filter { sourceManager.get(it.key) == null }
|
.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 eu.kanade.tachiyomi.util.system.notify
|
||||||
import tachiyomi.core.i18n.pluralStringResource
|
import tachiyomi.core.i18n.pluralStringResource
|
||||||
import tachiyomi.core.i18n.stringResource
|
import tachiyomi.core.i18n.stringResource
|
||||||
|
import tachiyomi.core.storage.displayablePath
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -73,7 +74,7 @@ class BackupNotifier(private val context: Context) {
|
|||||||
|
|
||||||
with(completeNotificationBuilder) {
|
with(completeNotificationBuilder) {
|
||||||
setContentTitle(context.stringResource(MR.strings.backup_created))
|
setContentTitle(context.stringResource(MR.strings.backup_created))
|
||||||
setContentText(file.filePath ?: file.name)
|
setContentText(file.displayablePath)
|
||||||
|
|
||||||
clearActions()
|
clearActions()
|
||||||
addAction(
|
addAction(
|
||||||
|
@ -19,8 +19,6 @@ import com.hippo.unifile.UniFile
|
|||||||
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
||||||
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
|
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
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.cancelNotification
|
||||||
import eu.kanade.tachiyomi.util.system.isRunning
|
import eu.kanade.tachiyomi.util.system.isRunning
|
||||||
import eu.kanade.tachiyomi.util.system.setForegroundSafely
|
import eu.kanade.tachiyomi.util.system.setForegroundSafely
|
||||||
@ -49,7 +47,7 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
|
|||||||
|
|
||||||
setForegroundSafely()
|
setForegroundSafely()
|
||||||
|
|
||||||
val options: BackupOptions = inputData.getBooleanArray(OPTIONS_KEY)?.asDataClass()
|
val options = inputData.getBooleanArray(OPTIONS_KEY)?.let { BackupOptions.fromBooleanArray(it) }
|
||||||
?: BackupOptions()
|
?: BackupOptions()
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
|
@ -15,28 +15,53 @@ data class BackupOptions(
|
|||||||
val privateSettings: Boolean = false,
|
val privateSettings: Boolean = false,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
fun asBooleanArray() = booleanArrayOf(
|
||||||
|
libraryEntries,
|
||||||
|
categories,
|
||||||
|
chapters,
|
||||||
|
tracking,
|
||||||
|
history,
|
||||||
|
appSettings,
|
||||||
|
sourceSettings,
|
||||||
|
privateSettings,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun anyEnabled() = libraryEntries || appSettings || sourceSettings
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val entries = persistentListOf(
|
val libraryOptions = persistentListOf(
|
||||||
|
Entry(
|
||||||
|
label = MR.strings.manga,
|
||||||
|
getter = BackupOptions::libraryEntries,
|
||||||
|
setter = { options, enabled -> options.copy(libraryEntries = enabled) },
|
||||||
|
),
|
||||||
Entry(
|
Entry(
|
||||||
label = MR.strings.categories,
|
label = MR.strings.categories,
|
||||||
getter = BackupOptions::categories,
|
getter = BackupOptions::categories,
|
||||||
setter = { options, enabled -> options.copy(categories = enabled) },
|
setter = { options, enabled -> options.copy(categories = enabled) },
|
||||||
|
enabled = { it.libraryEntries },
|
||||||
),
|
),
|
||||||
Entry(
|
Entry(
|
||||||
label = MR.strings.chapters,
|
label = MR.strings.chapters,
|
||||||
getter = BackupOptions::chapters,
|
getter = BackupOptions::chapters,
|
||||||
setter = { options, enabled -> options.copy(chapters = enabled) },
|
setter = { options, enabled -> options.copy(chapters = enabled) },
|
||||||
|
enabled = { it.libraryEntries },
|
||||||
),
|
),
|
||||||
Entry(
|
Entry(
|
||||||
label = MR.strings.track,
|
label = MR.strings.track,
|
||||||
getter = BackupOptions::tracking,
|
getter = BackupOptions::tracking,
|
||||||
setter = { options, enabled -> options.copy(tracking = enabled) },
|
setter = { options, enabled -> options.copy(tracking = enabled) },
|
||||||
|
enabled = { it.libraryEntries },
|
||||||
),
|
),
|
||||||
Entry(
|
Entry(
|
||||||
label = MR.strings.history,
|
label = MR.strings.history,
|
||||||
getter = BackupOptions::history,
|
getter = BackupOptions::history,
|
||||||
setter = { options, enabled -> options.copy(history = enabled) },
|
setter = { options, enabled -> options.copy(history = enabled) },
|
||||||
|
enabled = { it.libraryEntries },
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val settingsOptions = persistentListOf(
|
||||||
Entry(
|
Entry(
|
||||||
label = MR.strings.app_settings,
|
label = MR.strings.app_settings,
|
||||||
getter = BackupOptions::appSettings,
|
getter = BackupOptions::appSettings,
|
||||||
@ -51,13 +76,26 @@ data class BackupOptions(
|
|||||||
label = MR.strings.private_settings,
|
label = MR.strings.private_settings,
|
||||||
getter = BackupOptions::privateSettings,
|
getter = BackupOptions::privateSettings,
|
||||||
setter = { options, enabled -> options.copy(privateSettings = enabled) },
|
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(
|
data class Entry(
|
||||||
val label: StringResource,
|
val label: StringResource,
|
||||||
val getter: (BackupOptions) -> Boolean,
|
val getter: (BackupOptions) -> Boolean,
|
||||||
val setter: (BackupOptions, Boolean) -> BackupOptions,
|
val setter: (BackupOptions, Boolean) -> BackupOptions,
|
||||||
|
val enabled: (BackupOptions) -> Boolean = { true },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,7 @@ class PreferenceBackupCreator(
|
|||||||
.withPrivatePreferences(includePrivatePreferences),
|
.withPrivatePreferences(includePrivatePreferences),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
.filter { it.prefs.isNotEmpty() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
@ -13,8 +13,6 @@ import androidx.work.WorkerParameters
|
|||||||
import androidx.work.workDataOf
|
import androidx.work.workDataOf
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
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.cancelNotification
|
||||||
import eu.kanade.tachiyomi.util.system.isRunning
|
import eu.kanade.tachiyomi.util.system.isRunning
|
||||||
import eu.kanade.tachiyomi.util.system.setForegroundSafely
|
import eu.kanade.tachiyomi.util.system.setForegroundSafely
|
||||||
@ -32,7 +30,7 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
|
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri()
|
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) {
|
if (uri == null || options == null) {
|
||||||
return Result.failure()
|
return Result.failure()
|
||||||
|
@ -1,7 +1,52 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.restore
|
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(
|
data class RestoreOptions(
|
||||||
|
val library: Boolean = true,
|
||||||
val appSettings: Boolean = true,
|
val appSettings: Boolean = true,
|
||||||
val sourceSettings: 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 eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.i18n.stringResource
|
import tachiyomi.core.i18n.stringResource
|
||||||
|
import tachiyomi.core.storage.displayablePath
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
@ -41,7 +42,12 @@ class DownloadProvider(
|
|||||||
.createDirectory(getMangaDirName(mangaTitle))!!
|
.createDirectory(getMangaDirName(mangaTitle))!!
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
logcat(LogPriority.ERROR, e) { "Invalid download directory" }
|
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.awaitAll
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.ensureActive
|
import kotlinx.coroutines.ensureActive
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.coroutines.sync.Semaphore
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
import kotlinx.coroutines.sync.withPermit
|
import kotlinx.coroutines.sync.withPermit
|
||||||
import logcat.LogPriority
|
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.
|
* @param categoryId the ID of the category to update, or -1 if no category specified.
|
||||||
*/
|
*/
|
||||||
private fun addMangaToQueue(categoryId: Long) {
|
private suspend fun addMangaToQueue(categoryId: Long) {
|
||||||
val libraryManga = runBlocking { getLibraryManga.await() }
|
val libraryManga = getLibraryManga.await()
|
||||||
|
|
||||||
val listToUpdate = if (categoryId != -1L) {
|
val listToUpdate = if (categoryId != -1L) {
|
||||||
libraryManga.filter { it.category == categoryId }
|
libraryManga.filter { it.category == categoryId }
|
||||||
@ -181,7 +180,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
|
|
||||||
val restrictions = libraryPreferences.autoUpdateMangaRestrictions().get()
|
val restrictions = libraryPreferences.autoUpdateMangaRestrictions().get()
|
||||||
val skippedUpdates = mutableListOf<Pair<Manga, String?>>()
|
val skippedUpdates = mutableListOf<Pair<Manga, String?>>()
|
||||||
val fetchWindow = fetchInterval.getWindow(ZonedDateTime.now())
|
val (_, fetchWindowUpperBound) = fetchInterval.getWindow(ZonedDateTime.now())
|
||||||
|
|
||||||
mangaToUpdate = listToUpdate
|
mangaToUpdate = listToUpdate
|
||||||
.filter {
|
.filter {
|
||||||
@ -208,7 +207,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
MANGA_OUTSIDE_RELEASE_PERIOD in restrictions && it.manga.nextUpdate > fetchWindow.second -> {
|
MANGA_OUTSIDE_RELEASE_PERIOD in restrictions && it.manga.nextUpdate > fetchWindowUpperBound -> {
|
||||||
skippedUpdates.add(
|
skippedUpdates.add(
|
||||||
it.manga to context.stringResource(MR.strings.skipped_reason_not_in_release_period),
|
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 }
|
.sortedBy { it.manga.title }
|
||||||
|
|
||||||
// Warn when excessively checking a single source
|
notifier.showQueueSizeWarningNotificationIfNeeded(mangaToUpdate)
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (skippedUpdates.isNotEmpty()) {
|
if (skippedUpdates.isNotEmpty()) {
|
||||||
// TODO: surface skipped reasons to user?
|
// 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.NotificationHandler
|
||||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
|
import eu.kanade.tachiyomi.source.UnmeteredSource
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.util.lang.chop
|
import eu.kanade.tachiyomi.util.lang.chop
|
||||||
import eu.kanade.tachiyomi.util.system.cancelNotification
|
import eu.kanade.tachiyomi.util.system.cancelNotification
|
||||||
@ -30,15 +31,22 @@ import tachiyomi.core.i18n.pluralStringResource
|
|||||||
import tachiyomi.core.i18n.stringResource
|
import tachiyomi.core.i18n.stringResource
|
||||||
import tachiyomi.core.util.lang.launchUI
|
import tachiyomi.core.util.lang.launchUI
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
|
import tachiyomi.domain.library.model.LibraryManga
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
import tachiyomi.i18n.MR
|
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.math.RoundingMode
|
||||||
import java.text.NumberFormat
|
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 {
|
private val percentFormatter = NumberFormat.getPercentInstance().apply {
|
||||||
roundingMode = RoundingMode.DOWN
|
roundingMode = RoundingMode.DOWN
|
||||||
maximumFractionDigits = 0
|
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) }
|
val updatingText = manga.joinToString("\n") { it.title.chop(40) }
|
||||||
progressNotificationBuilder.setStyle(NotificationCompat.BigTextStyle().bigText(updatingText))
|
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(
|
context.notify(
|
||||||
Notifications.ID_LIBRARY_SIZE_WARNING,
|
Notifications.ID_LIBRARY_SIZE_WARNING,
|
||||||
Notifications.CHANNEL_LIBRARY_PROGRESS,
|
Notifications.CHANNEL_LIBRARY_PROGRESS,
|
||||||
@ -151,7 +171,7 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
Notifications.CHANNEL_NEW_CHAPTERS,
|
Notifications.CHANNEL_NEW_CHAPTERS,
|
||||||
) {
|
) {
|
||||||
setContentTitle(context.stringResource(MR.strings.notification_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))
|
setContentText(updates.first().first.title.chop(NOTIF_TITLE_MAX_LEN))
|
||||||
} else {
|
} else {
|
||||||
setContentText(
|
setContentText(
|
||||||
@ -162,7 +182,7 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!preferences.hideNotificationContent().get()) {
|
if (!securityPreferences.hideNotificationContent().get()) {
|
||||||
setStyle(
|
setStyle(
|
||||||
NotificationCompat.BigTextStyle().bigText(
|
NotificationCompat.BigTextStyle().bigText(
|
||||||
updates.joinToString("\n") {
|
updates.joinToString("\n") {
|
||||||
@ -186,7 +206,7 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Per-manga notification
|
// Per-manga notification
|
||||||
if (!preferences.hideNotificationContent().get()) {
|
if (!securityPreferences.hideNotificationContent().get()) {
|
||||||
launchUI {
|
launchUI {
|
||||||
context.notify(
|
context.notify(
|
||||||
updates.map { (manga, chapters) ->
|
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_MAX_CHAPTERS = 5
|
||||||
private const val NOTIF_TITLE_MAX_LEN = 45
|
private const val NOTIF_TITLE_MAX_LEN = 45
|
||||||
private const val NOTIF_ICON_SIZE = 192
|
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.domain.manga.model.toSManga
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.source.UnmeteredSource
|
|
||||||
import eu.kanade.tachiyomi.util.prepUpdateCover
|
import eu.kanade.tachiyomi.util.prepUpdateCover
|
||||||
import eu.kanade.tachiyomi.util.system.isRunning
|
import eu.kanade.tachiyomi.util.system.isRunning
|
||||||
import eu.kanade.tachiyomi.util.system.setForegroundSafely
|
import eu.kanade.tachiyomi.util.system.setForegroundSafely
|
||||||
@ -25,7 +24,6 @@ import kotlinx.coroutines.async
|
|||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.ensureActive
|
import kotlinx.coroutines.ensureActive
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.coroutines.sync.Semaphore
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
import kotlinx.coroutines.sync.withPermit
|
import kotlinx.coroutines.sync.withPermit
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
@ -92,17 +90,9 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame
|
|||||||
/**
|
/**
|
||||||
* Adds list of manga to be updated.
|
* Adds list of manga to be updated.
|
||||||
*/
|
*/
|
||||||
private fun addMangaToQueue() {
|
private suspend fun addMangaToQueue() {
|
||||||
mangaToUpdate = runBlocking { getLibraryManga.await() }
|
mangaToUpdate = getLibraryManga.await()
|
||||||
|
notifier.showQueueSizeWarningNotificationIfNeeded(mangaToUpdate)
|
||||||
// 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 updateMetadata() {
|
private suspend fun updateMetadata() {
|
||||||
|
@ -48,7 +48,7 @@ fun extensionsTab(
|
|||||||
},
|
},
|
||||||
onClickItemCancel = extensionsScreenModel::cancelInstallUpdateExtension,
|
onClickItemCancel = extensionsScreenModel::cancelInstallUpdateExtension,
|
||||||
onClickUpdateAll = extensionsScreenModel::updateAllExtensions,
|
onClickUpdateAll = extensionsScreenModel::updateAllExtensions,
|
||||||
onClickItemWebView = { extension ->
|
onOpenWebView = { extension ->
|
||||||
extension.sources.getOrNull(0)?.let {
|
extension.sources.getOrNull(0)?.let {
|
||||||
navigator.push(
|
navigator.push(
|
||||||
WebViewScreen(
|
WebViewScreen(
|
||||||
|
@ -19,7 +19,6 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.toMutableStateList
|
import androidx.compose.runtime.toMutableStateList
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
|
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
|
||||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
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.domain.track.interactor.InsertTrack
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.LabeledCheckbox
|
import tachiyomi.presentation.core.components.LabeledCheckbox
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
@ -96,7 +96,7 @@ internal fun MigrateDialog(
|
|||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
FlowRow(
|
FlowRow(
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
) {
|
) {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
|
@ -104,8 +104,6 @@ class MangaScreen(
|
|||||||
MangaScreen(
|
MangaScreen(
|
||||||
state = successState,
|
state = successState,
|
||||||
snackbarHostState = screenModel.snackbarHostState,
|
snackbarHostState = screenModel.snackbarHostState,
|
||||||
dateRelativeTime = screenModel.relativeTime,
|
|
||||||
dateFormat = screenModel.dateFormat,
|
|
||||||
fetchInterval = successState.manga.fetchInterval,
|
fetchInterval = successState.manga.fetchInterval,
|
||||||
isTabletUi = isTabletUi(),
|
isTabletUi = isTabletUi(),
|
||||||
chapterSwipeStartAction = screenModel.chapterSwipeStartAction,
|
chapterSwipeStartAction = screenModel.chapterSwipeStartAction,
|
||||||
@ -245,6 +243,7 @@ class MangaScreen(
|
|||||||
is MangaScreenModel.Dialog.SetFetchInterval -> {
|
is MangaScreenModel.Dialog.SetFetchInterval -> {
|
||||||
SetIntervalDialog(
|
SetIntervalDialog(
|
||||||
interval = dialog.manga.fetchInterval,
|
interval = dialog.manga.fetchInterval,
|
||||||
|
nextUpdate = dialog.manga.nextUpdate,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
onValueChanged = { screenModel.setFetchInterval(dialog.manga, it) },
|
onValueChanged = { screenModel.setFetchInterval(dialog.manga, it) },
|
||||||
)
|
)
|
||||||
|
@ -5,7 +5,6 @@ import androidx.compose.material3.SnackbarHostState
|
|||||||
import androidx.compose.material3.SnackbarResult
|
import androidx.compose.material3.SnackbarResult
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.ui.util.fastAny
|
import androidx.compose.ui.util.fastAny
|
||||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
import cafe.adriel.voyager.core.model.screenModelScope
|
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.downloadedFilter
|
||||||
import eu.kanade.domain.manga.model.toSManga
|
import eu.kanade.domain.manga.model.toSManga
|
||||||
import eu.kanade.domain.track.interactor.AddTracks
|
import eu.kanade.domain.track.interactor.AddTracks
|
||||||
import eu.kanade.domain.ui.UiPreferences
|
|
||||||
import eu.kanade.presentation.manga.DownloadAction
|
import eu.kanade.presentation.manga.DownloadAction
|
||||||
import eu.kanade.presentation.manga.components.ChapterDownloadAction
|
import eu.kanade.presentation.manga.components.ChapterDownloadAction
|
||||||
import eu.kanade.presentation.util.formattedMessage
|
import eu.kanade.presentation.util.formattedMessage
|
||||||
@ -92,7 +90,6 @@ class MangaScreenModel(
|
|||||||
private val downloadPreferences: DownloadPreferences = Injekt.get(),
|
private val downloadPreferences: DownloadPreferences = Injekt.get(),
|
||||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||||
readerPreferences: ReaderPreferences = Injekt.get(),
|
readerPreferences: ReaderPreferences = Injekt.get(),
|
||||||
uiPreferences: UiPreferences = Injekt.get(),
|
|
||||||
private val trackerManager: TrackerManager = Injekt.get(),
|
private val trackerManager: TrackerManager = Injekt.get(),
|
||||||
private val downloadManager: DownloadManager = Injekt.get(),
|
private val downloadManager: DownloadManager = Injekt.get(),
|
||||||
private val downloadCache: DownloadCache = Injekt.get(),
|
private val downloadCache: DownloadCache = Injekt.get(),
|
||||||
@ -138,8 +135,6 @@ class MangaScreenModel(
|
|||||||
val chapterSwipeStartAction = libraryPreferences.swipeToEndAction().get()
|
val chapterSwipeStartAction = libraryPreferences.swipeToEndAction().get()
|
||||||
val chapterSwipeEndAction = libraryPreferences.swipeToStartAction().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)
|
private val skipFiltered by readerPreferences.skipFiltered().asState(screenModelScope)
|
||||||
|
|
||||||
val isUpdateIntervalEnabled =
|
val isUpdateIntervalEnabled =
|
||||||
|
@ -30,7 +30,6 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
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.ScreenModel
|
||||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
@ -759,7 +758,7 @@ private data class TrackerRemoveScreen(
|
|||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(MR.strings.track_delete_text, serviceName),
|
text = stringResource(MR.strings.track_delete_text, serviceName),
|
||||||
|
@ -1,18 +1,15 @@
|
|||||||
package eu.kanade.tachiyomi.ui.updates
|
package eu.kanade.tachiyomi.ui.updates
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
import cafe.adriel.voyager.core.model.screenModelScope
|
import cafe.adriel.voyager.core.model.screenModelScope
|
||||||
import eu.kanade.core.preference.asState
|
import eu.kanade.core.preference.asState
|
||||||
import eu.kanade.core.util.addOrRemove
|
import eu.kanade.core.util.addOrRemove
|
||||||
import eu.kanade.core.util.insertSeparators
|
import eu.kanade.core.util.insertSeparators
|
||||||
import eu.kanade.domain.chapter.interactor.SetReadStatus
|
import eu.kanade.domain.chapter.interactor.SetReadStatus
|
||||||
import eu.kanade.domain.ui.UiPreferences
|
|
||||||
import eu.kanade.presentation.manga.components.ChapterDownloadAction
|
import eu.kanade.presentation.manga.components.ChapterDownloadAction
|
||||||
import eu.kanade.presentation.updates.UpdatesUiModel
|
import eu.kanade.presentation.updates.UpdatesUiModel
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadCache
|
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.download.model.Download
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||||
import eu.kanade.tachiyomi.util.lang.toDateKey
|
import eu.kanade.tachiyomi.util.lang.toDateKey
|
||||||
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
|
||||||
import kotlinx.collections.immutable.PersistentList
|
import kotlinx.collections.immutable.PersistentList
|
||||||
import kotlinx.collections.immutable.mutate
|
import kotlinx.collections.immutable.mutate
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
@ -63,14 +59,12 @@ class UpdatesScreenModel(
|
|||||||
private val getChapter: GetChapter = Injekt.get(),
|
private val getChapter: GetChapter = Injekt.get(),
|
||||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||||
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
||||||
uiPreferences: UiPreferences = Injekt.get(),
|
|
||||||
) : StateScreenModel<UpdatesScreenModel.State>(State()) {
|
) : StateScreenModel<UpdatesScreenModel.State>(State()) {
|
||||||
|
|
||||||
private val _events: Channel<Event> = Channel(Int.MAX_VALUE)
|
private val _events: Channel<Event> = Channel(Int.MAX_VALUE)
|
||||||
val events: Flow<Event> = _events.receiveAsFlow()
|
val events: Flow<Event> = _events.receiveAsFlow()
|
||||||
|
|
||||||
val lastUpdated by libraryPreferences.lastUpdatedTimestamp().asState(screenModelScope)
|
val lastUpdated by libraryPreferences.lastUpdatedTimestamp().asState(screenModelScope)
|
||||||
val relativeTime by uiPreferences.relativeTime().asState(screenModelScope)
|
|
||||||
|
|
||||||
// First and last selected index in list
|
// First and last selected index in list
|
||||||
private val selectedPositions: Array<Int> = arrayOf(-1, -1)
|
private val selectedPositions: Array<Int> = arrayOf(-1, -1)
|
||||||
@ -376,9 +370,7 @@ class UpdatesScreenModel(
|
|||||||
val selected = items.filter { it.selected }
|
val selected = items.filter { it.selected }
|
||||||
val selectionMode = selected.isNotEmpty()
|
val selectionMode = selected.isNotEmpty()
|
||||||
|
|
||||||
fun getUiModel(context: Context, relativeTime: Boolean): List<UpdatesUiModel> {
|
fun getUiModel(): List<UpdatesUiModel> {
|
||||||
val dateFormat by mutableStateOf(UiPreferences.dateFormat(Injekt.get<UiPreferences>().dateFormat().get()))
|
|
||||||
|
|
||||||
return items
|
return items
|
||||||
.map { UpdatesUiModel.Item(it) }
|
.map { UpdatesUiModel.Item(it) }
|
||||||
.insertSeparators { before, after ->
|
.insertSeparators { before, after ->
|
||||||
@ -386,12 +378,7 @@ class UpdatesScreenModel(
|
|||||||
val afterDate = after?.item?.update?.dateFetch?.toDateKey() ?: Date(0)
|
val afterDate = after?.item?.update?.dateFetch?.toDateKey() ?: Date(0)
|
||||||
when {
|
when {
|
||||||
beforeDate.time != afterDate.time && afterDate.time != 0L -> {
|
beforeDate.time != afterDate.time && afterDate.time != 0L -> {
|
||||||
val text = afterDate.toRelativeString(
|
UpdatesUiModel.Header(afterDate)
|
||||||
context = context,
|
|
||||||
relative = relativeTime,
|
|
||||||
dateFormat = dateFormat,
|
|
||||||
)
|
|
||||||
UpdatesUiModel.Header(text)
|
|
||||||
}
|
}
|
||||||
// Return null to avoid adding a separator between two items.
|
// Return null to avoid adding a separator between two items.
|
||||||
else -> null
|
else -> null
|
||||||
|
@ -59,7 +59,6 @@ object UpdatesTab : Tab {
|
|||||||
state = state,
|
state = state,
|
||||||
snackbarHostState = screenModel.snackbarHostState,
|
snackbarHostState = screenModel.snackbarHostState,
|
||||||
lastUpdated = screenModel.lastUpdated,
|
lastUpdated = screenModel.lastUpdated,
|
||||||
relativeTime = screenModel.relativeTime,
|
|
||||||
onClickCover = { item -> navigator.push(MangaScreen(item.update.mangaId)) },
|
onClickCover = { item -> navigator.push(MangaScreen(item.update.mangaId)) },
|
||||||
onSelectAll = screenModel::toggleAllSelection,
|
onSelectAll = screenModel::toggleAllSelection,
|
||||||
onInvertSelection = screenModel::invertSelection,
|
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
|
// JavaScript engine
|
||||||
implementation(libs.bundles.js.engine)
|
implementation(libs.bundles.js.engine)
|
||||||
|
|
||||||
|
testImplementation(libs.bundles.test)
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,9 @@ val UniFile.extension: String?
|
|||||||
val UniFile.nameWithoutExtension: String?
|
val UniFile.nameWithoutExtension: String?
|
||||||
get() = name?.substringBeforeLast('.')
|
get() = name?.substringBeforeLast('.')
|
||||||
|
|
||||||
|
val UniFile.displayablePath: String
|
||||||
|
get() = filePath ?: uri.toString()
|
||||||
|
|
||||||
fun UniFile.toTempFile(context: Context): File {
|
fun UniFile.toTempFile(context: Context): File {
|
||||||
val inputStream = context.contentResolver.openInputStream(uri)!!
|
val inputStream = context.contentResolver.openInputStream(uri)!!
|
||||||
val tempFile = File.createTempFile(
|
val tempFile = File.createTempFile(
|
||||||
|
@ -19,16 +19,15 @@ class FetchInterval(
|
|||||||
dateTime: ZonedDateTime,
|
dateTime: ZonedDateTime,
|
||||||
window: Pair<Long, Long>,
|
window: Pair<Long, Long>,
|
||||||
): MangaUpdate? {
|
): 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) {
|
val currentWindow = if (window.first == 0L && window.second == 0L) {
|
||||||
getWindow(ZonedDateTime.now())
|
getWindow(ZonedDateTime.now())
|
||||||
} else {
|
} else {
|
||||||
window
|
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)
|
val nextUpdate = calculateNextUpdate(manga, interval, dateTime, currentWindow)
|
||||||
|
|
||||||
return if (manga.nextUpdate == nextUpdate && manga.fetchInterval == interval) {
|
return if (manga.nextUpdate == nextUpdate && manga.fetchInterval == interval) {
|
||||||
@ -102,7 +101,7 @@ class FetchInterval(
|
|||||||
manga.fetchInterval == 0
|
manga.fetchInterval == 0
|
||||||
) {
|
) {
|
||||||
val latestDate = ZonedDateTime.ofInstant(
|
val latestDate = ZonedDateTime.ofInstant(
|
||||||
Instant.ofEpochMilli(manga.lastUpdate),
|
if (manga.lastUpdate > 0) Instant.ofEpochMilli(manga.lastUpdate) else Instant.now(),
|
||||||
dateTime.zone,
|
dateTime.zone,
|
||||||
)
|
)
|
||||||
.toLocalDate()
|
.toLocalDate()
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
[versions]
|
[versions]
|
||||||
aboutlib_version = "10.9.2"
|
aboutlib_version = "10.10.0"
|
||||||
|
acra = "5.11.3"
|
||||||
leakcanary = "2.12"
|
leakcanary = "2.12"
|
||||||
moko = "0.23.0"
|
moko = "0.23.0"
|
||||||
okhttp_version = "5.0.0-alpha.12"
|
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"
|
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"
|
disklrucache = "com.jakewharton:disklrucache:2.0.2"
|
||||||
unifile = "com.github.tachiyomiorg:unifile:7c257e1c64"
|
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"
|
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"
|
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-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-api = { module = "dev.rikka.shizuku:api", version.ref = "shizuku_version" }
|
||||||
shizuku-provider = { module = "dev.rikka.shizuku:provider", 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"
|
google-api-client-oauth = "com.google.oauth-client:google-oauth-client:1.34.1"
|
||||||
|
|
||||||
[bundles]
|
[bundles]
|
||||||
|
acra = ["acra-http", "acra-scheduler"]
|
||||||
okhttp = ["okhttp-core", "okhttp-logging", "okhttp-brotli", "okhttp-dnsoverhttps"]
|
okhttp = ["okhttp-core", "okhttp-logging", "okhttp-brotli", "okhttp-dnsoverhttps"]
|
||||||
js-engine = ["quickjs-android"]
|
js-engine = ["quickjs-android"]
|
||||||
sqlite = ["sqlite-framework", "sqlite-ktx", "sqlite-android"]
|
sqlite = ["sqlite-framework", "sqlite-ktx", "sqlite-android"]
|
||||||
|
@ -498,11 +498,12 @@
|
|||||||
<string name="pref_backup_interval">Automatic backup frequency</string>
|
<string name="pref_backup_interval">Automatic backup frequency</string>
|
||||||
<string name="action_create">Create</string>
|
<string name="action_create">Create</string>
|
||||||
<string name="backup_created">Backup created</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="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_sources">Missing sources:</string>
|
||||||
<string name="backup_restore_missing_trackers">Trackers not logged into:</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_completed">Restore completed</string>
|
||||||
<string name="restore_duration">%02d min, %02d sec</string>
|
<string name="restore_duration">%02d min, %02d sec</string>
|
||||||
<string name="backup_in_progress">Backup is already in progress</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="display_mode_chapter">Chapter %1$s</string>
|
||||||
<string name="manga_display_interval_title">Estimate every</string>
|
<string name="manga_display_interval_title">Estimate every</string>
|
||||||
<string name="manga_display_modified_interval_title">Set to update 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="manga_modify_calculated_interval_title">Customize interval</string>
|
||||||
<string name="chapter_downloading_progress">Downloading (%1$d/%2$d)</string>
|
<string name="chapter_downloading_progress">Downloading (%1$d/%2$d)</string>
|
||||||
<string name="chapter_error">Error</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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@ -10,7 +11,7 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ActionButton(
|
fun ActionButton(
|
||||||
@ -24,7 +25,7 @@ fun ActionButton(
|
|||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
|
@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LabeledCheckbox(
|
fun LabeledCheckbox(
|
||||||
@ -30,10 +31,14 @@ fun LabeledCheckbox(
|
|||||||
.heightIn(min = 48.dp)
|
.heightIn(min = 48.dp)
|
||||||
.clickable(
|
.clickable(
|
||||||
role = Role.Checkbox,
|
role = Role.Checkbox,
|
||||||
onClick = { onCheckedChange(!checked) },
|
onClick = {
|
||||||
|
if (enabled) {
|
||||||
|
onCheckedChange(!checked)
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||||
) {
|
) {
|
||||||
Checkbox(
|
Checkbox(
|
||||||
checked = checked,
|
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.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyItemScope
|
||||||
import androidx.compose.material3.ElevatedCard
|
import androidx.compose.material3.ElevatedCard
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@ -13,15 +15,18 @@ import tachiyomi.presentation.core.components.material.padding
|
|||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun StatsSection(
|
fun LazyItemScope.SectionCard(
|
||||||
titleRes: StringResource,
|
titleRes: StringResource? = null,
|
||||||
content: @Composable () -> Unit,
|
content: @Composable ColumnScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
|
if (titleRes != null) {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(horizontal = MaterialTheme.padding.extraLarge),
|
modifier = Modifier.padding(horizontal = MaterialTheme.padding.extraLarge),
|
||||||
text = stringResource(titleRes),
|
text = stringResource(titleRes),
|
||||||
style = MaterialTheme.typography.titleSmall,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
ElevatedCard(
|
ElevatedCard(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
@ -345,7 +345,7 @@ fun SettingsIconGrid(labelRes: StringResource, content: LazyGridScope.() -> Unit
|
|||||||
end = SettingsItemsPaddings.Horizontal,
|
end = SettingsItemsPaddings.Horizontal,
|
||||||
bottom = SettingsItemsPaddings.Vertical,
|
bottom = SettingsItemsPaddings.Vertical,
|
||||||
),
|
),
|
||||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny),
|
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||||
content = content,
|
content = content,
|
||||||
)
|
)
|
||||||
|
@ -19,7 +19,7 @@ class Padding {
|
|||||||
|
|
||||||
val small = 8.dp
|
val small = 8.dp
|
||||||
|
|
||||||
val tiny = 4.dp
|
val extraSmall = 4.dp
|
||||||
}
|
}
|
||||||
|
|
||||||
val MaterialTheme.padding: Padding
|
val MaterialTheme.padding: Padding
|
||||||
|
@ -45,11 +45,11 @@ fun NavigationRail(
|
|||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.windowInsetsPadding(windowInsets)
|
.windowInsetsPadding(windowInsets)
|
||||||
.widthIn(min = 80.dp)
|
.widthIn(min = 80.dp)
|
||||||
.padding(vertical = MaterialTheme.padding.tiny)
|
.padding(vertical = MaterialTheme.padding.extraSmall)
|
||||||
.selectableGroup(),
|
.selectableGroup(),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.spacedBy(
|
verticalArrangement = Arrangement.spacedBy(
|
||||||
MaterialTheme.padding.tiny,
|
MaterialTheme.padding.extraSmall,
|
||||||
alignment = Alignment.CenterVertically,
|
alignment = Alignment.CenterVertically,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
|
Loading…
Reference in New Issue
Block a user