diff --git a/README.md b/README.md
index 0beaf83ae..a9ac2dfc6 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,6 @@
|-------|----------|---------|------------|---------|
| [![CI](https://github.com/tachiyomiorg/tachiyomi/actions/workflows/build_push.yml/badge.svg)](https://github.com/tachiyomiorg/tachiyomi/actions/workflows/build_push.yml) | [![stable release](https://img.shields.io/github/release/tachiyomiorg/tachiyomi.svg?maxAge=3600&label=download)](https://github.com/tachiyomiorg/tachiyomi/releases) | [![latest preview build](https://img.shields.io/github/v/release/tachiyomiorg/tachiyomi-preview.svg?maxAge=3600&label=download)](https://github.com/tachiyomiorg/tachiyomi-preview/releases) | [![Translation status](https://hosted.weblate.org/widgets/tachiyomi/-/svg-badge.svg)](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) | [![Discord](https://img.shields.io/discord/349436576037732353.svg?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/tachiyomi) |
-
# ![app icon](./.github/readme-images/app-icon.png)Tachiyomi
Tachiyomi is a free and open source manga reader for Android 6.0 and above.
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index b1ea84c40..a4eb2c039 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -45,7 +45,7 @@
##---------------Begin: proguard configuration for kotlinx.serialization ----------
-keepattributes *Annotation*, InnerClasses
--dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations
+-dontnote kotlinx.serialization.** # core serialization annotations
# kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer
-keepclassmembers class kotlinx.serialization.json.** {
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d7a1aa265..78b43e888 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -8,7 +8,8 @@
-
+
@@ -20,10 +21,12 @@
-
+
-
+
diff --git a/app/src/main/java/eu/kanade/domain/track/interactor/RefreshTracks.kt b/app/src/main/java/eu/kanade/domain/track/interactor/RefreshTracks.kt
index 8c8952304..457dbf87e 100644
--- a/app/src/main/java/eu/kanade/domain/track/interactor/RefreshTracks.kt
+++ b/app/src/main/java/eu/kanade/domain/track/interactor/RefreshTracks.kt
@@ -25,7 +25,7 @@ class RefreshTracks(
suspend fun await(mangaId: Long): List> {
return supervisorScope {
return@supervisorScope getTracks.await(mangaId)
- .map { it to trackerManager.get(it.syncId) }
+ .map { it to trackerManager.get(it.trackerId) }
.filter { (_, service) -> service?.isLoggedIn == true }
.map { (track, service) ->
async {
diff --git a/app/src/main/java/eu/kanade/domain/track/interactor/TrackChapter.kt b/app/src/main/java/eu/kanade/domain/track/interactor/TrackChapter.kt
index 789a784ef..557c3be83 100644
--- a/app/src/main/java/eu/kanade/domain/track/interactor/TrackChapter.kt
+++ b/app/src/main/java/eu/kanade/domain/track/interactor/TrackChapter.kt
@@ -27,7 +27,7 @@ class TrackChapter(
if (tracks.isEmpty()) return@withNonCancellableContext
tracks.mapNotNull { track ->
- val service = trackerManager.get(track.syncId)
+ val service = trackerManager.get(track.trackerId)
if (service == null || !service.isLoggedIn || chapterNumber <= track.lastChapterRead) {
return@mapNotNull null
}
diff --git a/app/src/main/java/eu/kanade/domain/track/model/Track.kt b/app/src/main/java/eu/kanade/domain/track/model/Track.kt
index e16846e0e..e84e28ff0 100644
--- a/app/src/main/java/eu/kanade/domain/track/model/Track.kt
+++ b/app/src/main/java/eu/kanade/domain/track/model/Track.kt
@@ -13,10 +13,10 @@ fun Track.copyPersonalFrom(other: Track): Track {
)
}
-fun Track.toDbTrack(): DbTrack = DbTrack.create(syncId).also {
+fun Track.toDbTrack(): DbTrack = DbTrack.create(trackerId).also {
it.id = id
it.manga_id = mangaId
- it.media_id = remoteId
+ it.remote_id = remoteId
it.library_id = libraryId
it.title = title
it.last_chapter_read = lastChapterRead.toFloat()
@@ -33,8 +33,8 @@ fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? {
return Track(
id = trackId,
mangaId = manga_id,
- syncId = sync_id.toLong(),
- remoteId = media_id,
+ trackerId = tracker_id.toLong(),
+ remoteId = remote_id,
libraryId = library_id,
title = title,
lastChapterRead = last_chapter_read.toDouble(),
diff --git a/app/src/main/java/eu/kanade/domain/track/service/TrackPreferences.kt b/app/src/main/java/eu/kanade/domain/track/service/TrackPreferences.kt
index c7fb47581..8ed234b90 100644
--- a/app/src/main/java/eu/kanade/domain/track/service/TrackPreferences.kt
+++ b/app/src/main/java/eu/kanade/domain/track/service/TrackPreferences.kt
@@ -9,26 +9,24 @@ class TrackPreferences(
private val preferenceStore: PreferenceStore,
) {
- fun trackUsername(sync: Tracker) = preferenceStore.getString(trackUsername(sync.id), "")
+ fun trackUsername(tracker: Tracker) = preferenceStore.getString(
+ Preference.privateKey("pref_mangasync_username_${tracker.id}"),
+ "",
+ )
- fun trackPassword(sync: Tracker) = preferenceStore.getString(trackPassword(sync.id), "")
+ fun trackPassword(tracker: Tracker) = preferenceStore.getString(
+ Preference.privateKey("pref_mangasync_password_${tracker.id}"),
+ "",
+ )
- fun setCredentials(sync: Tracker, username: String, password: String) {
- trackUsername(sync).set(username)
- trackPassword(sync).set(password)
+ fun setCredentials(tracker: Tracker, username: String, password: String) {
+ trackUsername(tracker).set(username)
+ trackPassword(tracker).set(password)
}
- fun trackToken(sync: Tracker) = preferenceStore.getString(trackToken(sync.id), "")
+ fun trackToken(tracker: Tracker) = preferenceStore.getString(Preference.privateKey("track_token_${tracker.id}"), "")
fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10)
fun autoUpdateTrack() = preferenceStore.getBoolean("pref_auto_update_manga_sync_key", true)
-
- companion object {
- fun trackUsername(syncId: Long) = Preference.privateKey("pref_mangasync_username_$syncId")
-
- private fun trackPassword(syncId: Long) = Preference.privateKey("pref_mangasync_password_$syncId")
-
- private fun trackToken(syncId: Long) = Preference.privateKey("track_token_$syncId")
- }
}
diff --git a/app/src/main/java/eu/kanade/presentation/category/components/CategoryDialogs.kt b/app/src/main/java/eu/kanade/presentation/category/components/CategoryDialogs.kt
index 0441d014a..d7a484c6d 100644
--- a/app/src/main/java/eu/kanade/presentation/category/components/CategoryDialogs.kt
+++ b/app/src/main/java/eu/kanade/presentation/category/components/CategoryDialogs.kt
@@ -27,6 +27,7 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import eu.kanade.core.preference.asToggleableState
import eu.kanade.presentation.category.visualName
+import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.delay
import tachiyomi.core.preference.CheckboxState
import tachiyomi.domain.category.model.Category
@@ -39,7 +40,7 @@ import kotlin.time.Duration.Companion.seconds
fun CategoryCreateDialog(
onDismissRequest: () -> Unit,
onCreate: (String) -> Unit,
- categories: List,
+ categories: ImmutableList,
) {
var name by remember { mutableStateOf("") }
@@ -98,7 +99,7 @@ fun CategoryCreateDialog(
fun CategoryRenameDialog(
onDismissRequest: () -> Unit,
onRename: (String) -> Unit,
- categories: List,
+ categories: ImmutableList,
category: Category,
) {
var name by remember { mutableStateOf(category.name) }
diff --git a/app/src/main/java/eu/kanade/presentation/category/components/CategoryFloatingActionButton.kt b/app/src/main/java/eu/kanade/presentation/category/components/CategoryFloatingActionButton.kt
index 2aaf81efd..58d7f163a 100644
--- a/app/src/main/java/eu/kanade/presentation/category/components/CategoryFloatingActionButton.kt
+++ b/app/src/main/java/eu/kanade/presentation/category/components/CategoryFloatingActionButton.kt
@@ -6,6 +6,7 @@ import androidx.compose.material.icons.outlined.Add
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
import tachiyomi.presentation.core.i18n.stringResource
@@ -16,11 +17,13 @@ import tachiyomi.presentation.core.util.isScrollingUp
fun CategoryFloatingActionButton(
lazyListState: LazyListState,
onCreate: () -> Unit,
+ modifier: Modifier = Modifier,
) {
ExtendedFloatingActionButton(
text = { Text(text = stringResource(MR.strings.action_add)) },
icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = null) },
onClick = onCreate,
expanded = lazyListState.isScrollingUp() || lazyListState.isScrolledToEnd(),
+ modifier = modifier,
)
}
diff --git a/app/src/main/java/eu/kanade/presentation/components/DownloadDropdownMenu.kt b/app/src/main/java/eu/kanade/presentation/components/DownloadDropdownMenu.kt
index 36449f13c..a116c3ee1 100644
--- a/app/src/main/java/eu/kanade/presentation/components/DownloadDropdownMenu.kt
+++ b/app/src/main/java/eu/kanade/presentation/components/DownloadDropdownMenu.kt
@@ -3,7 +3,9 @@ package eu.kanade.presentation.components
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
import eu.kanade.presentation.manga.DownloadAction
+import kotlinx.collections.immutable.persistentListOf
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.pluralStringResource
import tachiyomi.presentation.core.i18n.stringResource
@@ -13,18 +15,22 @@ fun DownloadDropdownMenu(
expanded: Boolean,
onDismissRequest: () -> Unit,
onDownloadClicked: (DownloadAction) -> Unit,
+ modifier: Modifier = Modifier,
) {
+ val options = persistentListOf(
+ DownloadAction.NEXT_1_CHAPTER to pluralStringResource(MR.plurals.download_amount, 1, 1),
+ DownloadAction.NEXT_5_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 5, 5),
+ DownloadAction.NEXT_10_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 10, 10),
+ DownloadAction.NEXT_25_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 25, 25),
+ DownloadAction.UNREAD_CHAPTERS to stringResource(MR.strings.download_unread),
+ )
+
DropdownMenu(
expanded = expanded,
onDismissRequest = onDismissRequest,
+ modifier = modifier,
) {
- listOfNotNull(
- DownloadAction.NEXT_1_CHAPTER to pluralStringResource(MR.plurals.download_amount, 1, 1),
- DownloadAction.NEXT_5_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 5, 5),
- DownloadAction.NEXT_10_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 10, 10),
- DownloadAction.NEXT_25_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 25, 25),
- DownloadAction.UNREAD_CHAPTERS to stringResource(MR.strings.download_unread),
- ).map { (downloadAction, string) ->
+ options.map { (downloadAction, string) ->
DropdownMenuItem(
text = { Text(text = string) },
onClick = {
diff --git a/app/src/main/java/eu/kanade/presentation/components/DropdownMenu.kt b/app/src/main/java/eu/kanade/presentation/components/DropdownMenu.kt
index 531ebd406..c1fcbf524 100644
--- a/app/src/main/java/eu/kanade/presentation/components/DropdownMenu.kt
+++ b/app/src/main/java/eu/kanade/presentation/components/DropdownMenu.kt
@@ -1,7 +1,10 @@
package eu.kanade.presentation.components
+import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.sizeIn
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
import androidx.compose.material.icons.outlined.RadioButtonChecked
@@ -22,12 +25,17 @@ import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import androidx.compose.material3.DropdownMenu as ComposeDropdownMenu
+/**
+ * DropdownMenu but overlaps anchor and has width constraints to better
+ * match non-Compose implementation.
+ */
@Composable
fun DropdownMenu(
expanded: Boolean,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
offset: DpOffset = DpOffset(8.dp, (-56).dp),
+ scrollState: ScrollState = rememberScrollState(),
properties: PopupProperties = PopupProperties(focusable = true),
content: @Composable ColumnScope.() -> Unit,
) {
@@ -36,6 +44,7 @@ fun DropdownMenu(
onDismissRequest = onDismissRequest,
modifier = modifier.sizeIn(minWidth = 196.dp, maxWidth = 196.dp),
offset = offset,
+ scrollState = scrollState,
properties = properties,
content = content,
)
@@ -45,6 +54,7 @@ fun DropdownMenu(
fun RadioMenuItem(
text: @Composable () -> Unit,
isChecked: Boolean,
+ modifier: Modifier = Modifier,
onClick: () -> Unit,
) {
DropdownMenuItem(
@@ -64,6 +74,7 @@ fun RadioMenuItem(
)
}
},
+ modifier = modifier,
)
}
@@ -71,25 +82,29 @@ fun RadioMenuItem(
fun NestedMenuItem(
text: @Composable () -> Unit,
children: @Composable ColumnScope.(() -> Unit) -> Unit,
+ modifier: Modifier = Modifier,
) {
var nestedExpanded by remember { mutableStateOf(false) }
val closeMenu = { nestedExpanded = false }
- DropdownMenuItem(
- text = text,
- onClick = { nestedExpanded = true },
- trailingIcon = {
- Icon(
- imageVector = Icons.AutoMirrored.Outlined.ArrowRight,
- contentDescription = null,
- )
- },
- )
+ Box {
+ DropdownMenuItem(
+ text = text,
+ onClick = { nestedExpanded = true },
+ trailingIcon = {
+ Icon(
+ imageVector = Icons.AutoMirrored.Outlined.ArrowRight,
+ contentDescription = null,
+ )
+ },
+ )
- DropdownMenu(
- expanded = nestedExpanded,
- onDismissRequest = closeMenu,
- ) {
- children(closeMenu)
+ DropdownMenu(
+ expanded = nestedExpanded,
+ onDismissRequest = closeMenu,
+ modifier = modifier,
+ ) {
+ children(closeMenu)
+ }
}
}
diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt
index 18fbbc3f5..c3ce971e6 100644
--- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt
+++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt
@@ -77,6 +77,7 @@ import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.isScrolledToEnd
import tachiyomi.presentation.core.util.isScrollingUp
+import tachiyomi.source.local.isLocal
import java.text.DateFormat
import java.util.Date
@@ -718,13 +719,13 @@ fun MangaScreenLargeImpl(
@Composable
private fun SharedMangaBottomActionMenu(
selected: List,
- modifier: Modifier = Modifier,
onMultiBookmarkClicked: (List, bookmarked: Boolean) -> Unit,
onMultiMarkAsReadClicked: (List, markAsRead: Boolean) -> Unit,
onMarkPreviousAsReadClicked: (Chapter) -> Unit,
onDownloadChapter: ((List, ChapterDownloadAction) -> Unit)?,
onMultiDeleteClicked: (List) -> Unit,
fillFraction: Float,
+ modifier: Modifier = Modifier,
) {
MangaBottomActionMenu(
visible = selected.isNotEmpty(),
@@ -752,7 +753,7 @@ private fun SharedMangaBottomActionMenu(
onDeleteClicked = {
onMultiDeleteClicked(selected.fastMap { it.chapter })
}.takeIf {
- onDownloadChapter != null && selected.fastAny { it.downloadState == Download.State.DOWNLOADED }
+ selected.fastAny { it.downloadState == Download.State.DOWNLOADED }
},
)
}
@@ -818,7 +819,7 @@ private fun LazyListScope.sharedChapterItems(
read = item.chapter.read,
bookmark = item.chapter.bookmark,
selected = item.selected,
- downloadIndicatorEnabled = !isAnyChapterSelected,
+ downloadIndicatorEnabled = !isAnyChapterSelected && !manga.isLocal(),
downloadStateProvider = { item.downloadState },
downloadProgressProvider = { item.downloadProgress },
chapterSwipeStartAction = chapterSwipeStartAction,
diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/ChapterDownloadIndicator.kt b/app/src/main/java/eu/kanade/presentation/manga/components/ChapterDownloadIndicator.kt
index 7e1d0d60b..162aa92a3 100644
--- a/app/src/main/java/eu/kanade/presentation/manga/components/ChapterDownloadIndicator.kt
+++ b/app/src/main/java/eu/kanade/presentation/manga/components/ChapterDownloadIndicator.kt
@@ -49,10 +49,10 @@ enum class ChapterDownloadAction {
@Composable
fun ChapterDownloadIndicator(
enabled: Boolean,
- modifier: Modifier = Modifier,
downloadStateProvider: () -> Download.State,
downloadProgressProvider: () -> Int,
onClick: (ChapterDownloadAction) -> Unit,
+ modifier: Modifier = Modifier,
) {
when (val downloadState = downloadStateProvider()) {
Download.State.NOT_DOWNLOADED -> NotDownloadedIndicator(
@@ -109,10 +109,10 @@ private fun NotDownloadedIndicator(
@Composable
private fun DownloadingIndicator(
enabled: Boolean,
- modifier: Modifier = Modifier,
downloadState: Download.State,
downloadProgressProvider: () -> Int,
onClick: (ChapterDownloadAction) -> Unit,
+ modifier: Modifier = Modifier,
) {
var isMenuExpanded by remember { mutableStateOf(false) }
Box(
diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaBottomActionMenu.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaBottomActionMenu.kt
index 0f2701b7f..905a457a6 100644
--- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaBottomActionMenu.kt
+++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaBottomActionMenu.kt
@@ -182,7 +182,10 @@ private fun RowScope.Button(
onClick: () -> Unit,
content: (@Composable () -> Unit)? = null,
) {
- val animatedWeight by animateFloatAsState(if (toConfirm) 2f else 1f)
+ val animatedWeight by animateFloatAsState(
+ targetValue = if (toConfirm) 2f else 1f,
+ label = "weight",
+ )
Column(
modifier = Modifier
.size(48.dp)
diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaChapterListItem.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaChapterListItem.kt
index b14d2ed14..9c9f07c1a 100644
--- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaChapterListItem.kt
+++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaChapterListItem.kt
@@ -203,15 +203,13 @@ fun MangaChapterListItem(
}
}
- if (onDownloadClick != null) {
- ChapterDownloadIndicator(
- enabled = downloadIndicatorEnabled,
- modifier = Modifier.padding(start = 4.dp),
- downloadStateProvider = downloadStateProvider,
- downloadProgressProvider = downloadProgressProvider,
- onClick = onDownloadClick,
- )
- }
+ ChapterDownloadIndicator(
+ enabled = downloadIndicatorEnabled,
+ modifier = Modifier.padding(start = 4.dp),
+ downloadStateProvider = downloadStateProvider,
+ downloadProgressProvider = downloadProgressProvider,
+ onClick = { onDownloadClick?.invoke(it) },
+ )
}
}
}
diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaToolbar.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaToolbar.kt
index 12c14ce78..4415bbf27 100644
--- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaToolbar.kt
+++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaToolbar.kt
@@ -20,7 +20,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.alpha
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.AppBar
diff --git a/app/src/main/java/eu/kanade/presentation/more/onboarding/GuidesStep.kt b/app/src/main/java/eu/kanade/presentation/more/onboarding/GuidesStep.kt
index 77e0e7b88..d1ac967fa 100644
--- a/app/src/main/java/eu/kanade/presentation/more/onboarding/GuidesStep.kt
+++ b/app/src/main/java/eu/kanade/presentation/more/onboarding/GuidesStep.kt
@@ -20,6 +20,7 @@ import tachiyomi.presentation.core.i18n.stringResource
internal class GuidesStep(
private val onRestoreBackup: () -> Unit,
) : OnboardingStep {
+
override val isComplete: Boolean = true
@Composable
diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/Preference.kt b/app/src/main/java/eu/kanade/presentation/more/settings/Preference.kt
index c8f99b593..fd8d12067 100644
--- a/app/src/main/java/eu/kanade/presentation/more/settings/Preference.kt
+++ b/app/src/main/java/eu/kanade/presentation/more/settings/Preference.kt
@@ -4,6 +4,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.vector.ImageVector
import eu.kanade.tachiyomi.data.track.Tracker
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.ImmutableMap
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.core.preference.Preference as PreferenceData
@@ -64,20 +66,20 @@ sealed class Preference {
val pref: PreferenceData,
override val title: String,
override val subtitle: String? = "%s",
- val subtitleProvider: @Composable (value: T, entries: Map) -> String? =
+ val subtitleProvider: @Composable (value: T, entries: ImmutableMap) -> String? =
{ v, e -> subtitle?.format(e[v]) },
override val icon: ImageVector? = null,
override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: T) -> Boolean = { true },
- val entries: Map,
+ val entries: ImmutableMap,
) : PreferenceItem() {
internal fun internalSet(newValue: Any) = pref.set(newValue as T)
internal suspend fun internalOnValueChanged(newValue: Any) = onValueChanged(newValue as T)
@Composable
- internal fun internalSubtitleProvider(value: Any?, entries: Map) =
- subtitleProvider(value as T, entries as Map)
+ internal fun internalSubtitleProvider(value: Any?, entries: ImmutableMap) =
+ subtitleProvider(value as T, entries as ImmutableMap)
}
/**
@@ -87,13 +89,13 @@ sealed class Preference {
val value: String,
override val title: String,
override val subtitle: String? = "%s",
- val subtitleProvider: @Composable (value: String, entries: Map) -> String? =
+ val subtitleProvider: @Composable (value: String, entries: ImmutableMap) -> String? =
{ v, e -> subtitle?.format(e[v]) },
override val icon: ImageVector? = null,
override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: String) -> Boolean = { true },
- val entries: Map,
+ val entries: ImmutableMap,
) : PreferenceItem()
/**
@@ -104,7 +106,10 @@ sealed class Preference {
val pref: PreferenceData>,
override val title: String,
override val subtitle: String? = "%s",
- val subtitleProvider: @Composable (value: Set, entries: Map) -> String? = { v, e ->
+ val subtitleProvider: @Composable (
+ value: Set,
+ entries: ImmutableMap,
+ ) -> String? = { v, e ->
val combined = remember(v) {
v.map { e[it] }
.takeIf { it.isNotEmpty() }
@@ -116,7 +121,7 @@ sealed class Preference {
override val enabled: Boolean = true,
override val onValueChanged: suspend (newValue: Set) -> Boolean = { true },
- val entries: Map,
+ val entries: ImmutableMap,
) : PreferenceItem>()
/**
@@ -170,6 +175,6 @@ sealed class Preference {
override val title: String,
override val enabled: Boolean = true,
- val preferenceItems: List>,
+ val preferenceItems: ImmutableList>,
) : Preference()
}
diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceItem.kt b/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceItem.kt
index b68f17fcd..b22e69323 100644
--- a/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceItem.kt
+++ b/app/src/main/java/eu/kanade/presentation/more/settings/PreferenceItem.kt
@@ -21,7 +21,6 @@ import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
import eu.kanade.presentation.more.settings.widget.TrackingPreferenceWidget
import kotlinx.coroutines.launch
-import tachiyomi.core.preference.PreferenceStore
import tachiyomi.presentation.core.components.SliderItem
import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.Injekt
@@ -157,8 +156,8 @@ internal fun PreferenceItem(
)
}
is Preference.PreferenceItem.TrackerPreference -> {
- val uName by Injekt.get()
- .getString(TrackPreferences.trackUsername(item.tracker.id))
+ val uName by Injekt.get()
+ .trackUsername(item.tracker)
.collectAsState()
item.tracker.run {
TrackingPreferenceWidget(
diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/Commons.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/Commons.kt
index dc128a7aa..4c44abce3 100644
--- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/Commons.kt
+++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/Commons.kt
@@ -11,7 +11,6 @@ import tachiyomi.presentation.core.i18n.stringResource
/**
* Returns a string of categories name for settings subtitle
*/
-
@ReadOnlyComposable
@Composable
fun getCategoriesLabel(
diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt
index 56f606567..921fd4ae4 100644
--- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt
+++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt
@@ -51,6 +51,9 @@ import eu.kanade.tachiyomi.util.system.isShizukuInstalled
import eu.kanade.tachiyomi.util.system.powerManager
import eu.kanade.tachiyomi.util.system.setDefaultSettings
import eu.kanade.tachiyomi.util.system.toast
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.persistentMapOf
+import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.launch
import logcat.LogPriority
import okhttp3.Headers
@@ -149,7 +152,7 @@ object SettingsAdvancedScreen : SearchableSettings {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.label_background_activity),
- preferenceItems = listOf(
+ preferenceItems = persistentListOf(
Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.pref_disable_battery_optimization),
subtitle = stringResource(MR.strings.pref_disable_battery_optimization_summary),
@@ -188,7 +191,7 @@ object SettingsAdvancedScreen : SearchableSettings {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.label_data),
- preferenceItems = listOf(
+ preferenceItems = persistentListOf(
Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.pref_invalidate_download_cache),
subtitle = stringResource(MR.strings.pref_invalidate_download_cache_summary),
@@ -218,7 +221,7 @@ object SettingsAdvancedScreen : SearchableSettings {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.label_network),
- preferenceItems = listOf(
+ preferenceItems = persistentListOf(
Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.pref_clear_cookies),
onClick = {
@@ -249,7 +252,7 @@ object SettingsAdvancedScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference(
pref = networkPreferences.dohProvider(),
title = stringResource(MR.strings.pref_dns_over_https),
- entries = mapOf(
+ entries = persistentMapOf(
-1 to stringResource(MR.strings.disabled),
PREF_DOH_CLOUDFLARE to "Cloudflare",
PREF_DOH_GOOGLE to "Google",
@@ -302,7 +305,7 @@ object SettingsAdvancedScreen : SearchableSettings {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.label_library),
- preferenceItems = listOf(
+ preferenceItems = persistentListOf(
Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.pref_refresh_library_covers),
onClick = { MetadataUpdateJob.startNow(context) },
@@ -362,12 +365,13 @@ object SettingsAdvancedScreen : SearchableSettings {
}
return Preference.PreferenceGroup(
title = stringResource(MR.strings.label_extensions),
- preferenceItems = listOf(
+ preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = extensionInstallerPref,
title = stringResource(MR.strings.ext_installer_pref),
entries = extensionInstallerPref.entries
- .associateWith { stringResource(it.titleRes) },
+ .associateWith { stringResource(it.titleRes) }
+ .toImmutableMap(),
onValueChanged = {
if (it == BasePreferences.ExtensionInstaller.SHIZUKU &&
!context.isShizukuInstalled
diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt
index 8523de930..365e86b6f 100644
--- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt
+++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt
@@ -24,6 +24,9 @@ import eu.kanade.presentation.more.settings.widget.AppThemePreferenceWidget
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.toast
+import kotlinx.collections.immutable.ImmutableMap
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.toImmutableMap
import org.xmlpull.v1.XmlPullParser
import tachiyomi.core.i18n.stringResource
import tachiyomi.i18n.MR
@@ -66,7 +69,7 @@ object SettingsAppearanceScreen : SearchableSettings {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_theme),
- preferenceItems = listOf(
+ preferenceItems = persistentListOf(
Preference.PreferenceItem.CustomPreference(
title = stringResource(MR.strings.pref_app_theme),
) {
@@ -127,7 +130,7 @@ object SettingsAppearanceScreen : SearchableSettings {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_display),
- preferenceItems = listOf(
+ preferenceItems = persistentListOf(
Preference.PreferenceItem.BasicListPreference(
value = currentLanguage,
title = stringResource(MR.strings.pref_app_language),
@@ -140,7 +143,9 @@ object SettingsAppearanceScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference(
pref = uiPreferences.tabletUiMode(),
title = stringResource(MR.strings.pref_tablet_ui_mode),
- entries = TabletUiMode.entries.associateWith { stringResource(it.titleRes) },
+ entries = TabletUiMode.entries
+ .associateWith { stringResource(it.titleRes) }
+ .toImmutableMap(),
onValueChanged = {
context.toast(MR.strings.requires_app_restart)
true
@@ -153,7 +158,8 @@ object SettingsAppearanceScreen : SearchableSettings {
.associateWith {
val formattedDate = UiPreferences.dateFormat(it).format(now)
"${it.ifEmpty { stringResource(MR.strings.label_default) }} ($formattedDate)"
- },
+ }
+ .toImmutableMap(),
),
Preference.PreferenceItem.SwitchPreference(
pref = uiPreferences.relativeTime(),
@@ -167,7 +173,7 @@ object SettingsAppearanceScreen : SearchableSettings {
),
)
}
- private fun getLangs(context: Context): Map {
+ private fun getLangs(context: Context): ImmutableMap {
val langs = mutableListOf>()
val parser = context.resources.getXml(R.xml.locales_config)
var eventType = parser.eventType
@@ -189,7 +195,7 @@ object SettingsAppearanceScreen : SearchableSettings {
langs.sortBy { it.second }
langs.add(0, Pair("", context.stringResource(MR.strings.label_default)))
- return langs.toMap()
+ return langs.toMap().toImmutableMap()
}
}
diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBrowseScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBrowseScreen.kt
index 4d9aa2796..61c1db21e 100644
--- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBrowseScreen.kt
+++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBrowseScreen.kt
@@ -8,6 +8,7 @@ import androidx.fragment.app.FragmentActivity
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
+import kotlinx.collections.immutable.persistentListOf
import tachiyomi.core.i18n.stringResource
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
@@ -27,7 +28,7 @@ object SettingsBrowseScreen : SearchableSettings {
return listOf(
Preference.PreferenceGroup(
title = stringResource(MR.strings.label_sources),
- preferenceItems = listOf(
+ preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = sourcePreferences.hideInLibraryItems(),
title = stringResource(MR.strings.pref_hide_in_library_items),
@@ -36,7 +37,7 @@ object SettingsBrowseScreen : SearchableSettings {
),
Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_nsfw_content),
- preferenceItems = listOf(
+ preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = sourcePreferences.showNsfwSource(),
title = stringResource(MR.strings.pref_show_nsfw_source),
diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt
index 5d382d3f1..a3fa1183c 100644
--- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt
+++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt
@@ -1,21 +1,17 @@
package eu.kanade.presentation.more.settings.screen
import android.content.ActivityNotFoundException
-import android.content.Context
import android.content.Intent
import android.net.Uri
-import android.os.Environment
-import android.text.format.Formatter
-import android.widget.Toast
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.MultiChoiceSegmentedButtonRow
+import androidx.compose.material3.SegmentedButton
+import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@@ -35,21 +31,20 @@ import com.hippo.unifile.UniFile
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen
import eu.kanade.presentation.more.settings.screen.data.SyncOptionsScreen
+import eu.kanade.presentation.more.settings.screen.data.RestoreBackupScreen
+import eu.kanade.presentation.more.settings.screen.data.StorageInfo
import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
import eu.kanade.presentation.util.relativeTimeSpanString
-import eu.kanade.tachiyomi.data.backup.BackupFileValidator
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
-import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.sync.SyncDataJob
import eu.kanade.tachiyomi.data.sync.SyncManager
import eu.kanade.tachiyomi.data.sync.service.GoogleDriveService
import eu.kanade.tachiyomi.data.sync.service.GoogleDriveSyncService
-import eu.kanade.tachiyomi.util.storage.DiskUtil
-import eu.kanade.tachiyomi.util.system.DeviceUtil
-import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.system.toast
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.persistentMapOf
import kotlinx.coroutines.launch
import logcat.LogPriority
import tachiyomi.core.i18n.stringResource
@@ -68,6 +63,8 @@ import uy.kohesive.injekt.api.get
object SettingsDataScreen : SearchableSettings {
+ val restorePreferenceKeyString = MR.strings.label_backup
+
@ReadOnlyComposable
@Composable
override fun getTitleRes() = MR.strings.label_data_storage
@@ -80,7 +77,7 @@ object SettingsDataScreen : SearchableSettings {
val syncPreferences = remember { Injekt.get() }
val syncService by syncPreferences.syncService().collectAsState()
- return listOf(
+ return persistentListOf(
getStorageLocationPref(storagePreferences = storagePreferences),
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.pref_storage_location_info)),
@@ -151,20 +148,48 @@ object SettingsDataScreen : SearchableSettings {
@Composable
private fun getBackupAndRestoreGroup(backupPreferences: BackupPreferences): Preference.PreferenceGroup {
val context = LocalContext.current
+ val navigator = LocalNavigator.currentOrThrow
+
val lastAutoBackup by backupPreferences.lastAutoBackupTimestamp().collectAsState()
return Preference.PreferenceGroup(
title = stringResource(MR.strings.label_backup),
- preferenceItems = listOf(
+ preferenceItems = persistentListOf(
// Manual actions
- getCreateBackupPref(),
- getRestoreBackupPref(),
+ Preference.PreferenceItem.CustomPreference(
+ title = stringResource(restorePreferenceKeyString),
+ ) {
+ BasePreferenceWidget(
+ subcomponent = {
+ MultiChoiceSegmentedButtonRow(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = PrefsHorizontalPadding),
+ ) {
+ SegmentedButton(
+ checked = false,
+ onCheckedChange = { navigator.push(CreateBackupScreen()) },
+ shape = SegmentedButtonDefaults.itemShape(0, 2),
+ ) {
+ Text(stringResource(MR.strings.pref_create_backup))
+ }
+ SegmentedButton(
+ checked = false,
+ onCheckedChange = { navigator.push(RestoreBackupScreen()) },
+ shape = SegmentedButtonDefaults.itemShape(1, 2),
+ ) {
+ Text(stringResource(MR.strings.pref_restore_backup))
+ }
+ }
+ },
+ )
+ },
// Automatic backups
Preference.PreferenceItem.ListPreference(
pref = backupPreferences.backupInterval(),
title = stringResource(MR.strings.pref_backup_interval),
- entries = mapOf(
+ entries = persistentMapOf(
0 to stringResource(MR.strings.off),
6 to stringResource(MR.strings.update_6hour),
12 to stringResource(MR.strings.update_12hour),
@@ -185,142 +210,10 @@ object SettingsDataScreen : SearchableSettings {
)
}
- @Composable
- private fun getCreateBackupPref(): Preference.PreferenceItem.TextPreference {
- val navigator = LocalNavigator.currentOrThrow
- return Preference.PreferenceItem.TextPreference(
- title = stringResource(MR.strings.pref_create_backup),
- subtitle = stringResource(MR.strings.pref_create_backup_summ),
- onClick = { navigator.push(CreateBackupScreen()) },
- )
- }
-
- @Composable
- private fun getRestoreBackupPref(): Preference.PreferenceItem.TextPreference {
- val context = LocalContext.current
- var error by remember { mutableStateOf(null) }
- if (error != null) {
- val onDismissRequest = { error = null }
- when (val err = error) {
- is InvalidRestore -> {
- AlertDialog(
- onDismissRequest = onDismissRequest,
- title = { Text(text = stringResource(MR.strings.invalid_backup_file)) },
- text = { Text(text = listOfNotNull(err.uri, err.message).joinToString("\n\n")) },
- dismissButton = {
- TextButton(
- onClick = {
- context.copyToClipboard(err.message, err.message)
- onDismissRequest()
- },
- ) {
- Text(text = stringResource(MR.strings.action_copy_to_clipboard))
- }
- },
- confirmButton = {
- TextButton(onClick = onDismissRequest) {
- Text(text = stringResource(MR.strings.action_ok))
- }
- },
- )
- }
- is MissingRestoreComponents -> {
- AlertDialog(
- onDismissRequest = onDismissRequest,
- title = { Text(text = stringResource(MR.strings.pref_restore_backup)) },
- text = {
- Column(
- modifier = Modifier.verticalScroll(rememberScrollState()),
- ) {
- val msg = buildString {
- append(stringResource(MR.strings.backup_restore_content_full))
- if (err.sources.isNotEmpty()) {
- append("\n\n").append(stringResource(MR.strings.backup_restore_missing_sources))
- err.sources.joinTo(
- this,
- separator = "\n- ",
- prefix = "\n- ",
- )
- }
- if (err.trackers.isNotEmpty()) {
- append(
- "\n\n",
- ).append(stringResource(MR.strings.backup_restore_missing_trackers))
- err.trackers.joinTo(
- this,
- separator = "\n- ",
- prefix = "\n- ",
- )
- }
- }
- Text(text = msg)
- }
- },
- confirmButton = {
- TextButton(
- onClick = {
- BackupRestoreJob.start(context, err.uri)
- onDismissRequest()
- },
- ) {
- Text(text = stringResource(MR.strings.action_restore))
- }
- },
- )
- }
- else -> error = null // 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().validate(context, it)
- } catch (e: Exception) {
- error = InvalidRestore(it, e.message.toString())
- return@rememberLauncherForActivityResult
- }
-
- if (results.missingSources.isEmpty() && results.missingTrackers.isEmpty()) {
- BackupRestoreJob.start(context, it)
- return@rememberLauncherForActivityResult
- }
-
- error = MissingRestoreComponents(it, results.missingSources, results.missingTrackers)
- }
-
- return Preference.PreferenceItem.TextPreference(
- title = stringResource(MR.strings.pref_restore_backup),
- subtitle = stringResource(MR.strings.pref_restore_backup_summ),
- onClick = {
- if (!BackupRestoreJob.isRunning(context)) {
- if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
- context.toast(MR.strings.restore_miui_warning, Toast.LENGTH_LONG)
- }
- // no need to catch because it's wrapped with a chooser
- chooseBackup.launch("*/*")
- } else {
- context.toast(MR.strings.restore_in_progress)
- }
- },
- )
- }
-
@Composable
private fun getDataGroup(): Preference.PreferenceGroup {
- val scope = rememberCoroutineScope()
val context = LocalContext.current
+ val scope = rememberCoroutineScope()
val libraryPreferences = remember { Injekt.get() }
val chapterCache = remember { Injekt.get() }
@@ -328,9 +221,19 @@ object SettingsDataScreen : SearchableSettings {
val cacheReadableSize = remember(cacheReadableSizeSema) { chapterCache.readableSize }
return Preference.PreferenceGroup(
- title = stringResource(MR.strings.label_data),
- preferenceItems = listOf(
- getStorageInfoPref(cacheReadableSize),
+ title = stringResource(MR.strings.pref_storage_usage),
+ preferenceItems = persistentListOf(
+ Preference.PreferenceItem.CustomPreference(
+ title = stringResource(MR.strings.pref_storage_usage),
+ ) {
+ BasePreferenceWidget(
+ subcomponent = {
+ StorageInfo(
+ modifier = Modifier.padding(horizontal = PrefsHorizontalPadding),
+ )
+ },
+ )
+ },
Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.pref_clear_chapter_cache),
@@ -358,43 +261,16 @@ object SettingsDataScreen : SearchableSettings {
)
}
- @Composable
- fun getStorageInfoPref(
- chapterCacheReadableSize: String,
- ): Preference.PreferenceItem.CustomPreference {
- val context = LocalContext.current
- val available = remember {
- Formatter.formatFileSize(context, DiskUtil.getAvailableStorageSpace(Environment.getDataDirectory()))
- }
- val total = remember {
- Formatter.formatFileSize(context, DiskUtil.getTotalStorageSpace(Environment.getDataDirectory()))
- }
-
- return Preference.PreferenceItem.CustomPreference(
- title = stringResource(MR.strings.pref_storage_usage),
- ) {
- BasePreferenceWidget(
- title = stringResource(MR.strings.pref_storage_usage),
- subcomponent = {
- // TODO: downloads, SD cards, bar representation?, i18n
- Box(modifier = Modifier.padding(horizontal = PrefsHorizontalPadding)) {
- Text(text = "Available: $available / $total (chapter cache: $chapterCacheReadableSize)")
- }
- },
- )
- }
- }
-
@Composable
private fun getSyncPreferences(syncPreferences: SyncPreferences, syncService: Int): List {
return listOf(
Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_sync_service_category),
- preferenceItems = listOf(
+ preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = syncPreferences.syncService(),
title = stringResource(MR.strings.pref_sync_service),
- entries = mapOf(
+ entries = persistentMapOf(
SyncManager.SyncService.NONE.value to stringResource(MR.strings.off),
SyncManager.SyncService.SYNCYOMI.value to stringResource(MR.strings.syncyomi),
SyncManager.SyncService.GOOGLE_DRIVE.value to stringResource(MR.strings.google_drive),
@@ -405,162 +281,161 @@ object SettingsDataScreen : SearchableSettings {
),
) + getSyncServicePreferences(syncPreferences, syncService)
}
-}
-@Composable
-private fun getSyncServicePreferences(syncPreferences: SyncPreferences, syncService: Int): List {
- val syncServiceType = SyncManager.SyncService.fromInt(syncService)
+ @Composable
+ private fun getSyncServicePreferences(syncPreferences: SyncPreferences, syncService: Int): List {
+ val syncServiceType = SyncManager.SyncService.fromInt(syncService)
- val basePreferences = getBasePreferences(syncServiceType, syncPreferences)
+ val basePreferences = getBasePreferences(syncServiceType, syncPreferences)
- return if (syncServiceType != SyncManager.SyncService.NONE) {
- basePreferences + getAdditionalPreferences(syncPreferences)
- } else {
- basePreferences
+ return if (syncServiceType != SyncManager.SyncService.NONE) {
+ basePreferences + getAdditionalPreferences(syncPreferences)
+ } else {
+ basePreferences
+ }
}
-}
-@Composable
-private fun getBasePreferences(
- syncServiceType: SyncManager.SyncService,
- syncPreferences: SyncPreferences,
-): List {
- return when (syncServiceType) {
- SyncManager.SyncService.NONE -> emptyList()
- SyncManager.SyncService.SYNCYOMI -> getSelfHostPreferences(syncPreferences)
- SyncManager.SyncService.GOOGLE_DRIVE -> getGoogleDrivePreferences()
+ @Composable
+ private fun getBasePreferences(
+ syncServiceType: SyncManager.SyncService,
+ syncPreferences: SyncPreferences,
+ ): List {
+ return when (syncServiceType) {
+ SyncManager.SyncService.NONE -> emptyList()
+ SyncManager.SyncService.SYNCYOMI -> getSelfHostPreferences(syncPreferences)
+ SyncManager.SyncService.GOOGLE_DRIVE -> getGoogleDrivePreferences()
+ }
}
-}
-@Composable
-private fun getAdditionalPreferences(syncPreferences: SyncPreferences): List {
- return listOf(getSyncNowPref(), getAutomaticSyncGroup(syncPreferences))
-}
+ @Composable
+ private fun getAdditionalPreferences(syncPreferences: SyncPreferences): List {
+ return listOf(getSyncNowPref(), getAutomaticSyncGroup(syncPreferences))
+ }
-@Composable
-private fun getGoogleDrivePreferences(): List {
- val context = LocalContext.current
- val googleDriveSync = Injekt.get()
- return listOf(
- Preference.PreferenceItem.TextPreference(
- title = stringResource(MR.strings.pref_google_drive_sign_in),
- onClick = {
- val intent = googleDriveSync.getSignInIntent()
- context.startActivity(intent)
- },
- ),
- getGoogleDrivePurge(),
- )
-}
-
-@Composable
-private fun getGoogleDrivePurge(): Preference.PreferenceItem.TextPreference {
- val scope = rememberCoroutineScope()
- val context = LocalContext.current
- val googleDriveSync = remember { GoogleDriveSyncService(context) }
- var showPurgeDialog by remember { mutableStateOf(false) }
-
- if (showPurgeDialog) {
- PurgeConfirmationDialog(
- onConfirm = {
- showPurgeDialog = false
- scope.launch {
- val result = googleDriveSync.deleteSyncDataFromGoogleDrive()
- when (result) {
- GoogleDriveSyncService.DeleteSyncDataStatus.NOT_INITIALIZED -> context.toast(
- MR.strings.google_drive_not_signed_in,
- )
- GoogleDriveSyncService.DeleteSyncDataStatus.NO_FILES -> context.toast(
- MR.strings.google_drive_sync_data_not_found,
- )
- GoogleDriveSyncService.DeleteSyncDataStatus.SUCCESS -> context.toast(
- MR.strings.google_drive_sync_data_purged,
- )
- }
- }
- },
- onDismissRequest = { showPurgeDialog = false },
+ @Composable
+ private fun getGoogleDrivePreferences(): List {
+ val context = LocalContext.current
+ val googleDriveSync = Injekt.get()
+ return listOf(
+ Preference.PreferenceItem.TextPreference(
+ title = stringResource(MR.strings.pref_google_drive_sign_in),
+ onClick = {
+ val intent = googleDriveSync.getSignInIntent()
+ context.startActivity(intent)
+ },
+ ),
+ getGoogleDrivePurge(),
)
}
- return Preference.PreferenceItem.TextPreference(
- title = stringResource(MR.strings.pref_google_drive_purge_sync_data),
- onClick = { showPurgeDialog = true },
- )
-}
+ @Composable
+ private fun getGoogleDrivePurge(): Preference.PreferenceItem.TextPreference {
+ val scope = rememberCoroutineScope()
+ val context = LocalContext.current
+ val googleDriveSync = remember { GoogleDriveSyncService(context) }
+ var showPurgeDialog by remember { mutableStateOf(false) }
-@Composable
-private fun PurgeConfirmationDialog(
- onConfirm: () -> Unit,
- onDismissRequest: () -> Unit,
-) {
- AlertDialog(
- onDismissRequest = onDismissRequest,
- title = { Text(text = stringResource(MR.strings.pref_purge_confirmation_title)) },
- text = { Text(text = stringResource(MR.strings.pref_purge_confirmation_message)) },
- dismissButton = {
- TextButton(onClick = onDismissRequest) {
- Text(text = stringResource(MR.strings.action_cancel))
- }
- },
- confirmButton = {
- TextButton(onClick = onConfirm) {
- Text(text = stringResource(MR.strings.action_ok))
- }
- },
- )
-}
-
-@Composable
-private fun getSelfHostPreferences(syncPreferences: SyncPreferences): List {
- val scope = rememberCoroutineScope()
- return listOf(
- Preference.PreferenceItem.EditTextPreference(
- title = stringResource(MR.strings.pref_sync_host),
- subtitle = stringResource(MR.strings.pref_sync_host_summ),
- pref = syncPreferences.syncHost(),
- onValueChanged = { newValue ->
- scope.launch {
- // Trim spaces at the beginning and end, then remove trailing slash if present
- val trimmedValue = newValue.trim()
- val modifiedValue = trimmedValue.trimEnd { it == '/' }
- syncPreferences.syncHost().set(modifiedValue)
- }
- true
- },
- ),
- Preference.PreferenceItem.EditTextPreference(
- title = stringResource(MR.strings.pref_sync_api_key),
- subtitle = stringResource(MR.strings.pref_sync_api_key_summ),
- pref = syncPreferences.syncAPIKey(),
- ),
- )
-}
-
-@Composable
-private fun getSyncNowPref(): Preference.PreferenceGroup {
- val scope = rememberCoroutineScope()
- var showDialog by remember { mutableStateOf(false) }
- val context = LocalContext.current
- if (showDialog) {
- SyncConfirmationDialog(
- onConfirm = {
- showDialog = false
- scope.launch {
- if (!SyncDataJob.isAnyJobRunning(context)) {
- SyncDataJob.startNow(context)
- } else {
- context.toast(MR.strings.sync_in_progress)
+ if (showPurgeDialog) {
+ PurgeConfirmationDialog(
+ onConfirm = {
+ showPurgeDialog = false
+ scope.launch {
+ val result = googleDriveSync.deleteSyncDataFromGoogleDrive()
+ when (result) {
+ GoogleDriveSyncService.DeleteSyncDataStatus.NOT_INITIALIZED -> context.toast(
+ MR.strings.google_drive_not_signed_in,
+ )
+ GoogleDriveSyncService.DeleteSyncDataStatus.NO_FILES -> context.toast(
+ MR.strings.google_drive_sync_data_not_found,
+ )
+ GoogleDriveSyncService.DeleteSyncDataStatus.SUCCESS -> context.toast(
+ MR.strings.google_drive_sync_data_purged,
+ )
+ }
}
- }
- },
- onDismissRequest = { showDialog = false },
+ },
+ onDismissRequest = { showPurgeDialog = false },
+ )
+ }
+
+ return Preference.PreferenceItem.TextPreference(
+ title = stringResource(MR.strings.pref_google_drive_purge_sync_data),
+ onClick = { showPurgeDialog = true },
)
}
- return Preference.PreferenceGroup(
- title = stringResource(MR.strings.pref_sync_now_group_title),
- preferenceItems = listOf(
- getSyncOptionsPref(),
+
+ @Composable
+ private fun PurgeConfirmationDialog(
+ onConfirm: () -> Unit,
+ onDismissRequest: () -> Unit,
+ ) {
+ AlertDialog(
+ onDismissRequest = onDismissRequest,
+ title = { Text(text = stringResource(MR.strings.pref_purge_confirmation_title)) },
+ text = { Text(text = stringResource(MR.strings.pref_purge_confirmation_message)) },
+ dismissButton = {
+ TextButton(onClick = onDismissRequest) {
+ Text(text = stringResource(MR.strings.action_cancel))
+ }
+ },
+ confirmButton = {
+ TextButton(onClick = onConfirm) {
+ Text(text = stringResource(MR.strings.action_ok))
+ }
+ },
+ )
+ }
+
+ @Composable
+ private fun getSelfHostPreferences(syncPreferences: SyncPreferences): List {
+ val scope = rememberCoroutineScope()
+ return listOf(
+ Preference.PreferenceItem.EditTextPreference(
+ title = stringResource(MR.strings.pref_sync_host),
+ subtitle = stringResource(MR.strings.pref_sync_host_summ),
+ pref = syncPreferences.syncHost(),
+ onValueChanged = { newValue ->
+ scope.launch {
+ // Trim spaces at the beginning and end, then remove trailing slash if present
+ val trimmedValue = newValue.trim()
+ val modifiedValue = trimmedValue.trimEnd { it == '/' }
+ syncPreferences.syncHost().set(modifiedValue)
+ }
+ true
+ },
+ ),
+ Preference.PreferenceItem.EditTextPreference(
+ title = stringResource(MR.strings.pref_sync_api_key),
+ subtitle = stringResource(MR.strings.pref_sync_api_key_summ),
+ pref = syncPreferences.syncAPIKey(),
+ ),
+ )
+ }
+
+ @Composable
+ private fun getSyncNowPref(): Preference.PreferenceGroup {
+ val scope = rememberCoroutineScope()
+ var showDialog by remember { mutableStateOf(false) }
+ val context = LocalContext.current
+ if (showDialog) {
+ SyncConfirmationDialog(
+ onConfirm = {
+ showDialog = false
+ scope.launch {
+ if (!SyncDataJob.isAnyJobRunning(context)) {
+ SyncDataJob.startNow(context)
+ } else {
+ context.toast(MR.strings.sync_in_progress)
+ }
+ }
+ },
+ onDismissRequest = { showDialog = false },
+ )
+ }
+ return Preference.PreferenceGroup(
+ title = stringResource(MR.strings.pref_sync_now_group_title),
+ preferenceItems = persistentListOf(
+ getSyncOptionsPref(),
Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.pref_sync_now),
subtitle = stringResource(MR.strings.pref_sync_now_subtitle),
@@ -572,7 +447,7 @@ private fun getSyncNowPref(): Preference.PreferenceGroup {
)
}
-@Composable
+ @Composable
private fun getSyncOptionsPref(): Preference.PreferenceItem.TextPreference {
val navigator = LocalNavigator.currentOrThrow
return Preference.PreferenceItem.TextPreference(
@@ -583,69 +458,59 @@ private fun getSyncOptionsPref(): Preference.PreferenceItem.TextPreference {
}
@Composable
-private fun getAutomaticSyncGroup(syncPreferences: SyncPreferences): Preference.PreferenceGroup {
- val context = LocalContext.current
- val syncIntervalPref = syncPreferences.syncInterval()
- val lastSync by syncPreferences.lastSyncTimestamp().collectAsState()
+ private fun getAutomaticSyncGroup(syncPreferences: SyncPreferences): Preference.PreferenceGroup {
+ val context = LocalContext.current
+ val syncIntervalPref = syncPreferences.syncInterval()
+ val lastSync by syncPreferences.lastSyncTimestamp().collectAsState()
- return Preference.PreferenceGroup(
- title = stringResource(MR.strings.pref_sync_automatic_category),
- preferenceItems = listOf(
- Preference.PreferenceItem.ListPreference(
- pref = syncIntervalPref,
- title = stringResource(MR.strings.pref_sync_interval),
- entries = mapOf(
- 0 to stringResource(MR.strings.off),
- 30 to stringResource(MR.strings.update_30min),
- 60 to stringResource(MR.strings.update_1hour),
- 180 to stringResource(MR.strings.update_3hour),
- 360 to stringResource(MR.strings.update_6hour),
- 720 to stringResource(MR.strings.update_12hour),
- 1440 to stringResource(MR.strings.update_24hour),
- 2880 to stringResource(MR.strings.update_48hour),
- 10080 to stringResource(MR.strings.update_weekly),
+ return Preference.PreferenceGroup(
+ title = stringResource(MR.strings.pref_sync_automatic_category),
+ preferenceItems = persistentListOf(
+ Preference.PreferenceItem.ListPreference(
+ pref = syncIntervalPref,
+ title = stringResource(MR.strings.pref_sync_interval),
+ entries = persistentMapOf(
+ 0 to stringResource(MR.strings.off),
+ 30 to stringResource(MR.strings.update_30min),
+ 60 to stringResource(MR.strings.update_1hour),
+ 180 to stringResource(MR.strings.update_3hour),
+ 360 to stringResource(MR.strings.update_6hour),
+ 720 to stringResource(MR.strings.update_12hour),
+ 1440 to stringResource(MR.strings.update_24hour),
+ 2880 to stringResource(MR.strings.update_48hour),
+ 10080 to stringResource(MR.strings.update_weekly),
+ ),
+ onValueChanged = {
+ SyncDataJob.setupTask(context, it)
+ true
+ },
+ ),
+ Preference.PreferenceItem.InfoPreference(
+ stringResource(MR.strings.last_synchronization, relativeTimeSpanString(lastSync)),
),
- onValueChanged = {
- SyncDataJob.setupTask(context, it)
- true
- },
),
- Preference.PreferenceItem.InfoPreference(
- stringResource(MR.strings.last_synchronization, relativeTimeSpanString(lastSync)),
- ),
- ),
- )
+ )
+ }
+
+ @Composable
+ private fun SyncConfirmationDialog(
+ onConfirm: () -> Unit,
+ onDismissRequest: () -> Unit,
+ ) {
+ AlertDialog(
+ onDismissRequest = onDismissRequest,
+ title = { Text(text = stringResource(MR.strings.pref_sync_confirmation_title)) },
+ text = { Text(text = stringResource(MR.strings.pref_sync_confirmation_message)) },
+ dismissButton = {
+ TextButton(onClick = onDismissRequest) {
+ Text(text = stringResource(MR.strings.action_cancel))
+ }
+ },
+ confirmButton = {
+ TextButton(onClick = onConfirm) {
+ Text(text = stringResource(MR.strings.action_ok))
+ }
+ },
+ )
+ }
}
-
-@Composable
-private fun SyncConfirmationDialog(
- onConfirm: () -> Unit,
- onDismissRequest: () -> Unit,
-) {
- AlertDialog(
- onDismissRequest = onDismissRequest,
- title = { Text(text = stringResource(MR.strings.pref_sync_confirmation_title)) },
- text = { Text(text = stringResource(MR.strings.pref_sync_confirmation_message)) },
- dismissButton = {
- TextButton(onClick = onDismissRequest) {
- Text(text = stringResource(MR.strings.action_cancel))
- }
- },
- confirmButton = {
- TextButton(onClick = onConfirm) {
- Text(text = stringResource(MR.strings.action_ok))
- }
- },
- )
-}
-
-private data class MissingRestoreComponents(
- val uri: Uri,
- val sources: List,
- val trackers: List,
-)
-
-private data class InvalidRestore(
- val uri: Uri? = null,
- val message: String,
-)
diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDownloadScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDownloadScreen.kt
index c9dd746f1..072013415 100644
--- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDownloadScreen.kt
+++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDownloadScreen.kt
@@ -12,6 +12,9 @@ import androidx.compose.ui.util.fastMap
import eu.kanade.presentation.category.visualName
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.widget.TriStateListDialog
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.persistentMapOf
+import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.runBlocking
import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.model.Category
@@ -68,7 +71,7 @@ object SettingsDownloadScreen : SearchableSettings {
): Preference.PreferenceGroup {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_delete_chapters),
- preferenceItems = listOf(
+ preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = downloadPreferences.removeAfterMarkedAsRead(),
title = stringResource(MR.strings.pref_remove_after_marked_as_read),
@@ -76,7 +79,7 @@ object SettingsDownloadScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference(
pref = downloadPreferences.removeAfterReadSlots(),
title = stringResource(MR.strings.pref_remove_after_read),
- entries = mapOf(
+ entries = persistentMapOf(
-1 to stringResource(MR.strings.disabled),
0 to stringResource(MR.strings.last_read_chapter),
1 to stringResource(MR.strings.second_to_last),
@@ -105,7 +108,9 @@ object SettingsDownloadScreen : SearchableSettings {
return Preference.PreferenceItem.MultiSelectListPreference(
pref = downloadPreferences.removeExcludeCategories(),
title = stringResource(MR.strings.pref_remove_exclude_categories),
- entries = categories().associate { it.id.toString() to it.visualName },
+ entries = categories()
+ .associate { it.id.toString() to it.visualName }
+ .toImmutableMap(),
)
}
@@ -142,7 +147,7 @@ object SettingsDownloadScreen : SearchableSettings {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_auto_download),
- preferenceItems = listOf(
+ preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = downloadNewChaptersPref,
title = stringResource(MR.strings.pref_download_new),
@@ -167,17 +172,19 @@ object SettingsDownloadScreen : SearchableSettings {
): Preference.PreferenceGroup {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.download_ahead),
- preferenceItems = listOf(
+ preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = downloadPreferences.autoDownloadWhileReading(),
title = stringResource(MR.strings.auto_download_while_reading),
- entries = listOf(0, 2, 3, 5, 10).associateWith {
- if (it == 0) {
- stringResource(MR.strings.disabled)
- } else {
- pluralStringResource(MR.plurals.next_unread_chapters, count = it, it)
+ entries = listOf(0, 2, 3, 5, 10)
+ .associateWith {
+ if (it == 0) {
+ stringResource(MR.strings.disabled)
+ } else {
+ pluralStringResource(MR.plurals.next_unread_chapters, count = it, it)
+ }
}
- },
+ .toImmutableMap(),
),
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.download_ahead_info)),
),
diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt
index 564b0b647..1ad7410be 100644
--- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt
+++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt
@@ -20,6 +20,9 @@ import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.widget.TriStateListDialog
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.ui.category.CategoryScreen
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.persistentMapOf
+import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import tachiyomi.domain.category.interactor.GetCategories
@@ -65,7 +68,6 @@ object SettingsLibraryScreen : SearchableSettings {
allCategories: List,
libraryPreferences: LibraryPreferences,
): Preference.PreferenceGroup {
- val context = LocalContext.current
val scope = rememberCoroutineScope()
val userCategoriesCount = allCategories.filterNot(Category::isSystemCategory).size
@@ -76,11 +78,11 @@ object SettingsLibraryScreen : SearchableSettings {
val ids = listOf(libraryPreferences.defaultCategory().defaultValue()) +
allCategories.fastMap { it.id.toInt() }
val labels = listOf(stringResource(MR.strings.default_category_summary)) +
- allCategories.fastMap { it.visualName(context) }
+ allCategories.fastMap { it.visualName }
return Preference.PreferenceGroup(
title = stringResource(MR.strings.categories),
- preferenceItems = listOf(
+ preferenceItems = persistentListOf(
Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.action_edit_categories),
subtitle = pluralStringResource(
@@ -94,7 +96,7 @@ object SettingsLibraryScreen : SearchableSettings {
pref = libraryPreferences.defaultCategory(),
title = stringResource(MR.strings.default_category),
subtitle = selectedCategory?.visualName ?: stringResource(MR.strings.default_category_summary),
- entries = ids.zip(labels).toMap(),
+ entries = ids.zip(labels).toMap().toImmutableMap(),
),
Preference.PreferenceItem.SwitchPreference(
pref = libraryPreferences.categorizedDisplaySettings(),
@@ -147,11 +149,11 @@ object SettingsLibraryScreen : SearchableSettings {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_library_update),
- preferenceItems = listOf(
+ preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = autoUpdateIntervalPref,
title = stringResource(MR.strings.pref_library_update_interval),
- entries = mapOf(
+ entries = persistentMapOf(
0 to stringResource(MR.strings.update_never),
12 to stringResource(MR.strings.update_12hour),
24 to stringResource(MR.strings.update_24hour),
@@ -169,7 +171,7 @@ object SettingsLibraryScreen : SearchableSettings {
enabled = autoUpdateInterval > 0,
title = stringResource(MR.strings.pref_library_update_restriction),
subtitle = stringResource(MR.strings.restrictions),
- entries = mapOf(
+ entries = persistentMapOf(
DEVICE_ONLY_ON_WIFI to stringResource(MR.strings.connected_to_wifi),
DEVICE_NETWORK_NOT_METERED to stringResource(MR.strings.network_not_metered),
DEVICE_CHARGING to stringResource(MR.strings.charging),
@@ -197,7 +199,7 @@ object SettingsLibraryScreen : SearchableSettings {
Preference.PreferenceItem.MultiSelectListPreference(
pref = libraryPreferences.autoUpdateMangaRestrictions(),
title = stringResource(MR.strings.pref_library_update_manga_restriction),
- entries = mapOf(
+ entries = persistentMapOf(
MANGA_HAS_UNREAD to stringResource(MR.strings.pref_update_only_completely_read),
MANGA_NON_READ to stringResource(MR.strings.pref_update_only_started),
MANGA_NON_COMPLETED to stringResource(MR.strings.pref_update_only_non_completed),
@@ -218,11 +220,11 @@ object SettingsLibraryScreen : SearchableSettings {
): Preference.PreferenceGroup {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_chapter_swipe),
- preferenceItems = listOf(
+ preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = libraryPreferences.swipeToStartAction(),
title = stringResource(MR.strings.pref_chapter_swipe_start),
- entries = mapOf(
+ entries = persistentMapOf(
LibraryPreferences.ChapterSwipeAction.Disabled to
stringResource(MR.strings.disabled),
LibraryPreferences.ChapterSwipeAction.ToggleBookmark to
@@ -236,7 +238,7 @@ object SettingsLibraryScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference(
pref = libraryPreferences.swipeToEndAction(),
title = stringResource(MR.strings.pref_chapter_swipe_end),
- entries = mapOf(
+ entries = persistentMapOf(
LibraryPreferences.ChapterSwipeAction.Disabled to
stringResource(MR.strings.disabled),
LibraryPreferences.ChapterSwipeAction.ToggleBookmark to
diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsReaderScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsReaderScreen.kt
index aac9259a1..c28b12d48 100644
--- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsReaderScreen.kt
+++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsReaderScreen.kt
@@ -10,6 +10,9 @@ import eu.kanade.presentation.more.settings.Preference
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.persistentMapOf
+import kotlinx.collections.immutable.toImmutableMap
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState
@@ -31,12 +34,13 @@ object SettingsReaderScreen : SearchableSettings {
pref = readerPref.defaultReadingMode(),
title = stringResource(MR.strings.pref_viewer_type),
entries = ReadingMode.entries.drop(1)
- .associate { it.flagValue to stringResource(it.stringRes) },
+ .associate { it.flagValue to stringResource(it.stringRes) }
+ .toImmutableMap(),
),
Preference.PreferenceItem.ListPreference(
pref = readerPref.doubleTapAnimSpeed(),
title = stringResource(MR.strings.pref_double_tap_anim_speed),
- entries = mapOf(
+ entries = persistentMapOf(
1 to stringResource(MR.strings.double_tap_anim_speed_0),
500 to stringResource(MR.strings.double_tap_anim_speed_normal),
250 to stringResource(MR.strings.double_tap_anim_speed_fast),
@@ -82,17 +86,18 @@ object SettingsReaderScreen : SearchableSettings {
val fullscreen by fullscreenPref.collectAsState()
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_display),
- preferenceItems = listOf(
+ preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = readerPreferences.defaultOrientationType(),
title = stringResource(MR.strings.pref_rotation_type),
entries = ReaderOrientation.entries.drop(1)
- .associate { it.flagValue to stringResource(it.stringRes) },
+ .associate { it.flagValue to stringResource(it.stringRes) }
+ .toImmutableMap(),
),
Preference.PreferenceItem.ListPreference(
pref = readerPreferences.readerTheme(),
title = stringResource(MR.strings.pref_reader_theme),
- entries = mapOf(
+ entries = persistentMapOf(
1 to stringResource(MR.strings.black_background),
2 to stringResource(MR.strings.gray_background),
0 to stringResource(MR.strings.white_background),
@@ -126,7 +131,7 @@ object SettingsReaderScreen : SearchableSettings {
private fun getReadingGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_reading),
- preferenceItems = listOf(
+ preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.skipRead(),
title = stringResource(MR.strings.pref_skip_read_chapters),
@@ -161,23 +166,26 @@ object SettingsReaderScreen : SearchableSettings {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pager_viewer),
- preferenceItems = listOf(
+ preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = navModePref,
title = stringResource(MR.strings.pref_viewer_nav),
entries = ReaderPreferences.TapZones
.mapIndexed { index, it -> index to stringResource(it) }
- .toMap(),
+ .toMap()
+ .toImmutableMap(),
),
Preference.PreferenceItem.ListPreference(
pref = readerPreferences.pagerNavInverted(),
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
- entries = listOf(
+ entries = persistentListOf(
ReaderPreferences.TappingInvertMode.NONE,
ReaderPreferences.TappingInvertMode.HORIZONTAL,
ReaderPreferences.TappingInvertMode.VERTICAL,
ReaderPreferences.TappingInvertMode.BOTH,
- ).associateWith { stringResource(it.titleRes) },
+ )
+ .associateWith { stringResource(it.titleRes) }
+ .toImmutableMap(),
enabled = navMode != 5,
),
Preference.PreferenceItem.ListPreference(
@@ -185,14 +193,16 @@ object SettingsReaderScreen : SearchableSettings {
title = stringResource(MR.strings.pref_image_scale_type),
entries = ReaderPreferences.ImageScaleType
.mapIndexed { index, it -> index + 1 to stringResource(it) }
- .toMap(),
+ .toMap()
+ .toImmutableMap(),
),
Preference.PreferenceItem.ListPreference(
pref = readerPreferences.zoomStart(),
title = stringResource(MR.strings.pref_zoom_start),
entries = ReaderPreferences.ZoomStart
.mapIndexed { index, it -> index + 1 to stringResource(it) }
- .toMap(),
+ .toMap()
+ .toImmutableMap(),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.cropBorders(),
@@ -255,23 +265,26 @@ object SettingsReaderScreen : SearchableSettings {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.webtoon_viewer),
- preferenceItems = listOf(
+ preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = navModePref,
title = stringResource(MR.strings.pref_viewer_nav),
entries = ReaderPreferences.TapZones
.mapIndexed { index, it -> index to stringResource(it) }
- .toMap(),
+ .toMap()
+ .toImmutableMap(),
),
Preference.PreferenceItem.ListPreference(
pref = readerPreferences.webtoonNavInverted(),
title = stringResource(MR.strings.pref_read_with_tapping_inverted),
- entries = listOf(
+ entries = persistentListOf(
ReaderPreferences.TappingInvertMode.NONE,
ReaderPreferences.TappingInvertMode.HORIZONTAL,
ReaderPreferences.TappingInvertMode.VERTICAL,
ReaderPreferences.TappingInvertMode.BOTH,
- ).associateWith { stringResource(it.titleRes) },
+ )
+ .associateWith { stringResource(it.titleRes) }
+ .toImmutableMap(),
enabled = navMode != 5,
),
Preference.PreferenceItem.SliderPreference(
@@ -288,7 +301,7 @@ object SettingsReaderScreen : SearchableSettings {
Preference.PreferenceItem.ListPreference(
pref = readerPreferences.readerHideThreshold(),
title = stringResource(MR.strings.pref_hide_threshold),
- entries = mapOf(
+ entries = persistentMapOf(
ReaderPreferences.ReaderHideThreshold.HIGHEST to stringResource(MR.strings.pref_highest),
ReaderPreferences.ReaderHideThreshold.HIGH to stringResource(MR.strings.pref_high),
ReaderPreferences.ReaderHideThreshold.LOW to stringResource(MR.strings.pref_low),
@@ -341,7 +354,7 @@ object SettingsReaderScreen : SearchableSettings {
val readWithVolumeKeys by readWithVolumeKeysPref.collectAsState()
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_reader_navigation),
- preferenceItems = listOf(
+ preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = readWithVolumeKeysPref,
title = stringResource(MR.strings.pref_read_with_volume_keys),
@@ -359,7 +372,7 @@ object SettingsReaderScreen : SearchableSettings {
private fun getActionsGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_reader_actions),
- preferenceItems = listOf(
+ preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.readWithLongTap(),
title = stringResource(MR.strings.pref_read_with_long_tap),
diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSecurityScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSecurityScreen.kt
index 0acb22083..fb1e0932c 100644
--- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSecurityScreen.kt
+++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSecurityScreen.kt
@@ -10,6 +10,8 @@ import eu.kanade.presentation.more.settings.Preference
import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.isAuthenticationSupported
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.toImmutableMap
import tachiyomi.core.i18n.stringResource
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.pluralStringResource
@@ -55,7 +57,8 @@ object SettingsSecurityScreen : SearchableSettings {
0 -> stringResource(MR.strings.lock_always)
else -> pluralStringResource(MR.plurals.lock_after_mins, count = it, it)
}
- },
+ }
+ .toImmutableMap(),
onValueChanged = {
(context as FragmentActivity).authenticate(
title = context.stringResource(MR.strings.lock_when_idle),
@@ -70,14 +73,15 @@ object SettingsSecurityScreen : SearchableSettings {
pref = securityPreferences.secureScreen(),
title = stringResource(MR.strings.secure_screen),
entries = SecurityPreferences.SecureScreenMode.entries
- .associateWith { stringResource(it.titleRes) },
+ .associateWith { stringResource(it.titleRes) }
+ .toImmutableMap(),
),
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.secure_screen_summary)),
)
}
}
-private val LockAfterValues = listOf(
+private val LockAfterValues = persistentListOf(
0, // Always
1,
2,
diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt
index 4aa7a26bd..f81f1ba5d 100644
--- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt
+++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt
@@ -51,6 +51,8 @@ import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeListApi
import eu.kanade.tachiyomi.data.track.shikimori.ShikimoriApi
import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.util.system.toast
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.toImmutableList
import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.lang.withUIContext
import tachiyomi.domain.source.service.SourceManager
@@ -125,7 +127,7 @@ object SettingsTrackingScreen : SearchableSettings {
),
Preference.PreferenceGroup(
title = stringResource(MR.strings.services),
- preferenceItems = listOf(
+ preferenceItems = persistentListOf(
Preference.PreferenceItem.TrackerPreference(
title = trackerManager.myAnimeList.name,
tracker = trackerManager.myAnimeList,
@@ -167,15 +169,17 @@ object SettingsTrackingScreen : SearchableSettings {
),
Preference.PreferenceGroup(
title = stringResource(MR.strings.enhanced_services),
- preferenceItems = enhancedTrackers.first
- .map { service ->
- Preference.PreferenceItem.TrackerPreference(
- title = service.name,
- tracker = service,
- login = { (service as EnhancedTracker).loginNoop() },
- logout = service::logout,
- )
- } + listOf(Preference.PreferenceItem.InfoPreference(enhancedTrackerInfo)),
+ preferenceItems = (
+ enhancedTrackers.first
+ .map { service ->
+ Preference.PreferenceItem.TrackerPreference(
+ title = service.name,
+ tracker = service,
+ login = { (service as EnhancedTracker).loginNoop() },
+ logout = service::logout,
+ )
+ } + listOf(Preference.PreferenceItem.InfoPreference(enhancedTrackerInfo))
+ ).toImmutableList(),
),
)
}
diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt
index 261a1b20a..9811a3cce 100644
--- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt
+++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt
@@ -4,7 +4,6 @@ import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
-import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column
@@ -28,16 +27,18 @@ import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.components.AppBar
+import eu.kanade.presentation.components.WarningBanner
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
-import eu.kanade.tachiyomi.data.backup.models.Backup
+import eu.kanade.tachiyomi.data.backup.create.BackupCreator
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.collections.immutable.PersistentSet
import kotlinx.collections.immutable.minus
+import kotlinx.collections.immutable.persistentMapOf
+import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.plus
-import kotlinx.collections.immutable.toPersistentSet
import kotlinx.coroutines.flow.update
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.LabeledCheckbox
@@ -83,16 +84,21 @@ class CreateBackupScreen : Screen() {
.fillMaxSize(),
) {
LazyColumn(
- modifier = Modifier
- .weight(1f)
- .padding(horizontal = MaterialTheme.padding.medium),
+ 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),
)
}
BackupChoices.forEach { (k, v) ->
@@ -103,6 +109,7 @@ class CreateBackupScreen : Screen() {
onCheckedChange = {
model.toggleFlag(k)
},
+ modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium),
)
}
}
@@ -116,11 +123,8 @@ class CreateBackupScreen : Screen() {
.fillMaxWidth(),
onClick = {
if (!BackupCreateJob.isManualJobRunning(context)) {
- if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
- context.toast(MR.strings.restore_miui_warning, Toast.LENGTH_LONG)
- }
try {
- chooseBackupDir.launch(Backup.getFilename())
+ chooseBackupDir.launch(BackupCreator.getFilename())
} catch (e: ActivityNotFoundException) {
context.toast(MR.strings.file_picker_error)
}
@@ -158,11 +162,16 @@ private class CreateBackupScreenModel : StateScreenModel = BackupChoices.keys.toPersistentSet(),
+ val flags: PersistentSet = persistentSetOf(
+ BackupCreateFlags.BACKUP_CATEGORY,
+ BackupCreateFlags.BACKUP_CHAPTER,
+ BackupCreateFlags.BACKUP_TRACK,
+ BackupCreateFlags.BACKUP_HISTORY,
+ ),
)
}
-private val BackupChoices = mapOf(
+private val BackupChoices = persistentMapOf(
BackupCreateFlags.BACKUP_CATEGORY to MR.strings.categories,
BackupCreateFlags.BACKUP_CHAPTER to MR.strings.chapters,
BackupCreateFlags.BACKUP_TRACK to MR.strings.track,
diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt
new file mode 100644
index 000000000..2d6aebced
--- /dev/null
+++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt
@@ -0,0 +1,242 @@
+package eu.kanade.presentation.more.settings.screen.data
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import cafe.adriel.voyager.core.model.StateScreenModel
+import cafe.adriel.voyager.core.model.rememberScreenModel
+import cafe.adriel.voyager.navigator.LocalNavigator
+import cafe.adriel.voyager.navigator.currentOrThrow
+import eu.kanade.presentation.components.AppBar
+import eu.kanade.presentation.components.WarningBanner
+import eu.kanade.presentation.util.Screen
+import eu.kanade.tachiyomi.data.backup.BackupFileValidator
+import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
+import eu.kanade.tachiyomi.data.backup.restore.RestoreOptions
+import eu.kanade.tachiyomi.util.system.DeviceUtil
+import eu.kanade.tachiyomi.util.system.copyToClipboard
+import eu.kanade.tachiyomi.util.system.toast
+import kotlinx.coroutines.flow.update
+import tachiyomi.core.i18n.stringResource
+import tachiyomi.i18n.MR
+import tachiyomi.presentation.core.components.material.Scaffold
+import tachiyomi.presentation.core.components.material.padding
+import tachiyomi.presentation.core.i18n.stringResource
+
+class RestoreBackupScreen : Screen() {
+
+ @Composable
+ override fun Content() {
+ val context = LocalContext.current
+ val navigator = LocalNavigator.currentOrThrow
+ val model = rememberScreenModel { RestoreBackupScreenModel() }
+ val state by model.state.collectAsState()
+
+ Scaffold(
+ topBar = {
+ AppBar(
+ title = stringResource(MR.strings.pref_restore_backup),
+ navigateUp = navigator::pop,
+ scrollBehavior = it,
+ )
+ },
+ ) { contentPadding ->
+ if (state.error != null) {
+ val onDismissRequest = model::clearError
+ when (val err = state.error) {
+ is InvalidRestore -> {
+ AlertDialog(
+ onDismissRequest = onDismissRequest,
+ title = { Text(text = stringResource(MR.strings.invalid_backup_file)) },
+ text = { Text(text = listOfNotNull(err.uri, err.message).joinToString("\n\n")) },
+ dismissButton = {
+ TextButton(
+ onClick = {
+ context.copyToClipboard(err.message, err.message)
+ onDismissRequest()
+ },
+ ) {
+ Text(text = stringResource(MR.strings.action_copy_to_clipboard))
+ }
+ },
+ confirmButton = {
+ TextButton(onClick = onDismissRequest) {
+ Text(text = stringResource(MR.strings.action_ok))
+ }
+ },
+ )
+ }
+ is MissingRestoreComponents -> {
+ AlertDialog(
+ onDismissRequest = onDismissRequest,
+ title = { Text(text = stringResource(MR.strings.pref_restore_backup)) },
+ text = {
+ Column(
+ modifier = Modifier.verticalScroll(rememberScrollState()),
+ ) {
+ val msg = buildString {
+ append(stringResource(MR.strings.backup_restore_content_full))
+ if (err.sources.isNotEmpty()) {
+ append(
+ "\n\n",
+ ).append(stringResource(MR.strings.backup_restore_missing_sources))
+ err.sources.joinTo(
+ this,
+ separator = "\n- ",
+ prefix = "\n- ",
+ )
+ }
+ if (err.trackers.isNotEmpty()) {
+ append(
+ "\n\n",
+ ).append(stringResource(MR.strings.backup_restore_missing_trackers))
+ err.trackers.joinTo(
+ this,
+ separator = "\n- ",
+ prefix = "\n- ",
+ )
+ }
+ }
+ Text(text = msg)
+ }
+ },
+ confirmButton = {
+ TextButton(
+ onClick = {
+ BackupRestoreJob.start(
+ context = context,
+ uri = err.uri,
+ options = state.options,
+ )
+ onDismissRequest()
+ },
+ ) {
+ Text(text = stringResource(MR.strings.action_restore))
+ }
+ },
+ )
+ }
+ else -> onDismissRequest() // Unknown
+ }
+ }
+
+ val chooseBackup = rememberLauncherForActivityResult(
+ object : ActivityResultContracts.GetContent() {
+ override fun createIntent(context: Context, input: String): Intent {
+ val intent = super.createIntent(context, input)
+ return Intent.createChooser(intent, context.stringResource(MR.strings.file_select_backup))
+ }
+ },
+ ) {
+ if (it == null) {
+ context.toast(MR.strings.file_null_uri_error)
+ return@rememberLauncherForActivityResult
+ }
+
+ val results = try {
+ BackupFileValidator(context).validate(it)
+ } catch (e: Exception) {
+ model.setError(InvalidRestore(it, e.message.toString()))
+ return@rememberLauncherForActivityResult
+ }
+
+ if (results.missingSources.isEmpty() && results.missingTrackers.isEmpty()) {
+ BackupRestoreJob.start(
+ context = context,
+ uri = it,
+ options = state.options,
+ )
+ return@rememberLauncherForActivityResult
+ }
+
+ model.setError(MissingRestoreComponents(it, results.missingSources, results.missingTrackers))
+ }
+
+ LazyColumn(
+ modifier = Modifier
+ .padding(contentPadding)
+ .fillMaxSize(),
+ ) {
+ if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
+ item {
+ WarningBanner(MR.strings.restore_miui_warning)
+ }
+ }
+
+ item {
+ Button(
+ modifier = Modifier
+ .padding(horizontal = MaterialTheme.padding.medium)
+ .fillMaxWidth(),
+ onClick = {
+ if (!BackupRestoreJob.isRunning(context)) {
+ // no need to catch because it's wrapped with a chooser
+ chooseBackup.launch("*/*")
+ } else {
+ context.toast(MR.strings.restore_in_progress)
+ }
+ },
+ ) {
+ Text(stringResource(MR.strings.pref_restore_backup))
+ }
+ }
+
+ // TODO: show validation errors inline
+ // TODO: show options for what to restore
+ }
+ }
+ }
+}
+
+private class RestoreBackupScreenModel : StateScreenModel(State()) {
+
+ fun setError(error: Any) {
+ mutableState.update {
+ it.copy(error = error)
+ }
+ }
+
+ fun clearError() {
+ mutableState.update {
+ it.copy(error = null)
+ }
+ }
+
+ @Immutable
+ data class State(
+ val error: Any? = null,
+ // TODO: allow user-selectable restore options
+ val options: RestoreOptions = RestoreOptions(),
+ )
+}
+
+private data class MissingRestoreComponents(
+ val uri: Uri,
+ val sources: List,
+ val trackers: List,
+)
+
+private data class InvalidRestore(
+ val uri: Uri? = null,
+ val message: String,
+)
diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/StorageInfo.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/StorageInfo.kt
new file mode 100644
index 000000000..e45b6bafc
--- /dev/null
+++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/StorageInfo.kt
@@ -0,0 +1,74 @@
+package eu.kanade.presentation.more.settings.screen.data
+
+import android.text.format.Formatter
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.material3.LinearProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import eu.kanade.tachiyomi.util.storage.DiskUtil
+import tachiyomi.i18n.MR
+import tachiyomi.presentation.core.i18n.stringResource
+import tachiyomi.presentation.core.theme.header
+import tachiyomi.presentation.core.util.secondaryItemAlpha
+import java.io.File
+
+@Composable
+fun StorageInfo(
+ modifier: Modifier = Modifier,
+) {
+ val context = LocalContext.current
+ val storages = remember { DiskUtil.getExternalStorages(context) }
+
+ Column(
+ modifier = modifier,
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ storages.forEach {
+ StorageInfo(it)
+ }
+ }
+}
+
+@Composable
+private fun StorageInfo(
+ file: File,
+) {
+ val context = LocalContext.current
+
+ val available = remember(file) { DiskUtil.getAvailableStorageSpace(file) }
+ val availableText = remember(available) { Formatter.formatFileSize(context, available) }
+ val total = remember(file) { DiskUtil.getTotalStorageSpace(file) }
+ val totalText = remember(total) { Formatter.formatFileSize(context, total) }
+
+ Column(
+ verticalArrangement = Arrangement.spacedBy(4.dp),
+ ) {
+ Text(
+ text = file.absolutePath,
+ style = MaterialTheme.typography.header,
+ )
+
+ LinearProgressIndicator(
+ modifier = Modifier
+ .clip(MaterialTheme.shapes.small)
+ .fillMaxWidth()
+ .height(12.dp),
+ progress = { (1 - (available / total.toFloat())) },
+ )
+
+ Text(
+ text = stringResource(MR.strings.available_disk_space_info, availableText, totalText),
+ modifier = Modifier.secondaryItemAlpha(),
+ style = MaterialTheme.typography.bodySmall,
+ )
+ }
+}
diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/DebugInfoScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/DebugInfoScreen.kt
index 3eecee438..0db4bd3c7 100644
--- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/DebugInfoScreen.kt
+++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/debug/DebugInfoScreen.kt
@@ -15,6 +15,8 @@ import eu.kanade.presentation.more.settings.screen.about.AboutScreen
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.WebViewUtil
+import kotlinx.collections.immutable.mutate
+import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.guava.await
import tachiyomi.i18n.MR
@@ -47,7 +49,7 @@ class DebugInfoScreen : Screen() {
private fun getAppInfoGroup(): Preference.PreferenceGroup {
return Preference.PreferenceGroup(
title = "App info",
- preferenceItems = listOf(
+ preferenceItems = persistentListOf(
Preference.PreferenceItem.TextPreference(
title = "Version",
subtitle = AboutScreen.getVersionName(false),
@@ -96,8 +98,8 @@ class DebugInfoScreen : Screen() {
}
private fun getDeviceInfoGroup(): Preference.PreferenceGroup {
- val items = buildList {
- add(
+ val items = persistentListOf>().mutate {
+ it.add(
Preference.PreferenceItem.TextPreference(
title = "Model",
subtitle = "${Build.MANUFACTURER} ${Build.MODEL} (${Build.DEVICE})",
@@ -105,14 +107,14 @@ class DebugInfoScreen : Screen() {
)
if (DeviceUtil.oneUiVersion != null) {
- add(
+ it.add(
Preference.PreferenceItem.TextPreference(
title = "OneUI version",
subtitle = "${DeviceUtil.oneUiVersion}",
),
)
} else if (DeviceUtil.miuiMajorVersion != null) {
- add(
+ it.add(
Preference.PreferenceItem.TextPreference(
title = "MIUI version",
subtitle = "${DeviceUtil.miuiMajorVersion}",
@@ -127,7 +129,7 @@ class DebugInfoScreen : Screen() {
} else {
Build.VERSION.RELEASE
}
- add(
+ it.add(
Preference.PreferenceItem.TextPreference(
title = "Android version",
subtitle = "$androidVersion (${Build.DISPLAY})",
diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/widget/BasePreferenceWidget.kt b/app/src/main/java/eu/kanade/presentation/more/settings/widget/BasePreferenceWidget.kt
index 4af36dc03..bba72cf98 100644
--- a/app/src/main/java/eu/kanade/presentation/more/settings/widget/BasePreferenceWidget.kt
+++ b/app/src/main/java/eu/kanade/presentation/more/settings/widget/BasePreferenceWidget.kt
@@ -114,6 +114,7 @@ internal fun Modifier.highlightBackground(highlighted: Boolean): Modifier = comp
} else {
tween(200)
},
+ label = "highlight",
)
Modifier.background(color = highlight)
}
diff --git a/app/src/main/java/eu/kanade/presentation/more/stats/StatsScreenContent.kt b/app/src/main/java/eu/kanade/presentation/more/stats/StatsScreenContent.kt
index 64f59280c..a24b4cb64 100644
--- a/app/src/main/java/eu/kanade/presentation/more/stats/StatsScreenContent.kt
+++ b/app/src/main/java/eu/kanade/presentation/more/stats/StatsScreenContent.kt
@@ -140,7 +140,7 @@ private fun TrackerStats(
val meanScoreStr = remember(data.trackedTitleCount, data.meanScore) {
if (data.trackedTitleCount > 0 && !data.meanScore.isNaN()) {
// All other numbers are localized in English
- String.format(Locale.ENGLISH, "%.2f ★", data.meanScore)
+ "%.2f ★".format(Locale.ENGLISH, data.meanScore)
} else {
notApplicable
}
diff --git a/app/src/main/java/eu/kanade/presentation/reader/ChapterTransition.kt b/app/src/main/java/eu/kanade/presentation/reader/ChapterTransition.kt
index 422192383..d464382c3 100644
--- a/app/src/main/java/eu/kanade/presentation/reader/ChapterTransition.kt
+++ b/app/src/main/java/eu/kanade/presentation/reader/ChapterTransition.kt
@@ -34,11 +34,11 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import eu.kanade.presentation.theme.TachiyomiTheme
-import eu.kanade.tachiyomi.data.database.models.Chapter
-import eu.kanade.tachiyomi.data.database.models.ChapterImpl
import eu.kanade.tachiyomi.data.database.models.toDomainChapter
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
+import kotlinx.collections.immutable.persistentMapOf
+import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.chapter.service.calculateChapterGap
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.pluralStringResource
@@ -51,8 +51,8 @@ fun ChapterTransition(
currChapterDownloaded: Boolean,
goingToChapterDownloaded: Boolean,
) {
- val currChapter = transition.from.chapter
- val goingToChapter = transition.to?.chapter
+ val currChapter = transition.from.chapter.toDomainChapter()
+ val goingToChapter = transition.to?.chapter?.toDomainChapter()
ProvideTextStyle(MaterialTheme.typography.bodyMedium) {
when (transition) {
@@ -65,7 +65,7 @@ fun ChapterTransition(
bottomChapter = currChapter,
bottomChapterDownloaded = currChapterDownloaded,
fallbackLabel = stringResource(MR.strings.transition_no_previous),
- chapterGap = calculateChapterGap(currChapter.toDomainChapter(), goingToChapter?.toDomainChapter()),
+ chapterGap = calculateChapterGap(currChapter, goingToChapter),
)
}
is ChapterTransition.Next -> {
@@ -77,7 +77,7 @@ fun ChapterTransition(
bottomChapter = goingToChapter,
bottomChapterDownloaded = goingToChapterDownloaded,
fallbackLabel = stringResource(MR.strings.transition_no_next),
- chapterGap = calculateChapterGap(goingToChapter?.toDomainChapter(), currChapter.toDomainChapter()),
+ chapterGap = calculateChapterGap(goingToChapter, currChapter),
)
}
}
@@ -235,7 +235,7 @@ private fun ChapterText(
maxLines = 5,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.titleLarge,
- inlineContent = mapOf(
+ inlineContent = persistentMapOf(
DownloadedIconContentId to InlineTextContent(
Placeholder(
width = 22.sp,
@@ -275,24 +275,23 @@ private val CardColor: CardColors
private val VerticalSpacerSize = 24.dp
private const val DownloadedIconContentId = "downloaded"
-private fun previewChapter(name: String, scanlator: String, chapterNumber: Float) = ChapterImpl().apply {
- this.name = name
- this.scanlator = scanlator
- this.chapter_number = chapterNumber
-
- this.id = 0
- this.manga_id = 0
- this.url = ""
-}
+private fun previewChapter(name: String, scanlator: String, chapterNumber: Double) = Chapter.create().copy(
+ id = 0L,
+ mangaId = 0L,
+ url = "",
+ name = name,
+ scanlator = scanlator,
+ chapterNumber = chapterNumber,
+)
private val FakeChapter = previewChapter(
name = "Vol.1, Ch.1 - Fake Chapter Title",
scanlator = "Scanlator Name",
- chapterNumber = 1f,
+ chapterNumber = 1.0,
)
private val FakeGapChapter = previewChapter(
name = "Vol.5, Ch.44 - Fake Gap Chapter Title",
scanlator = "Scanlator Name",
- chapterNumber = 44f,
+ chapterNumber = 44.0,
)
private val FakeChapterLongTitle = previewChapter(
name = "Vol.1, Ch.0 - The Mundane Musings of a Metafictional Manga: A Chapter About a Chapter, Featuring" +
@@ -301,7 +300,7 @@ private val FakeChapterLongTitle = previewChapter(
"Fictional Realities and Reality-Bending Fiction, Where the Fourth Wall is Always in Danger of Being Broken " +
"and the Line Between Author and Character is Forever Blurred.",
scanlator = "Long Long Funny Scanlator Sniper Group Name Reborn",
- chapterNumber = 1f,
+ chapterNumber = 1.0,
)
@PreviewLightDark
diff --git a/app/src/main/java/eu/kanade/presentation/reader/DisplayRefreshHost.kt b/app/src/main/java/eu/kanade/presentation/reader/DisplayRefreshHost.kt
index 018dbb948..f0afd08a9 100644
--- a/app/src/main/java/eu/kanade/presentation/reader/DisplayRefreshHost.kt
+++ b/app/src/main/java/eu/kanade/presentation/reader/DisplayRefreshHost.kt
@@ -30,7 +30,7 @@ fun DisplayRefreshHost(
val currentDisplayRefresh = hostState.currentDisplayRefresh
LaunchedEffect(currentDisplayRefresh) {
if (currentDisplayRefresh) {
- delay(200)
+ delay(1500)
hostState.currentDisplayRefresh = false
}
}
@@ -39,7 +39,7 @@ fun DisplayRefreshHost(
Canvas(
modifier = modifier.fillMaxSize(),
) {
- drawRect(Color.White)
+ drawRect(Color.Black)
}
}
}
diff --git a/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHome.kt b/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHome.kt
index 07693aa3a..0aec41dce 100644
--- a/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHome.kt
+++ b/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHome.kt
@@ -47,7 +47,6 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import dev.icerock.moko.resources.StringResource
-import eu.kanade.domain.track.model.toDbTrack
import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.presentation.track.components.TrackLogoIcon
@@ -101,7 +100,7 @@ fun TrackInfoDialogHome(
}
},
onChaptersClick = { onChapterClick(item) },
- score = item.tracker.displayScore(item.track.toDbTrack())
+ score = item.tracker.displayScore(item.track)
.takeIf { supportsScoring && item.track.score != 0.0 },
onScoreClick = { onScoreClick(item) }
.takeIf { supportsScoring },
diff --git a/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHomePreviewProvider.kt b/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHomePreviewProvider.kt
index e2733b7bf..51b7ca3f8 100644
--- a/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHomePreviewProvider.kt
+++ b/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogHomePreviewProvider.kt
@@ -13,7 +13,7 @@ internal class TrackInfoDialogHomePreviewProvider :
private val aTrack = Track(
id = 1L,
mangaId = 2L,
- syncId = 3L,
+ trackerId = 3L,
remoteId = 4L,
libraryId = null,
title = "Manage Name On Tracker Site",
diff --git a/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogSelector.kt b/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogSelector.kt
index c7675fbe7..3531df865 100644
--- a/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogSelector.kt
+++ b/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogSelector.kt
@@ -34,6 +34,7 @@ import androidx.compose.ui.unit.dp
import dev.icerock.moko.resources.StringResource
import eu.kanade.presentation.theme.TachiyomiTheme
import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableList
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
@@ -233,7 +234,7 @@ private fun TrackStatusSelectorPreviews() {
TrackStatusSelector(
selection = 1,
onSelectionChange = {},
- selections = mapOf(
+ selections = persistentMapOf(
// Anilist values
1 to MR.strings.reading,
2 to MR.strings.plan_to_read,
diff --git a/app/src/main/java/eu/kanade/presentation/track/TrackerSearchPreviewProvider.kt b/app/src/main/java/eu/kanade/presentation/track/TrackerSearchPreviewProvider.kt
index b945e2ad4..7bc78781b 100644
--- a/app/src/main/java/eu/kanade/presentation/track/TrackerSearchPreviewProvider.kt
+++ b/app/src/main/java/eu/kanade/presentation/track/TrackerSearchPreviewProvider.kt
@@ -62,8 +62,8 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
private fun randTrackSearch() = TrackSearch().let {
it.id = Random.nextLong()
it.manga_id = Random.nextLong()
- it.sync_id = Random.nextInt()
- it.media_id = Random.nextLong()
+ it.tracker_id = Random.nextInt()
+ it.remote_id = Random.nextLong()
it.library_id = Random.nextLong()
it.title = lorem((1..10).random()).joinToString()
it.last_chapter_read = (0..100).random().toFloat()
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupDecoder.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupDecoder.kt
new file mode 100644
index 000000000..e33572caf
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupDecoder.kt
@@ -0,0 +1,39 @@
+package eu.kanade.tachiyomi.data.backup
+
+import android.content.Context
+import android.net.Uri
+import eu.kanade.tachiyomi.data.backup.models.Backup
+import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
+import kotlinx.serialization.protobuf.ProtoBuf
+import okio.buffer
+import okio.gzip
+import okio.source
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+class BackupDecoder(
+ private val context: Context,
+ private val parser: ProtoBuf = Injekt.get(),
+) {
+
+ /**
+ * Decode a potentially-gzipped backup.
+ */
+ fun decode(uri: Uri): Backup {
+ return context.contentResolver.openInputStream(uri)!!.use { inputStream ->
+ val source = inputStream.source().buffer()
+
+ val peeked = source.peek().apply {
+ require(2)
+ }
+ val id1id2 = peeked.readShort()
+ val backupString = if (id1id2.toInt() == 0x1f8b) { // 0x1f8b is gzip magic bytes
+ source.gzip().buffer()
+ } else {
+ source
+ }.use { it.readByteArray() }
+
+ parser.decodeFromByteArray(BackupSerializer, backupString)
+ }
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupFileValidator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupFileValidator.kt
index fdf3d68d9..acc768e5a 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupFileValidator.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupFileValidator.kt
@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.data.backup
import android.content.Context
import android.net.Uri
import eu.kanade.tachiyomi.data.track.TrackerManager
-import eu.kanade.tachiyomi.util.BackupUtil
import tachiyomi.core.i18n.stringResource
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.i18n.MR
@@ -11,6 +10,8 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class BackupFileValidator(
+ private val context: Context,
+
private val sourceManager: SourceManager = Injekt.get(),
private val trackerManager: TrackerManager = Injekt.get(),
) {
@@ -21,9 +22,9 @@ class BackupFileValidator(
* @throws Exception if manga cannot be found.
* @return List of missing sources or missing trackers.
*/
- fun validate(context: Context, uri: Uri): Results {
+ fun validate(uri: Uri): Results {
val backup = try {
- BackupUtil.decodeBackup(context, uri)
+ BackupDecoder(context).decode(uri)
} catch (e: Exception) {
throw IllegalStateException(e)
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreateJob.kt
index 4ca6b056b..c2f1e5ded 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreateJob.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreateJob.kt
@@ -29,7 +29,6 @@ import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.storage.service.StorageManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
-import java.time.Instant
import java.util.concurrent.TimeUnit
class BackupCreateJob(private val context: Context, workerParams: WorkerParameters) :
@@ -49,13 +48,10 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
setForegroundSafely()
val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupCreateFlags.AutomaticDefaults)
- val backupPreferences = Injekt.get()
return try {
- val location = BackupCreator(context).createBackup(uri, flags, isAutoBackup)
- if (isAutoBackup) {
- backupPreferences.lastAutoBackupTimestamp().set(Instant.now().toEpochMilli())
- } else {
+ val location = BackupCreator(context, isAutoBackup).backup(uri, flags)
+ if (!isAutoBackup) {
notifier.showBackupComplete(UniFile.fromUri(context, location.toUri())!!)
}
Result.success()
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt
index 61b8a99c5..2ec832e3e 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt
@@ -3,85 +3,56 @@ package eu.kanade.tachiyomi.data.backup.create
import android.content.Context
import android.net.Uri
import com.hippo.unifile.UniFile
+import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_APP_PREFS
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_CATEGORY
-import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_CHAPTER
-import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_HISTORY
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_SOURCE_PREFS
-import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_TRACK
+import eu.kanade.tachiyomi.data.backup.create.creators.CategoriesBackupCreator
+import eu.kanade.tachiyomi.data.backup.create.creators.MangaBackupCreator
+import eu.kanade.tachiyomi.data.backup.create.creators.PreferenceBackupCreator
+import eu.kanade.tachiyomi.data.backup.create.creators.SourcesBackupCreator
import eu.kanade.tachiyomi.data.backup.models.Backup
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
-import eu.kanade.tachiyomi.data.backup.models.BackupChapter
-import eu.kanade.tachiyomi.data.backup.models.BackupHistory
import eu.kanade.tachiyomi.data.backup.models.BackupManga
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
import eu.kanade.tachiyomi.data.backup.models.BackupSource
import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
-import eu.kanade.tachiyomi.data.backup.models.BooleanPreferenceValue
-import eu.kanade.tachiyomi.data.backup.models.FloatPreferenceValue
-import eu.kanade.tachiyomi.data.backup.models.IntPreferenceValue
-import eu.kanade.tachiyomi.data.backup.models.LongPreferenceValue
-import eu.kanade.tachiyomi.data.backup.models.StringPreferenceValue
-import eu.kanade.tachiyomi.data.backup.models.StringSetPreferenceValue
-import eu.kanade.tachiyomi.data.backup.models.backupCategoryMapper
-import eu.kanade.tachiyomi.data.backup.models.backupChapterMapper
-import eu.kanade.tachiyomi.data.backup.models.backupTrackMapper
-import eu.kanade.tachiyomi.source.ConfigurableSource
-import eu.kanade.tachiyomi.source.preferenceKey
-import eu.kanade.tachiyomi.source.sourcePreferences
import kotlinx.serialization.protobuf.ProtoBuf
import logcat.LogPriority
import okio.buffer
import okio.gzip
import okio.sink
import tachiyomi.core.i18n.stringResource
-import tachiyomi.core.preference.Preference
-import tachiyomi.core.preference.PreferenceStore
import tachiyomi.core.util.system.logcat
-import tachiyomi.data.DatabaseHandler
-import tachiyomi.domain.category.interactor.GetCategories
-import tachiyomi.domain.category.model.Category
-import tachiyomi.domain.history.interactor.GetHistory
+import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.manga.interactor.GetFavorites
import tachiyomi.domain.manga.model.Manga
-import tachiyomi.domain.source.service.SourceManager
import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.FileOutputStream
+import java.text.SimpleDateFormat
+import java.time.Instant
+import java.util.Date
+import java.util.Locale
class BackupCreator(
private val context: Context,
+ private val isAutoBackup: Boolean,
+
+ private val parser: ProtoBuf = Injekt.get(),
+ private val getFavorites: GetFavorites = Injekt.get(),
+ private val backupPreferences: BackupPreferences = Injekt.get(),
+
+ private val categoriesBackupCreator: CategoriesBackupCreator = CategoriesBackupCreator(),
+ private val mangaBackupCreator: MangaBackupCreator = MangaBackupCreator(),
+ private val preferenceBackupCreator: PreferenceBackupCreator = PreferenceBackupCreator(),
+ private val sourcesBackupCreator: SourcesBackupCreator = SourcesBackupCreator(),
) {
- private val handler: DatabaseHandler = Injekt.get()
- private val sourceManager: SourceManager = Injekt.get()
- private val getCategories: GetCategories = Injekt.get()
- private val getFavorites: GetFavorites = Injekt.get()
- private val getHistory: GetHistory = Injekt.get()
- private val preferenceStore: PreferenceStore = Injekt.get()
-
- internal val parser = ProtoBuf
-
- /**
- * Create backup file.
- *
- * @param uri path of Uri
- * @param isAutoBackup backup called from scheduled backup job
- */
- suspend fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String {
- val databaseManga = getFavorites.await()
- val backup = Backup(
- backupMangas(databaseManga, flags),
- backupCategories(flags),
- emptyList(),
- prepExtensionInfoForSync(databaseManga),
- backupAppPreferences(flags),
- backupSourcePreferences(flags),
- )
-
+ suspend fun backup(uri: Uri, flags: Int): String {
var file: UniFile? = null
try {
file = (
@@ -90,14 +61,14 @@ class BackupCreator(
val dir = UniFile.fromUri(context, uri)
// Delete older backups
- dir?.listFiles { _, filename -> Backup.filenameRegex.matches(filename) }
+ dir?.listFiles { _, filename -> FILENAME_REGEX.matches(filename) }
.orEmpty()
.sortedByDescending { it.name }
.drop(MAX_AUTO_BACKUPS - 1)
.forEach { it.delete() }
// Create new file to place backup
- dir?.createFile(Backup.getFilename())
+ dir?.createFile(getFilename())
} else {
UniFile.fromUri(context, uri)
}
@@ -108,19 +79,36 @@ class BackupCreator(
throw IllegalStateException("Failed to get handle on a backup file")
}
+ val databaseManga = getFavorites.await()
+ val backup = Backup(
+ backupManga = backupMangas(databaseManga, flags),
+ backupCategories = backupCategories(flags),
+ backupSources = backupSources(databaseManga),
+ backupPreferences = backupAppPreferences(flags),
+ backupSourcePreferences = backupSourcePreferences(flags),
+ )
+
val byteArray = parser.encodeToByteArray(BackupSerializer, backup)
if (byteArray.isEmpty()) {
throw IllegalStateException(context.stringResource(MR.strings.empty_backup_error))
}
- file.openOutputStream().also {
- // Force overwrite old file
- (it as? FileOutputStream)?.channel?.truncate(0)
- }.sink().gzip().buffer().use { it.write(byteArray) }
+ file.openOutputStream()
+ .also {
+ // Force overwrite old file
+ (it as? FileOutputStream)?.channel?.truncate(0)
+ }
+ .sink().gzip().buffer().use {
+ it.write(byteArray)
+ }
val fileUri = file.uri
// Make sure it's a valid backup file
- BackupFileValidator().validate(context, fileUri)
+ BackupFileValidator(context).validate(fileUri)
+
+ if (isAutoBackup) {
+ backupPreferences.lastAutoBackupTimestamp().set(Instant.now().toEpochMilli())
+ }
return fileUri.toString()
} catch (e: Exception) {
@@ -130,135 +118,39 @@ class BackupCreator(
}
}
- fun prepExtensionInfoForSync(mangas: List): List {
- return mangas
- .asSequence()
- .map(Manga::source)
- .distinct()
- .map(sourceManager::getOrStub)
- .map(BackupSource::copyFrom)
- .toList()
- }
-
- /**
- * Backup the categories of library
- *
- * @return list of [BackupCategory] to be backed up
- */
suspend fun backupCategories(options: Int): List {
- // Check if user wants category information in backup
- return if (options and BACKUP_CATEGORY == BACKUP_CATEGORY) {
- getCategories.await()
- .filterNot(Category::isSystemCategory)
- .map(backupCategoryMapper)
- } else {
- emptyList()
- }
+ if (options and BACKUP_CATEGORY != BACKUP_CATEGORY) return emptyList()
+
+ return categoriesBackupCreator.backupCategories()
}
suspend fun backupMangas(mangas: List, flags: Int): List {
- return mangas.map {
- backupManga(it, flags)
- }
+ return mangaBackupCreator.backupMangas(mangas, flags)
}
- /**
- * Convert a manga to Json
- *
- * @param manga manga that gets converted
- * @param options options for the backup
- * @return [BackupManga] containing manga in a serializable form
- */
- suspend fun backupManga(manga: Manga, options: Int): BackupManga {
- // Entry for this manga
- val mangaObject = BackupManga.copyFrom(manga)
-
- // Check if user wants chapter information in backup
- if (options and BACKUP_CHAPTER == BACKUP_CHAPTER) {
- // Backup all the chapters
- handler.awaitList {
- chaptersQueries.getChaptersByMangaId(
- mangaId = manga.id,
- applyScanlatorFilter = 0, // false
- mapper = backupChapterMapper,
- )
- }
- .takeUnless(List::isEmpty)
- ?.let { mangaObject.chapters = it }
- }
-
- // Check if user wants category information in backup
- if (options and BACKUP_CATEGORY == BACKUP_CATEGORY) {
- // Backup categories for this manga
- val categoriesForManga = getCategories.await(manga.id)
- if (categoriesForManga.isNotEmpty()) {
- mangaObject.categories = categoriesForManga.map { it.order }
- }
- }
-
- // Check if user wants track information in backup
- if (options and BACKUP_TRACK == BACKUP_TRACK) {
- val tracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id, backupTrackMapper) }
- if (tracks.isNotEmpty()) {
- mangaObject.tracking = tracks
- }
- }
-
- // Check if user wants history information in backup
- if (options and BACKUP_HISTORY == BACKUP_HISTORY) {
- val historyByMangaId = getHistory.await(manga.id)
- if (historyByMangaId.isNotEmpty()) {
- val history = historyByMangaId.map { history ->
- val chapter = handler.awaitOne { chaptersQueries.getChapterById(history.chapterId) }
- BackupHistory(chapter.url, history.readAt?.time ?: 0L, history.readDuration)
- }
- if (history.isNotEmpty()) {
- mangaObject.history = history
- }
- }
- }
-
- return mangaObject
+ fun backupSources(mangas: List): List {
+ return sourcesBackupCreator.backupSources(mangas)
}
fun backupAppPreferences(flags: Int): List {
if (flags and BACKUP_APP_PREFS != BACKUP_APP_PREFS) return emptyList()
- return preferenceStore.getAll().toBackupPreferences()
+ return preferenceBackupCreator.backupAppPreferences()
}
fun backupSourcePreferences(flags: Int): List {
if (flags and BACKUP_SOURCE_PREFS != BACKUP_SOURCE_PREFS) return emptyList()
- return sourceManager.getCatalogueSources()
- .filterIsInstance()
- .map {
- BackupSourcePreferences(
- it.preferenceKey(),
- it.sourcePreferences().all.toBackupPreferences(),
- )
- }
+ return preferenceBackupCreator.backupSourcePreferences()
}
- @Suppress("UNCHECKED_CAST")
- private fun Map.toBackupPreferences(): List {
- return this.filterKeys {
- !Preference.isPrivate(it) && !Preference.isAppState(it)
+ companion object {
+ private const val MAX_AUTO_BACKUPS: Int = 4
+ private val FILENAME_REGEX = """${BuildConfig.APPLICATION_ID}_\d{4}-\d{2}-\d{2}_\d{2}-\d{2}.tachibk""".toRegex()
+
+ fun getFilename(): String {
+ val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.ENGLISH).format(Date())
+ return "${BuildConfig.APPLICATION_ID}_$date.tachibk"
}
- .mapNotNull { (key, value) ->
- when (value) {
- is Int -> BackupPreference(key, IntPreferenceValue(value))
- is Long -> BackupPreference(key, LongPreferenceValue(value))
- is Float -> BackupPreference(key, FloatPreferenceValue(value))
- is String -> BackupPreference(key, StringPreferenceValue(value))
- is Boolean -> BackupPreference(key, BooleanPreferenceValue(value))
- is Set<*> -> (value as? Set)?.let {
- BackupPreference(key, StringSetPreferenceValue(it))
- }
- else -> null
- }
- }
}
}
-
-private val MAX_AUTO_BACKUPS: Int = 4
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/CategoriesBackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/CategoriesBackupCreator.kt
new file mode 100644
index 000000000..e1ed56ee1
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/CategoriesBackupCreator.kt
@@ -0,0 +1,19 @@
+package eu.kanade.tachiyomi.data.backup.create.creators
+
+import eu.kanade.tachiyomi.data.backup.models.BackupCategory
+import eu.kanade.tachiyomi.data.backup.models.backupCategoryMapper
+import tachiyomi.domain.category.interactor.GetCategories
+import tachiyomi.domain.category.model.Category
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+class CategoriesBackupCreator(
+ private val getCategories: GetCategories = Injekt.get(),
+) {
+
+ suspend fun backupCategories(): List {
+ return getCategories.await()
+ .filterNot(Category::isSystemCategory)
+ .map(backupCategoryMapper)
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt
new file mode 100644
index 000000000..67182ba83
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt
@@ -0,0 +1,101 @@
+package eu.kanade.tachiyomi.data.backup.create.creators
+
+import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags
+import eu.kanade.tachiyomi.data.backup.models.BackupChapter
+import eu.kanade.tachiyomi.data.backup.models.BackupHistory
+import eu.kanade.tachiyomi.data.backup.models.BackupManga
+import eu.kanade.tachiyomi.data.backup.models.backupChapterMapper
+import eu.kanade.tachiyomi.data.backup.models.backupTrackMapper
+import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
+import tachiyomi.data.DatabaseHandler
+import tachiyomi.domain.category.interactor.GetCategories
+import tachiyomi.domain.history.interactor.GetHistory
+import tachiyomi.domain.manga.model.Manga
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+class MangaBackupCreator(
+ private val handler: DatabaseHandler = Injekt.get(),
+ private val getCategories: GetCategories = Injekt.get(),
+ private val getHistory: GetHistory = Injekt.get(),
+) {
+
+ suspend fun backupMangas(mangas: List, flags: Int): List {
+ return mangas.map {
+ backupManga(it, flags)
+ }
+ }
+
+ private suspend fun backupManga(manga: Manga, options: Int): BackupManga {
+ // Entry for this manga
+ val mangaObject = manga.toBackupManga()
+
+ // Check if user wants chapter information in backup
+ if (options and BackupCreateFlags.BACKUP_CHAPTER == BackupCreateFlags.BACKUP_CHAPTER) {
+ // Backup all the chapters
+ handler.awaitList {
+ chaptersQueries.getChaptersByMangaId(
+ mangaId = manga.id,
+ applyScanlatorFilter = 0, // false
+ mapper = backupChapterMapper,
+ )
+ }
+ .takeUnless(List::isEmpty)
+ ?.let { mangaObject.chapters = it }
+ }
+
+ // Check if user wants category information in backup
+ if (options and BackupCreateFlags.BACKUP_CATEGORY == BackupCreateFlags.BACKUP_CATEGORY) {
+ // Backup categories for this manga
+ val categoriesForManga = getCategories.await(manga.id)
+ if (categoriesForManga.isNotEmpty()) {
+ mangaObject.categories = categoriesForManga.map { it.order }
+ }
+ }
+
+ // Check if user wants track information in backup
+ if (options and BackupCreateFlags.BACKUP_TRACK == BackupCreateFlags.BACKUP_TRACK) {
+ val tracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id, backupTrackMapper) }
+ if (tracks.isNotEmpty()) {
+ mangaObject.tracking = tracks
+ }
+ }
+
+ // Check if user wants history information in backup
+ if (options and BackupCreateFlags.BACKUP_HISTORY == BackupCreateFlags.BACKUP_HISTORY) {
+ val historyByMangaId = getHistory.await(manga.id)
+ if (historyByMangaId.isNotEmpty()) {
+ val history = historyByMangaId.map { history ->
+ val chapter = handler.awaitOne { chaptersQueries.getChapterById(history.chapterId) }
+ BackupHistory(chapter.url, history.readAt?.time ?: 0L, history.readDuration)
+ }
+ if (history.isNotEmpty()) {
+ mangaObject.history = history
+ }
+ }
+ }
+
+ return mangaObject
+ }
+}
+
+private fun Manga.toBackupManga() =
+ BackupManga(
+ url = this.url,
+ title = this.title,
+ artist = this.artist,
+ author = this.author,
+ description = this.description,
+ genre = this.genre.orEmpty(),
+ status = this.status.toInt(),
+ thumbnailUrl = this.thumbnailUrl,
+ favorite = this.favorite,
+ source = this.source,
+ dateAdded = this.dateAdded,
+ viewer = (this.viewerFlags.toInt() and ReadingMode.MASK),
+ viewer_flags = this.viewerFlags.toInt(),
+ chapterFlags = this.chapterFlags.toInt(),
+ updateStrategy = this.updateStrategy,
+ lastModifiedAt = this.lastModifiedAt,
+ favoriteModifiedAt = this.favoriteModifiedAt,
+ )
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/PreferenceBackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/PreferenceBackupCreator.kt
new file mode 100644
index 000000000..c75612de9
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/PreferenceBackupCreator.kt
@@ -0,0 +1,59 @@
+package eu.kanade.tachiyomi.data.backup.create.creators
+
+import eu.kanade.tachiyomi.data.backup.models.BackupPreference
+import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
+import eu.kanade.tachiyomi.data.backup.models.BooleanPreferenceValue
+import eu.kanade.tachiyomi.data.backup.models.FloatPreferenceValue
+import eu.kanade.tachiyomi.data.backup.models.IntPreferenceValue
+import eu.kanade.tachiyomi.data.backup.models.LongPreferenceValue
+import eu.kanade.tachiyomi.data.backup.models.StringPreferenceValue
+import eu.kanade.tachiyomi.data.backup.models.StringSetPreferenceValue
+import eu.kanade.tachiyomi.source.ConfigurableSource
+import eu.kanade.tachiyomi.source.preferenceKey
+import eu.kanade.tachiyomi.source.sourcePreferences
+import tachiyomi.core.preference.Preference
+import tachiyomi.core.preference.PreferenceStore
+import tachiyomi.domain.source.service.SourceManager
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+class PreferenceBackupCreator(
+ private val sourceManager: SourceManager = Injekt.get(),
+ private val preferenceStore: PreferenceStore = Injekt.get(),
+) {
+
+ fun backupAppPreferences(): List {
+ return preferenceStore.getAll().toBackupPreferences()
+ }
+
+ fun backupSourcePreferences(): List {
+ return sourceManager.getCatalogueSources()
+ .filterIsInstance()
+ .map {
+ BackupSourcePreferences(
+ it.preferenceKey(),
+ it.sourcePreferences().all.toBackupPreferences(),
+ )
+ }
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ private fun Map.toBackupPreferences(): List {
+ return this.filterKeys {
+ !Preference.isPrivate(it) && !Preference.isAppState(it)
+ }
+ .mapNotNull { (key, value) ->
+ when (value) {
+ is Int -> BackupPreference(key, IntPreferenceValue(value))
+ is Long -> BackupPreference(key, LongPreferenceValue(value))
+ is Float -> BackupPreference(key, FloatPreferenceValue(value))
+ is String -> BackupPreference(key, StringPreferenceValue(value))
+ is Boolean -> BackupPreference(key, BooleanPreferenceValue(value))
+ is Set<*> -> (value as? Set)?.let {
+ BackupPreference(key, StringSetPreferenceValue(it))
+ }
+ else -> null
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/SourcesBackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/SourcesBackupCreator.kt
new file mode 100644
index 000000000..075e449a7
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/SourcesBackupCreator.kt
@@ -0,0 +1,29 @@
+package eu.kanade.tachiyomi.data.backup.create.creators
+
+import eu.kanade.tachiyomi.data.backup.models.BackupSource
+import eu.kanade.tachiyomi.source.Source
+import tachiyomi.domain.manga.model.Manga
+import tachiyomi.domain.source.service.SourceManager
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+class SourcesBackupCreator(
+ private val sourceManager: SourceManager = Injekt.get(),
+) {
+
+ fun backupSources(mangas: List): List {
+ return mangas
+ .asSequence()
+ .map(Manga::source)
+ .distinct()
+ .map(sourceManager::getOrStub)
+ .map { it.toBackupSource() }
+ .toList()
+ }
+}
+
+private fun Source.toBackupSource() =
+ BackupSource(
+ name = this.name,
+ sourceId = this.id,
+ )
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt
index 0bfe17e59..cdc5c4ad2 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt
@@ -1,11 +1,11 @@
package eu.kanade.tachiyomi.data.backup.models
-import eu.kanade.tachiyomi.BuildConfig
import kotlinx.serialization.Serializable
+import kotlinx.serialization.Serializer
import kotlinx.serialization.protobuf.ProtoNumber
-import java.text.SimpleDateFormat
-import java.util.Date
-import java.util.Locale
+
+@Serializer(forClass = Backup::class)
+object BackupSerializer
@Serializable
data class Backup(
@@ -15,14 +15,4 @@ data class Backup(
@ProtoNumber(101) var backupSources: List = emptyList(),
@ProtoNumber(104) var backupPreferences: List = emptyList(),
@ProtoNumber(105) var backupSourcePreferences: List = emptyList(),
-) {
-
- companion object {
- val filenameRegex = """${BuildConfig.APPLICATION_ID}_\d+-\d+-\d+_\d+-\d+.tachibk""".toRegex()
-
- fun getFilename(): String {
- val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
- return "${BuildConfig.APPLICATION_ID}_$date.tachibk"
- }
- }
-}
+)
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt
index 003b1ae19..43a8a906c 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt
@@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.data.backup.models
import eu.kanade.tachiyomi.source.model.UpdateStrategy
-import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
import tachiyomi.domain.manga.model.Manga
@@ -60,28 +59,4 @@ data class BackupManga(
favoriteModifiedAt = this@BackupManga.favoriteModifiedAt,
)
}
-
- companion object {
- fun copyFrom(manga: Manga): BackupManga {
- return BackupManga(
- url = manga.url,
- title = manga.title,
- artist = manga.artist,
- author = manga.author,
- description = manga.description,
- genre = manga.genre.orEmpty(),
- status = manga.status.toInt(),
- thumbnailUrl = manga.thumbnailUrl,
- favorite = manga.favorite,
- source = manga.source,
- dateAdded = manga.dateAdded,
- viewer = (manga.viewerFlags.toInt() and ReadingMode.MASK),
- viewer_flags = manga.viewerFlags.toInt(),
- chapterFlags = manga.chapterFlags.toInt(),
- updateStrategy = manga.updateStrategy,
- lastModifiedAt = manga.lastModifiedAt,
- favoriteModifiedAt = manga.favoriteModifiedAt,
- )
- }
- }
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupSerializer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupSerializer.kt
deleted file mode 100644
index 2e79ebecd..000000000
--- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupSerializer.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package eu.kanade.tachiyomi.data.backup.models
-
-import kotlinx.serialization.Serializer
-
-@Serializer(forClass = Backup::class)
-object BackupSerializer
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupSource.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupSource.kt
index 34e4cac31..bfd2c93bf 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupSource.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupSource.kt
@@ -1,9 +1,14 @@
package eu.kanade.tachiyomi.data.backup.models
-import eu.kanade.tachiyomi.source.Source
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
+@Serializable
+data class BackupSource(
+ @ProtoNumber(1) var name: String = "",
+ @ProtoNumber(2) var sourceId: Long,
+)
+
@Serializable
data class BrokenBackupSource(
@ProtoNumber(0) var name: String = "",
@@ -11,18 +16,3 @@ data class BrokenBackupSource(
) {
fun toBackupSource() = BackupSource(name, sourceId)
}
-
-@Serializable
-data class BackupSource(
- @ProtoNumber(1) var name: String = "",
- @ProtoNumber(2) var sourceId: Long,
-) {
- companion object {
- fun copyFrom(source: Source): BackupSource {
- return BackupSource(
- name = source.name,
- sourceId = source.id,
- )
- }
- }
-}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupTracking.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupTracking.kt
index 35d486492..910a8adac 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupTracking.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupTracking.kt
@@ -7,7 +7,6 @@ import tachiyomi.domain.track.model.Track
@Serializable
data class BackupTracking(
// in 1.x some of these values have different types or names
- // syncId is called siteId in 1,x
@ProtoNumber(1) var syncId: Int,
// LibraryId is not null in 1.x
@ProtoNumber(2) var libraryId: Long,
@@ -34,7 +33,7 @@ data class BackupTracking(
return Track(
id = -1,
mangaId = -1,
- syncId = this@BackupTracking.syncId.toLong(),
+ trackerId = this@BackupTracking.syncId.toLong(),
remoteId = if (this@BackupTracking.mediaIdInt != 0) {
this@BackupTracking.mediaIdInt.toLong()
} else {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestoreJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestoreJob.kt
index 9883eb023..f1b3a28c9 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestoreJob.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestoreJob.kt
@@ -30,13 +30,19 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet
override suspend fun doWork(): Result {
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri()
- ?: return Result.failure()
+ val options = inputData.getBooleanArray(OPTIONS_KEY)
+ ?.let { RestoreOptions.fromBooleanArray(it) }
+
+ if (uri == null || options == null) {
+ return Result.failure()
+ }
+
val isSync = inputData.getBoolean(SYNC_KEY, false)
setForegroundSafely()
return try {
- BackupRestorer(context, notifier, isSync).restore(uri)
+ BackupRestorer(context, notifier, isSync).restore(uri, options)
Result.success()
} catch (e: Exception) {
if (e is CancellationException) {
@@ -69,10 +75,16 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet
return context.workManager.isRunning(TAG)
}
- fun start(context: Context, uri: Uri, sync: Boolean = false) {
+ fun start(
+ context: Context,
+ uri: Uri,
+ options: RestoreOptions,
+ sync: Boolean = false,
+ ) {
val inputData = workDataOf(
LOCATION_URI_KEY to uri.toString(),
SYNC_KEY to sync,
+ OPTIONS_KEY to options.toBooleanArray(),
)
val request = OneTimeWorkRequestBuilder()
.addTag(TAG)
@@ -91,3 +103,4 @@ private const val TAG = "BackupRestore"
private const val LOCATION_URI_KEY = "location_uri" // String
private const val SYNC_KEY = "sync" // Boolean
+private const val OPTIONS_KEY = "options" // BooleanArray
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt
index 636811ce5..eb71fa7e5 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt
@@ -2,12 +2,15 @@ package eu.kanade.tachiyomi.data.backup.restore
import android.content.Context
import android.net.Uri
+import eu.kanade.tachiyomi.data.backup.BackupDecoder
import eu.kanade.tachiyomi.data.backup.BackupNotifier
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
import eu.kanade.tachiyomi.data.backup.models.BackupManga
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
-import eu.kanade.tachiyomi.util.BackupUtil
+import eu.kanade.tachiyomi.data.backup.restore.restorers.CategoriesRestorer
+import eu.kanade.tachiyomi.data.backup.restore.restorers.MangaRestorer
+import eu.kanade.tachiyomi.data.backup.restore.restorers.PreferenceRestorer
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.coroutineScope
@@ -39,10 +42,10 @@ class BackupRestorer(
*/
private var sourceMapping: Map = emptyMap()
- suspend fun restore(uri: Uri) {
+ suspend fun restore(uri: Uri, options: RestoreOptions) {
val startTime = System.currentTimeMillis()
- restoreFromFile(uri)
+ restoreFromFile(uri, options)
val time = System.currentTimeMillis() - startTime
@@ -57,20 +60,36 @@ class BackupRestorer(
)
}
- private suspend fun restoreFromFile(uri: Uri) {
- val backup = BackupUtil.decodeBackup(context, uri)
-
- restoreAmount = backup.backupManga.size + 3 // +3 for categories, app prefs, source prefs
+ private suspend fun restoreFromFile(uri: Uri, options: RestoreOptions) {
+ val backup = BackupDecoder(context).decode(uri)
// Store source mapping for error messages
val backupMaps = backup.backupSources + backup.backupBrokenSources.map { it.toBackupSource() }
sourceMapping = backupMaps.associate { it.sourceId to it.name }
+ if (options.library) {
+ restoreAmount += backup.backupManga.size + 1 // +1 for categories
+ }
+ if (options.appSettings) {
+ restoreAmount += 1
+ }
+ if (options.sourceSettings) {
+ restoreAmount += 1
+ }
+
coroutineScope {
- restoreCategories(backup.backupCategories)
- restoreAppPreferences(backup.backupPreferences)
- restoreSourcePreferences(backup.backupSourcePreferences)
- restoreManga(backup.backupManga, backup.backupCategories)
+ if (options.library) {
+ restoreCategories(backup.backupCategories)
+ }
+ if (options.appSettings) {
+ restoreAppPreferences(backup.backupPreferences)
+ }
+ if (options.sourceSettings) {
+ restoreSourcePreferences(backup.backupSourcePreferences)
+ }
+ if (options.library) {
+ restoreManga(backup.backupManga, backup.backupCategories)
+ }
// TODO: optionally trigger online library + tracker update
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/RestoreOptions.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/RestoreOptions.kt
new file mode 100644
index 000000000..d9e124a81
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/RestoreOptions.kt
@@ -0,0 +1,17 @@
+package eu.kanade.tachiyomi.data.backup.restore
+
+data class RestoreOptions(
+ val appSettings: Boolean = true,
+ val sourceSettings: Boolean = true,
+ val library: Boolean = true,
+) {
+ fun toBooleanArray() = booleanArrayOf(appSettings, sourceSettings, library)
+
+ companion object {
+ fun fromBooleanArray(booleanArray: BooleanArray) = RestoreOptions(
+ appSettings = booleanArray[0],
+ sourceSettings = booleanArray[1],
+ library = booleanArray[2],
+ )
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/CategoriesRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/CategoriesRestorer.kt
similarity index 95%
rename from app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/CategoriesRestorer.kt
rename to app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/CategoriesRestorer.kt
index 5557bb59f..f98af1045 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/CategoriesRestorer.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/CategoriesRestorer.kt
@@ -1,4 +1,4 @@
-package eu.kanade.tachiyomi.data.backup.restore
+package eu.kanade.tachiyomi.data.backup.restore.restorers
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
import tachiyomi.data.DatabaseHandler
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/MangaRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt
similarity index 98%
rename from app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/MangaRestorer.kt
rename to app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt
index b130039e1..f514af7f1 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/MangaRestorer.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt
@@ -1,4 +1,4 @@
-package eu.kanade.tachiyomi.data.backup.restore
+package eu.kanade.tachiyomi.data.backup.restore.restorers
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
@@ -329,7 +329,7 @@ class MangaRestorer(
readAt = max(item.readAt?.time ?: 0L, dbHistory.last_read?.time ?: 0L)
.takeIf { it > 0L }
?.let { Date(it) },
- readDuration = max(item.readDuration, dbHistory.time_read),
+ readDuration = max(item.readDuration, dbHistory.time_read) - dbHistory.time_read,
)
}
@@ -347,12 +347,12 @@ class MangaRestorer(
}
private suspend fun restoreTracking(manga: Manga, backupTracks: List) {
- val dbTrackBySyncId = getTracks.await(manga.id).associateBy { it.syncId }
+ val dbTrackByTrackerId = getTracks.await(manga.id).associateBy { it.trackerId }
val (existingTracks, newTracks) = backupTracks
.mapNotNull {
val track = it.getTrackImpl()
- val dbTrack = dbTrackBySyncId[track.syncId]
+ val dbTrack = dbTrackByTrackerId[track.trackerId]
?: // New track
return@mapNotNull track.copy(
id = 0, // Let DB assign new ID
@@ -381,7 +381,7 @@ class MangaRestorer(
existingTracks.forEach { track ->
manga_syncQueries.update(
track.mangaId,
- track.syncId,
+ track.trackerId,
track.remoteId,
track.libraryId,
track.title,
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/PreferenceRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/PreferenceRestorer.kt
similarity index 98%
rename from app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/PreferenceRestorer.kt
rename to app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/PreferenceRestorer.kt
index 69622d60b..1062937d4 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/PreferenceRestorer.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/PreferenceRestorer.kt
@@ -1,4 +1,4 @@
-package eu.kanade.tachiyomi.data.backup.restore
+package eu.kanade.tachiyomi.data.backup.restore.restorers
import android.content.Context
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt
index 3f155e7f5..56d6262c9 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt
@@ -14,7 +14,6 @@ import okio.buffer
import okio.sink
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.chapter.model.Chapter
-import uy.kohesive.injekt.injectLazy
import java.io.File
import java.io.IOException
@@ -26,9 +25,10 @@ import java.io.IOException
*
* @param context the application context.
*/
-class ChapterCache(private val context: Context) {
-
- private val json: Json by injectLazy()
+class ChapterCache(
+ private val context: Context,
+ private val json: Json,
+) {
/** Cache class used for cache management. */
private val diskCache = DiskLruCache.open(
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Track.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Track.kt
index 38df114e2..9d7f98983 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Track.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Track.kt
@@ -8,9 +8,9 @@ interface Track : Serializable {
var manga_id: Long
- var sync_id: Int
+ var tracker_id: Int
- var media_id: Long
+ var remote_id: Long
var library_id: Long?
@@ -40,7 +40,7 @@ interface Track : Serializable {
companion object {
fun create(serviceId: Long): Track = TrackImpl().apply {
- sync_id = serviceId.toInt()
+ tracker_id = serviceId.toInt()
}
}
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/TrackImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/TrackImpl.kt
index a83a5f7a7..2a0abce31 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/TrackImpl.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/TrackImpl.kt
@@ -6,9 +6,9 @@ class TrackImpl : Track {
override var manga_id: Long = 0
- override var sync_id: Int = 0
+ override var tracker_id: Int = 0
- override var media_id: Long = 0
+ override var remote_id: Long = 0
override var library_id: Long? = null
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt
index e29628878..e1339f986 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt
@@ -25,7 +25,7 @@ class DownloadProvider(
private val storageManager: StorageManager = Injekt.get(),
) {
- val downloadsDir: UniFile?
+ private val downloadsDir: UniFile?
get() = storageManager.getDownloadsDirectory()
/**
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt
index 30f11c4d1..8b54cfabd 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt
@@ -59,6 +59,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.BufferedOutputStream
import java.io.File
+import java.util.Locale
import java.util.zip.CRC32
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
@@ -422,7 +423,7 @@ class Downloader(
}
val digitCount = (download.pages?.size ?: 0).toString().length.coerceAtLeast(3)
- val filename = String.format("%0${digitCount}d", page.number)
+ val filename = "%0${digitCount}d".format(Locale.ENGLISH, page.number)
val tmpFile = tmpDir.findFile("$filename.tmp")
// Delete temp file if it exists
@@ -537,7 +538,7 @@ class Downloader(
if (!downloadPreferences.splitTallImages().get()) return
try {
- val filenamePrefix = String.format("%03d", page.number)
+ val filenamePrefix = "%03d".format(Locale.ENGLISH, page.number)
val imageFile = tmpDir.listFiles()?.firstOrNull { it.name.orEmpty().startsWith(filenamePrefix) }
?: error(context.stringResource(MR.strings.download_notifier_split_page_not_found, page.number))
@@ -579,11 +580,7 @@ class Downloader(
else -> true
}
}
- if (downloadedImagesCount != downloadPageCount) {
- return false
- }
-
- return true
+ return downloadedImagesCount == downloadPageCount
}
/**
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt
index 7e0f89966..c7a714a01 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt
@@ -235,7 +235,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
.map { (reason, entries) -> "$reason: [${entries.map { it.first.title }.sorted().joinToString()}]" }
.joinToString()
}
- notifier.showUpdateSkippedNotification(skippedUpdates.size)
}
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt
index d9e8dc54a..06d61589d 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt
@@ -139,27 +139,6 @@ class LibraryUpdateNotifier(private val context: Context) {
}
}
- /**
- * Shows notification containing update entries that were skipped.
- *
- * @param skipped Number of entries that were skipped during the update.
- */
- fun showUpdateSkippedNotification(skipped: Int) {
- if (skipped == 0) {
- return
- }
-
- context.notify(
- Notifications.ID_LIBRARY_SKIPPED,
- Notifications.CHANNEL_LIBRARY_SKIPPED,
- ) {
- setContentTitle(context.stringResource(MR.strings.notification_update_skipped, skipped))
- setContentText(context.stringResource(MR.strings.learn_more))
- setSmallIcon(R.drawable.ic_tachi)
- setContentIntent(NotificationHandler.openUrl(context, HELP_SKIPPED_URL))
- }
- }
-
/**
* Shows the notification containing the result of the update done by the service.
*
@@ -246,7 +225,7 @@ class LibraryUpdateNotifier(private val context: Context) {
// Mark chapters as read action
addAction(
- R.drawable.ic_glasses_24dp,
+ R.drawable.ic_done_24dp,
context.stringResource(MR.strings.action_mark_as_read),
NotificationReceiver.markAsReadPendingBroadcast(
context,
@@ -385,4 +364,3 @@ class LibraryUpdateNotifier(private val context: Context) {
private const val NOTIF_MAX_CHAPTERS = 5
private const val NOTIF_TITLE_MAX_LEN = 45
private const val NOTIF_ICON_SIZE = 192
-private const val HELP_SKIPPED_URL = "https://tachiyomi.org/docs/faq/library#why-is-global-update-skipping-entries"
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt
index 12e8b96c6..fddd669d5 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt
@@ -247,7 +247,6 @@ class NotificationReceiver : BroadcastReceiver() {
private const val NAME = "NotificationReceiver"
private const val ACTION_SHARE_IMAGE = "$ID.$NAME.SHARE_IMAGE"
- private const val ACTION_DELETE_IMAGE = "$ID.$NAME.DELETE_IMAGE"
private const val ACTION_SHARE_BACKUP = "$ID.$NAME.SEND_BACKUP"
@@ -270,7 +269,6 @@ class NotificationReceiver : BroadcastReceiver() {
private const val ACTION_DISMISS_NOTIFICATION = "$ID.$NAME.ACTION_DISMISS_NOTIFICATION"
- private const val EXTRA_FILE_LOCATION = "$ID.$NAME.FILE_LOCATION"
private const val EXTRA_URI = "$ID.$NAME.URI"
private const val EXTRA_NOTIFICATION_ID = "$ID.$NAME.NOTIFICATION_ID"
private const val EXTRA_GROUP_ID = "$ID.$NAME.EXTRA_GROUP_ID"
@@ -375,7 +373,7 @@ class NotificationReceiver : BroadcastReceiver() {
it.id == notificationId
}?.groupKey
- if (groupId != null && groupId != 0 && groupKey != null && groupKey.isNotEmpty()) {
+ if (groupId != null && groupId != 0 && !groupKey.isNullOrEmpty()) {
val notifications = context.notificationManager.activeNotifications.filter {
it.groupKey == groupKey
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt
index 3a76b045d..7632d06db 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt
@@ -30,8 +30,6 @@ object Notifications {
const val ID_LIBRARY_SIZE_WARNING = -103
const val CHANNEL_LIBRARY_ERROR = "library_errors_channel"
const val ID_LIBRARY_ERROR = -102
- const val CHANNEL_LIBRARY_SKIPPED = "library_skipped_channel"
- const val ID_LIBRARY_SKIPPED = -104
/**
* Notification channel and ids used by the downloader.
@@ -86,6 +84,7 @@ object Notifications {
"updates_ext_channel",
"downloader_cache_renewal",
"crash_logs_channel",
+ "library_skipped_channel",
)
/**
@@ -132,11 +131,6 @@ object Notifications {
setGroup(GROUP_LIBRARY)
setShowBadge(false)
},
- buildNotificationChannel(CHANNEL_LIBRARY_SKIPPED, IMPORTANCE_LOW) {
- setName(context.stringResource(MR.strings.channel_skipped))
- setGroup(GROUP_LIBRARY)
- setShowBadge(false)
- },
buildNotificationChannel(CHANNEL_NEW_CHAPTERS, IMPORTANCE_DEFAULT) {
setName(context.stringResource(MR.strings.channel_new_chapters))
},
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncManager.kt
index b1801297f..414218cc7 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncManager.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncManager.kt
@@ -9,7 +9,8 @@ import eu.kanade.tachiyomi.data.backup.models.BackupChapter
import eu.kanade.tachiyomi.data.backup.models.BackupManga
import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
-import eu.kanade.tachiyomi.data.backup.restore.MangaRestorer
+import eu.kanade.tachiyomi.data.backup.restore.RestoreOptions
+import eu.kanade.tachiyomi.data.backup.restore.restorers.MangaRestorer
import eu.kanade.tachiyomi.data.sync.service.GoogleDriveSyncService
import eu.kanade.tachiyomi.data.sync.service.SyncData
import eu.kanade.tachiyomi.data.sync.service.SyncYomiSyncService
@@ -21,6 +22,7 @@ import tachiyomi.data.Chapters
import tachiyomi.data.DatabaseHandler
import tachiyomi.data.manga.MangaMapper.mapManga
import tachiyomi.domain.category.interactor.GetCategories
+import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.manga.interactor.GetFavorites
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.sync.SyncPreferences
@@ -48,7 +50,7 @@ class SyncManager(
private val getFavorites: GetFavorites = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(),
) {
- private val backupCreator: BackupCreator = BackupCreator(context)
+ private val backupCreator: BackupCreator = BackupCreator(context, false)
private val notifier: SyncNotifier = SyncNotifier(context)
private val mangaRestorer: MangaRestorer = MangaRestorer()
@@ -72,12 +74,11 @@ class SyncManager(
suspend fun syncData() {
val databaseManga = getAllMangaFromDB()
val backup = Backup(
- backupCreator.backupMangas(databaseManga, BackupCreateFlags.AutomaticDefaults),
- backupCreator.backupCategories(BackupCreateFlags.AutomaticDefaults),
- emptyList(),
- backupCreator.prepExtensionInfoForSync(databaseManga),
- backupCreator.backupAppPreferences(BackupCreateFlags.AutomaticDefaults),
- backupCreator.backupSourcePreferences(BackupCreateFlags.AutomaticDefaults),
+ backupManga = backupCreator.backupMangas(databaseManga, BackupCreateFlags.AutomaticDefaults),
+ backupCategories = backupCreator.backupCategories(BackupCreateFlags.AutomaticDefaults),
+ backupSources = backupCreator.backupSources(databaseManga),
+ backupPreferences = backupCreator.backupAppPreferences(BackupCreateFlags.AutomaticDefaults),
+ backupSourcePreferences = backupCreator.backupSourcePreferences(BackupCreateFlags.AutomaticDefaults),
)
// Create the SyncData object
@@ -116,6 +117,8 @@ class SyncManager(
backupManga = filteredFavorites,
backupCategories = remoteBackup.backupCategories,
backupSources = remoteBackup.backupSources,
+ backupPreferences = remoteBackup.backupPreferences,
+ backupSourcePreferences = remoteBackup.backupSourcePreferences,
)
// It's local sync no need to restore data. (just update remote data)
@@ -128,7 +131,16 @@ class SyncManager(
val backupUri = writeSyncDataToCache(context, newSyncData)
logcat(LogPriority.DEBUG) { "Got Backup Uri: $backupUri" }
if (backupUri != null) {
- BackupRestoreJob.start(context, backupUri, sync = true)
+ BackupRestoreJob.start(
+ context,
+ backupUri,
+ sync = true,
+ options = RestoreOptions(
+ appSettings = true,
+ sourceSettings = true,
+ library = true,
+ ),
+ )
} else {
logcat(LogPriority.ERROR) { "Failed to write sync data to file" }
}
@@ -158,58 +170,73 @@ class SyncManager(
}
/**
- * Compares two Manga objects (one from the local database and one from the backup) to check if they are different.
- * @param localManga the Manga object from the local database.
- * @param remoteManga the BackupManga object from the backup.
- * @return true if the Manga objects are different, otherwise false.
+ * Determines if there are differences between the local manga details and categories and their corresponding
+ * remote backup versions. This is used to identify changes or updates that might have occurred.
+ *
+ * @param localManga The manga object from the local database of type [Manga].
+ * @param backupManga The manga object from the remote backup of type [BackupManga].
+ *
+ * @return Boolean indicating whether there are differences. Returns true if any of the following is true:
+ * - The local manga details differ from the remote backup manga details.
+ * - The chapters of the local manga differ from the chapters of the remote backup manga.
+ * - The categories of the local manga differ from the categories of the remote backup manga.
+ *
+ * This function uses a combination of direct property comparisons and delegated comparison functions
+ * to assess differences across manga details, chapters, and categories. It relies on proper conversion
+ * of remote manga and chapters to the local format for accurate comparison.
*/
- private suspend fun isMangaDifferent(localManga: Manga, remoteManga: BackupManga): Boolean {
+ private suspend fun isMangaDifferent(localManga: Manga, backupManga: BackupManga): Boolean {
+ val remoteManga = backupManga.getMangaImpl()
val localChapters = handler.await { chaptersQueries.getChaptersByMangaId(localManga.id, 0).executeAsList() }
val localCategories = getCategories.await(localManga.id).map { it.order }
- return localManga.source != remoteManga.source ||
- localManga.url != remoteManga.url ||
- localManga.title != remoteManga.title ||
- localManga.artist != remoteManga.artist ||
- localManga.author != remoteManga.author ||
- localManga.description != remoteManga.description ||
- localManga.genre != remoteManga.genre ||
- localManga.status.toInt() != remoteManga.status ||
- localManga.thumbnailUrl != remoteManga.thumbnailUrl ||
- localManga.dateAdded != remoteManga.dateAdded ||
- localManga.chapterFlags.toInt() != remoteManga.chapterFlags ||
- localManga.favorite != remoteManga.favorite ||
- localManga.viewerFlags.toInt() != remoteManga.viewer_flags ||
- localManga.updateStrategy != remoteManga.updateStrategy ||
- areChaptersDifferent(localChapters, remoteManga.chapters) ||
- localCategories != remoteManga.categories
+ return localManga != remoteManga || areChaptersDifferent(localChapters, backupManga.chapters) || localCategories != backupManga.categories
}
/**
- * Compares two lists of chapters (one from the local database and one from the backup) to check if they are different.
- * @param localChapters the list of chapters from the local database.
- * @param remoteChapters the list of BackupChapter objects from the backup.
- * @return true if the lists of chapters are different, otherwise false.
+ * Checks if there are any differences between a list of local chapters and a list of backup chapters.
+ * This function is used to determine if updates or changes have occurred between the two sets of chapters.
+ *
+ * @param localChapters The list of chapters from the local source, of type [Chapters].
+ * @param remoteChapters The list of chapters from the remote backup source, of type [BackupChapter].
+ *
+ * @return Boolean indicating whether there are differences. Returns true if any of the following is true:
+ * - The count of local and remote chapters differs.
+ * - Any corresponding chapters (matched by URL) have differing attributes including name, scanlator,
+ * read status, bookmark status, last page read, chapter number, source order, fetch date, upload date,
+ * or last modified date.
+ *
+ * Each chapter is compared based on a set of fields that define its content and state. If any of these fields
+ * differ between the local chapter and its corresponding remote chapter, it is considered a difference.
+ *
*/
private fun areChaptersDifferent(localChapters: List, remoteChapters: List): Boolean {
+ // Early return if the sizes are different
if (localChapters.size != remoteChapters.size) {
return true
}
+ // Convert all remote chapters to Chapter instances
+ val convertedRemoteChapters = remoteChapters.map { it.toChapterImpl() }
+
+ // Create a map for the local chapters for efficient comparison
val localChapterMap = localChapters.associateBy { it.url }
- return remoteChapters.any { remoteChapter ->
- localChapterMap[remoteChapter.url]?.let { localChapter ->
+ // Check for any differences
+ return convertedRemoteChapters.any { remoteChapter ->
+ val localChapter = localChapterMap[remoteChapter.url]
+ localChapter == null || // No corresponding local chapter
+ localChapter.url != remoteChapter.url ||
localChapter.name != remoteChapter.name ||
- localChapter.scanlator != remoteChapter.scanlator ||
- localChapter.read != remoteChapter.read ||
- localChapter.bookmark != remoteChapter.bookmark ||
- localChapter.last_page_read != remoteChapter.lastPageRead ||
- localChapter.date_fetch != remoteChapter.dateFetch ||
- localChapter.date_upload != remoteChapter.dateUpload ||
- localChapter.chapter_number.toFloat() != remoteChapter.chapterNumber ||
- localChapter.source_order != remoteChapter.sourceOrder
- } ?: true
+ localChapter.scanlator != remoteChapter.scanlator ||
+ localChapter.read != remoteChapter.read ||
+ localChapter.bookmark != remoteChapter.bookmark ||
+ localChapter.last_page_read != remoteChapter.lastPageRead ||
+ localChapter.chapter_number != remoteChapter.chapterNumber ||
+ localChapter.source_order != remoteChapter.sourceOrder ||
+ localChapter.date_fetch != remoteChapter.dateFetch ||
+ localChapter.date_upload != remoteChapter.dateUpload ||
+ localChapter.last_modified_at != remoteChapter.lastModifiedAt
}
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/DeletableTracker.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/DeletableTracker.kt
index c61c55e78..900163926 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/track/DeletableTracker.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/DeletableTracker.kt
@@ -1,11 +1,11 @@
package eu.kanade.tachiyomi.data.track
-import eu.kanade.tachiyomi.data.database.models.Track
+import tachiyomi.domain.track.model.Track
/**
* Tracker that support deleting am entry from a user's list.
*/
interface DeletableTracker {
- suspend fun delete(track: Track): Track
+ suspend fun delete(track: Track)
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/Tracker.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/Tracker.kt
index 3b3223de9..59cf64648 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/track/Tracker.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/Tracker.kt
@@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.collections.immutable.ImmutableList
import okhttp3.OkHttpClient
+import tachiyomi.domain.track.model.Track as DomainTrack
interface Tracker {
@@ -39,11 +40,11 @@ interface Tracker {
fun getScoreList(): ImmutableList
// TODO: Store all scores as 10 point in the future maybe?
- fun get10PointScore(track: tachiyomi.domain.track.model.Track): Double
+ fun get10PointScore(track: DomainTrack): Double
fun indexToScore(index: Int): Float
- fun displayScore(track: Track): String
+ fun displayScore(track: DomainTrack): String
suspend fun update(track: Track, didReadChapter: Boolean = false): Track
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackerManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackerManager.kt
index a62ad4401..598a0c06c 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackerManager.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackerManager.kt
@@ -33,6 +33,4 @@ class TrackerManager {
fun loggedInTrackers() = trackers.filter { it.isLoggedIn }
fun get(id: Long) = trackers.find { it.id == id }
-
- fun hasLoggedIn() = trackers.any { it.isLoggedIn }
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt
index 95191bc00..93fd75a45 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.track.anilist
import android.graphics.Color
import dev.icerock.moko.resources.StringResource
+import eu.kanade.domain.track.model.toDbTrack
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.BaseTracker
@@ -120,16 +121,16 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
}
}
- override fun displayScore(track: Track): String {
+ override fun displayScore(track: DomainTrack): String {
val score = track.score
return when (scorePreference.get()) {
POINT_5 -> when (score) {
- 0f -> "0 ★"
+ 0.0 -> "0 ★"
else -> "${((score + 10) / 20).toInt()} ★"
}
POINT_3 -> when {
- score == 0f -> "0"
+ score == 0.0 -> "0"
score <= 35 -> "😦"
score <= 60 -> "😐"
else -> "😊"
@@ -167,13 +168,13 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
return api.updateLibManga(track)
}
- override suspend fun delete(track: Track): Track {
- if (track.library_id == null || track.library_id!! == 0L) {
- val libManga = api.findLibManga(track, getUsername().toInt()) ?: return track
- track.library_id = libManga.library_id
+ override suspend fun delete(track: DomainTrack) {
+ if (track.libraryId == null || track.libraryId == 0L) {
+ val libManga = api.findLibManga(track.toDbTrack(), getUsername().toInt()) ?: return
+ return api.deleteLibManga(track.copy(id = libManga.library_id!!))
}
- return api.deleteLibManga(track)
+ api.deleteLibManga(track)
}
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt
index c385d7614..de6ef2f8e 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt
@@ -31,6 +31,7 @@ import java.time.LocalDate
import java.time.ZoneId
import java.time.ZonedDateTime
import kotlin.time.Duration.Companion.minutes
+import tachiyomi.domain.track.model.Track as DomainTrack
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
@@ -55,7 +56,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
val payload = buildJsonObject {
put("query", query)
putJsonObject("variables") {
- put("mangaId", track.media_id)
+ put("mangaId", track.remote_id)
put("progress", track.last_chapter_read.toInt())
put("status", track.toAnilistStatus())
}
@@ -113,8 +114,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
}
}
- suspend fun deleteLibManga(track: Track): Track {
- return withIOContext {
+ suspend fun deleteLibManga(track: DomainTrack) {
+ withIOContext {
val query = """
|mutation DeleteManga(${'$'}listId: Int) {
|DeleteMediaListEntry(id: ${'$'}listId) {
@@ -126,12 +127,11 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
val payload = buildJsonObject {
put("query", query)
putJsonObject("variables") {
- put("listId", track.library_id)
+ put("listId", track.libraryId)
}
}
authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime)))
.awaitSuccess()
- track
}
}
suspend fun search(search: String): List {
@@ -235,7 +235,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("query", query)
putJsonObject("variables") {
put("id", userid)
- put("manga_id", track.media_id)
+ put("manga_id", track.remote_id)
}
}
with(json) {
@@ -258,8 +258,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
}
}
- suspend fun getLibManga(track: Track, userid: Int): Track {
- return findLibManga(track, userid) ?: throw Exception("Could not find manga")
+ suspend fun getLibManga(track: Track, userId: Int): Track {
+ return findLibManga(track, userId) ?: throw Exception("Could not find manga")
}
fun createOAuth(token: String): OAuth {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt
index eb2f15ab2..ed5550464 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt
@@ -9,9 +9,10 @@ import kotlinx.serialization.Serializable
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
+import tachiyomi.domain.track.model.Track as DomainTrack
data class ALManga(
- val media_id: Long,
+ val remote_id: Long,
val title_user_pref: String,
val image_url_lge: String,
val description: String?,
@@ -23,13 +24,13 @@ data class ALManga(
) {
fun toTrack() = TrackSearch.create(TrackerManager.ANILIST).apply {
- media_id = this@ALManga.media_id
+ remote_id = this@ALManga.remote_id
title = title_user_pref
total_chapters = this@ALManga.total_chapters
cover_url = image_url_lge
summary = description?.htmlDecode() ?: ""
score = average_score.toFloat()
- tracking_url = AnilistApi.mangaUrl(media_id)
+ tracking_url = AnilistApi.mangaUrl(remote_id)
publishing_status = this@ALManga.publishing_status
publishing_type = format
if (start_date_fuzzy != 0L) {
@@ -54,7 +55,7 @@ data class ALUserManga(
) {
fun toTrack() = Track.create(TrackerManager.ANILIST).apply {
- media_id = manga.media_id
+ remote_id = manga.remote_id
title = manga.title_user_pref
status = toTrackStatus()
score = score_raw.toFloat()
@@ -98,28 +99,28 @@ fun Track.toAnilistStatus() = when (status) {
private val preferences: TrackPreferences by injectLazy()
-fun Track.toAnilistScore(): String = when (preferences.anilistScoreType().get()) {
-// 10 point
+fun DomainTrack.toAnilistScore(): String = when (preferences.anilistScoreType().get()) {
+ // 10 point
"POINT_10" -> (score.toInt() / 10).toString()
-// 100 point
+ // 100 point
"POINT_100" -> score.toInt().toString()
-// 5 stars
+ // 5 stars
"POINT_5" -> when {
- score == 0f -> "0"
+ score == 0.0 -> "0"
score < 30 -> "1"
score < 50 -> "2"
score < 70 -> "3"
score < 90 -> "4"
else -> "5"
}
-// Smiley
+ // Smiley
"POINT_3" -> when {
- score == 0f -> "0"
+ score == 0.0 -> "0"
score <= 35 -> ":("
score <= 60 -> ":|"
else -> ":)"
}
-// 10 point decimal
+ // 10 point decimal
"POINT_10_DECIMAL" -> (score / 10).toString()
else -> throw NotImplementedError("Unknown score type")
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt
index bac2bba6d..a85d5e583 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/Bangumi.kt
@@ -12,6 +12,7 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import tachiyomi.i18n.MR
import uy.kohesive.injekt.injectLazy
+import tachiyomi.domain.track.model.Track as DomainTrack
class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
@@ -23,7 +24,7 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
override fun getScoreList(): ImmutableList = SCORE_LIST
- override fun displayScore(track: Track): String {
+ override fun displayScore(track: DomainTrack): String {
return track.score.toInt().toString()
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt
index 29db49ec5..8dbde5324 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiApi.kt
@@ -42,7 +42,7 @@ class BangumiApi(
.add("rating", track.score.toInt().toString())
.add("status", track.toBangumiStatus())
.build()
- authClient.newCall(POST("$apiUrl/collection/${track.media_id}/update", body = body))
+ authClient.newCall(POST("$apiUrl/collection/${track.remote_id}/update", body = body))
.awaitSuccess()
track
}
@@ -55,7 +55,7 @@ class BangumiApi(
.add("rating", track.score.toInt().toString())
.add("status", track.toBangumiStatus())
.build()
- authClient.newCall(POST("$apiUrl/collection/${track.media_id}/update", body = sbody))
+ authClient.newCall(POST("$apiUrl/collection/${track.remote_id}/update", body = sbody))
.awaitSuccess()
// chapter update
@@ -64,7 +64,7 @@ class BangumiApi(
.build()
authClient.newCall(
POST(
- "$apiUrl/subject/${track.media_id}/update/watched_eps",
+ "$apiUrl/subject/${track.remote_id}/update/watched_eps",
body = body,
),
).awaitSuccess()
@@ -111,7 +111,7 @@ class BangumiApi(
}
val rating = obj["rating"]?.jsonObject?.get("score")?.jsonPrimitive?.floatOrNull ?: -1f
return TrackSearch.create(trackId).apply {
- media_id = obj["id"]!!.jsonPrimitive.long
+ remote_id = obj["id"]!!.jsonPrimitive.long
title = obj["name_cn"]!!.jsonPrimitive.content
cover_url = coverUrl
summary = obj["name"]!!.jsonPrimitive.content
@@ -124,7 +124,7 @@ class BangumiApi(
suspend fun findLibManga(track: Track): Track? {
return withIOContext {
with(json) {
- authClient.newCall(GET("$apiUrl/subject/${track.media_id}"))
+ authClient.newCall(GET("$apiUrl/subject/${track.remote_id}"))
.awaitSuccess()
.parseAs()
.let { jsonToSearch(it) }
@@ -134,7 +134,7 @@ class BangumiApi(
suspend fun statusLibManga(track: Track): Track? {
return withIOContext {
- val urlUserRead = "$apiUrl/collection/${track.media_id}"
+ val urlUserRead = "$apiUrl/collection/${track.remote_id}"
val requestUserRead = Request.Builder()
.url(urlUserRead)
.cacheControl(CacheControl.FORCE_NETWORK)
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiInterceptor.kt
index 7507ba3ef..d2183fdbf 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiInterceptor.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiInterceptor.kt
@@ -6,7 +6,7 @@ import okhttp3.Interceptor
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
-class BangumiInterceptor(val bangumi: Bangumi) : Interceptor {
+class BangumiInterceptor(private val bangumi: Bangumi) : Interceptor {
private val json: Json by injectLazy()
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiModels.kt
index 92fa49f24..445fb5194 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiModels.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/bangumi/BangumiModels.kt
@@ -62,12 +62,3 @@ fun Track.toBangumiStatus() = when (status) {
Bangumi.PLAN_TO_READ -> "wish"
else -> throw NotImplementedError("Unknown status: $status")
}
-
-fun toTrackStatus(status: String) = when (status) {
- "do" -> Bangumi.READING
- "collect" -> Bangumi.COMPLETED
- "on_hold" -> Bangumi.ON_HOLD
- "dropped" -> Bangumi.DROPPED
- "wish" -> Bangumi.PLAN_TO_READ
- else -> throw NotImplementedError("Unknown status: $status")
-}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/Kavita.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/Kavita.kt
index 0fff84efa..bcfbb1ec5 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/Kavita.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/Kavita.kt
@@ -55,7 +55,7 @@ class Kavita(id: Long) : BaseTracker(id, "Kavita"), EnhancedTracker {
override fun getScoreList(): ImmutableList = persistentListOf()
- override fun displayScore(track: Track): String = ""
+ override fun displayScore(track: DomainTrack): String = ""
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
if (track.status != COMPLETED) {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt
index 86fc808ad..03bfcd137 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt
@@ -14,6 +14,7 @@ import kotlinx.serialization.json.Json
import tachiyomi.i18n.MR
import uy.kohesive.injekt.injectLazy
import java.text.DecimalFormat
+import tachiyomi.domain.track.model.Track as DomainTrack
class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
@@ -65,7 +66,7 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
return if (index > 0) (index + 1) / 2f else 0f
}
- override fun displayScore(track: Track): String {
+ override fun displayScore(track: DomainTrack): String {
val df = DecimalFormat("0.#")
return df.format(track.score)
}
@@ -92,15 +93,15 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
return api.updateLibManga(track)
}
- override suspend fun delete(track: Track): Track {
- return api.removeLibManga(track)
+ override suspend fun delete(track: DomainTrack) {
+ api.removeLibManga(track)
}
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
val remoteTrack = api.findLibManga(track, getUserId())
return if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack)
- track.media_id = remoteTrack.media_id
+ track.remote_id = remoteTrack.remote_id
if (track.status != COMPLETED) {
track.status = if (hasReadChapters) READING else track.status
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt
index 5406be0ad..e6c3acadf 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt
@@ -29,6 +29,7 @@ import tachiyomi.core.util.lang.withIOContext
import uy.kohesive.injekt.injectLazy
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
+import tachiyomi.domain.track.model.Track as DomainTrack
class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) {
@@ -54,7 +55,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
}
putJsonObject("media") {
putJsonObject("data") {
- put("id", track.media_id)
+ put("id", track.remote_id)
put("type", "manga")
}
}
@@ -77,7 +78,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
.awaitSuccess()
.parseAs()
.let {
- track.media_id = it["data"]!!.jsonObject["id"]!!.jsonPrimitive.long
+ track.remote_id = it["data"]!!.jsonObject["id"]!!.jsonPrimitive.long
track
}
}
@@ -89,7 +90,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
val data = buildJsonObject {
putJsonObject("data") {
put("type", "libraryEntries")
- put("id", track.media_id)
+ put("id", track.remote_id)
putJsonObject("attributes") {
put("status", track.toKitsuStatus())
put("progress", track.last_chapter_read.toInt())
@@ -103,7 +104,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
with(json) {
authClient.newCall(
Request.Builder()
- .url("${baseUrl}library-entries/${track.media_id}")
+ .url("${baseUrl}library-entries/${track.remote_id}")
.headers(
headersOf(
"Content-Type",
@@ -124,19 +125,19 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
}
}
- suspend fun removeLibManga(track: Track): Track {
- return withIOContext {
- authClient.newCall(
- DELETE(
- "${baseUrl}library-entries/${track.media_id}",
- headers = headersOf(
- "Content-Type",
- "application/vnd.api+json",
+ suspend fun removeLibManga(track: DomainTrack) {
+ withIOContext {
+ authClient
+ .newCall(
+ DELETE(
+ "${baseUrl}library-entries/${track.remoteId}",
+ headers = headersOf(
+ "Content-Type",
+ "application/vnd.api+json",
+ ),
),
- ),
- )
+ )
.awaitSuccess()
- track
}
}
suspend fun search(query: String): List {
@@ -187,7 +188,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
suspend fun findLibManga(track: Track, userId: String): Track? {
return withIOContext {
val url = "${baseUrl}library-entries".toUri().buildUpon()
- .encodedQuery("filter[manga_id]=${track.media_id}&filter[user_id]=$userId")
+ .encodedQuery("filter[manga_id]=${track.remote_id}&filter[user_id]=$userId")
.appendQueryParameter("include", "manga")
.build()
with(json) {
@@ -210,7 +211,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
suspend fun getLibManga(track: Track): Track {
return withIOContext {
val url = "${baseUrl}library-entries".toUri().buildUpon()
- .encodedQuery("filter[id]=${track.media_id}")
+ .encodedQuery("filter[id]=${track.remote_id}")
.appendQueryParameter("include", "manga")
.build()
with(json) {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuInterceptor.kt
index 45abfa675..fe7e42292 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuInterceptor.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuInterceptor.kt
@@ -5,7 +5,7 @@ import okhttp3.Interceptor
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
-class KitsuInterceptor(val kitsu: Kitsu) : Interceptor {
+class KitsuInterceptor(private val kitsu: Kitsu) : Interceptor {
private val json: Json by injectLazy()
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt
index 156f6d8f3..ba6684339 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuModels.kt
@@ -37,12 +37,12 @@ class KitsuSearchManga(obj: JsonObject) {
@CallSuper
fun toTrack() = TrackSearch.create(TrackerManager.KITSU).apply {
- media_id = this@KitsuSearchManga.id
+ remote_id = this@KitsuSearchManga.id
title = canonicalTitle
total_chapters = chapterCount ?: 0
cover_url = original ?: ""
summary = synopsis ?: ""
- tracking_url = KitsuApi.mangaUrl(media_id)
+ tracking_url = KitsuApi.mangaUrl(remote_id)
score = rating ?: -1f
publishing_status = if (endDate == null) {
"Publishing"
@@ -70,12 +70,12 @@ class KitsuLibManga(obj: JsonObject, manga: JsonObject) {
val progress = obj["attributes"]!!.jsonObject["progress"]!!.jsonPrimitive.int
fun toTrack() = TrackSearch.create(TrackerManager.KITSU).apply {
- media_id = libraryId
+ remote_id = libraryId
title = canonicalTitle
total_chapters = chapterCount ?: 0
cover_url = original
summary = synopsis
- tracking_url = KitsuApi.mangaUrl(media_id)
+ tracking_url = KitsuApi.mangaUrl(remote_id)
publishing_status = this@KitsuLibManga.status
publishing_type = type
start_date = startDate
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/Komga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/Komga.kt
index 76eabfda9..6e69bf764 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/Komga.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/Komga.kt
@@ -52,7 +52,7 @@ class Komga(id: Long) : BaseTracker(id, "Komga"), EnhancedTracker {
override fun getScoreList(): ImmutableList = persistentListOf()
- override fun displayScore(track: Track): String = ""
+ override fun displayScore(track: DomainTrack): String = ""
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
if (track.status != COMPLETED) {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdates.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdates.kt
index 698ad123c..f5c33cf8d 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdates.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdates.kt
@@ -12,6 +12,7 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import tachiyomi.i18n.MR
+import tachiyomi.domain.track.model.Track as DomainTrack
class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker {
@@ -60,7 +61,7 @@ class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker
override fun indexToScore(index: Int): Float = SCORE_LIST[index].toFloat()
- override fun displayScore(track: Track): String = track.score.toString()
+ override fun displayScore(track: DomainTrack): String = track.score.toString()
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
if (track.status != COMPLETE_LIST && didReadChapter) {
@@ -70,9 +71,8 @@ class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker
return track
}
- override suspend fun delete(track: Track): Track {
+ override suspend fun delete(track: DomainTrack) {
api.deleteSeriesFromList(track)
- return track
}
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt
index ab6b8a309..794ad11ef 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt
@@ -30,6 +30,7 @@ import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody
import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.injectLazy
+import tachiyomi.domain.track.model.Track as DomainTrack
class MangaUpdatesApi(
interceptor: MangaUpdatesInterceptor,
@@ -48,7 +49,7 @@ class MangaUpdatesApi(
suspend fun getSeriesListItem(track: Track): Pair {
val listItem = with(json) {
- authClient.newCall(GET("$baseUrl/v1/lists/series/${track.media_id}"))
+ authClient.newCall(GET("$baseUrl/v1/lists/series/${track.remote_id}"))
.awaitSuccess()
.parseAs()
}
@@ -63,7 +64,7 @@ class MangaUpdatesApi(
val body = buildJsonArray {
addJsonObject {
putJsonObject("series") {
- put("id", track.media_id)
+ put("id", track.remote_id)
}
put("list_id", status)
}
@@ -87,7 +88,7 @@ class MangaUpdatesApi(
val body = buildJsonArray {
addJsonObject {
putJsonObject("series") {
- put("id", track.media_id)
+ put("id", track.remote_id)
}
put("list_id", track.status)
putJsonObject("status") {
@@ -106,9 +107,9 @@ class MangaUpdatesApi(
updateSeriesRating(track)
}
- suspend fun deleteSeriesFromList(track: Track) {
+ suspend fun deleteSeriesFromList(track: DomainTrack) {
val body = buildJsonArray {
- add(track.media_id)
+ add(track.remoteId)
}
authClient.newCall(
POST(
@@ -122,7 +123,7 @@ class MangaUpdatesApi(
private suspend fun getSeriesRating(track: Track): Rating? {
return try {
with(json) {
- authClient.newCall(GET("$baseUrl/v1/series/${track.media_id}/rating"))
+ authClient.newCall(GET("$baseUrl/v1/series/${track.remote_id}/rating"))
.awaitSuccess()
.parseAs()
}
@@ -138,7 +139,7 @@ class MangaUpdatesApi(
}
authClient.newCall(
PUT(
- url = "$baseUrl/v1/series/${track.media_id}/rating",
+ url = "$baseUrl/v1/series/${track.remote_id}/rating",
body = body.toString().toRequestBody(contentType),
),
)
@@ -146,7 +147,7 @@ class MangaUpdatesApi(
} else {
authClient.newCall(
DELETE(
- url = "$baseUrl/v1/series/${track.media_id}/rating",
+ url = "$baseUrl/v1/series/${track.remote_id}/rating",
),
)
.awaitSuccess()
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Record.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Record.kt
index fb959a89b..4b66273e8 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Record.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Record.kt
@@ -25,7 +25,7 @@ data class Record(
fun Record.toTrackSearch(id: Long): TrackSearch {
return TrackSearch.create(id).apply {
- media_id = this@toTrackSearch.seriesId ?: 0L
+ remote_id = this@toTrackSearch.seriesId ?: 0L
title = this@toTrackSearch.title?.htmlDecode() ?: ""
total_chapters = 0
cover_url = this@toTrackSearch.image?.url?.original ?: ""
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/model/TrackSearch.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/model/TrackSearch.kt
index 7151aab62..b8a71687f 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/track/model/TrackSearch.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/model/TrackSearch.kt
@@ -8,9 +8,9 @@ class TrackSearch : Track {
override var manga_id: Long = 0
- override var sync_id: Int = 0
+ override var tracker_id: Int = 0
- override var media_id: Long = 0
+ override var remote_id: Long = 0
override var library_id: Long? = null
@@ -47,22 +47,22 @@ class TrackSearch : Track {
other as TrackSearch
if (manga_id != other.manga_id) return false
- if (sync_id != other.sync_id) return false
- if (media_id != other.media_id) return false
+ if (tracker_id != other.tracker_id) return false
+ if (remote_id != other.remote_id) return false
return true
}
override fun hashCode(): Int {
var result = manga_id.hashCode()
- result = 31 * result + sync_id
- result = 31 * result + media_id.hashCode()
+ result = 31 * result + tracker_id
+ result = 31 * result + remote_id.hashCode()
return result
}
companion object {
fun create(serviceId: Long): TrackSearch = TrackSearch().apply {
- sync_id = serviceId.toInt()
+ tracker_id = serviceId.toInt()
}
}
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt
index 5ab94265c..5d66b73d4 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt
@@ -13,6 +13,7 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import tachiyomi.i18n.MR
import uy.kohesive.injekt.injectLazy
+import tachiyomi.domain.track.model.Track as DomainTrack
class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker {
@@ -65,7 +66,7 @@ class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker {
override fun getScoreList(): ImmutableList = SCORE_LIST
- override fun displayScore(track: Track): String {
+ override fun displayScore(track: DomainTrack): String {
return track.score.toInt().toString()
}
@@ -91,15 +92,15 @@ class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker {
return api.updateItem(track)
}
- override suspend fun delete(track: Track): Track {
- return api.deleteItem(track)
+ override suspend fun delete(track: DomainTrack) {
+ api.deleteItem(track)
}
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
val remoteTrack = api.findListItem(track)
return if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack)
- track.media_id = remoteTrack.media_id
+ track.remote_id = remoteTrack.remote_id
if (track.status != COMPLETED) {
val isRereading = track.status == REREADING
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt
index 444c5512f..c67cc2a2a 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt
@@ -4,6 +4,7 @@ import android.net.Uri
import androidx.core.net.toUri
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.model.TrackSearch
+import eu.kanade.tachiyomi.network.DELETE
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess
@@ -31,6 +32,7 @@ import tachiyomi.core.util.lang.withIOContext
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
+import tachiyomi.domain.track.model.Track as DomainTrack
class MyAnimeListApi(
private val trackId: Long,
@@ -114,7 +116,7 @@ class MyAnimeListApi(
.let {
val obj = it.jsonObject
TrackSearch.create(trackId).apply {
- media_id = obj["id"]!!.jsonPrimitive.long
+ remote_id = obj["id"]!!.jsonPrimitive.long
title = obj["title"]!!.jsonPrimitive.content
summary = obj["synopsis"]?.jsonPrimitive?.content ?: ""
total_chapters = obj["num_chapters"]!!.jsonPrimitive.int
@@ -122,7 +124,7 @@ class MyAnimeListApi(
cover_url =
obj["main_picture"]?.jsonObject?.get("large")?.jsonPrimitive?.content
?: ""
- tracking_url = "https://myanimelist.net/manga/$media_id"
+ tracking_url = "https://myanimelist.net/manga/$remote_id"
publishing_status =
obj["status"]!!.jsonPrimitive.content.replace("_", " ")
publishing_type =
@@ -154,7 +156,7 @@ class MyAnimeListApi(
}
val request = Request.Builder()
- .url(mangaUrl(track.media_id).toString())
+ .url(mangaUrl(track.remote_id).toString())
.put(formBodyBuilder.build())
.build()
with(json) {
@@ -166,24 +168,18 @@ class MyAnimeListApi(
}
}
- suspend fun deleteItem(track: Track): Track {
- return withIOContext {
- val request = Request.Builder()
- .url(mangaUrl(track.media_id).toString())
- .delete()
- .build()
- with(json) {
- authClient.newCall(request)
- .awaitSuccess()
- track
- }
+ suspend fun deleteItem(track: DomainTrack) {
+ withIOContext {
+ authClient
+ .newCall(DELETE(mangaUrl(track.remoteId).toString()))
+ .awaitSuccess()
}
}
suspend fun findListItem(track: Track): Track? {
return withIOContext {
val uri = "$baseApiUrl/manga".toUri().buildUpon()
- .appendPath(track.media_id.toString())
+ .appendPath(track.remote_id.toString())
.appendQueryParameter("fields", "num_chapters,my_list_status{start_date,finish_date}")
.build()
with(json) {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt
index d8e2bdd97..8f70e03dd 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/Shikimori.kt
@@ -13,6 +13,7 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import tachiyomi.i18n.MR
import uy.kohesive.injekt.injectLazy
+import tachiyomi.domain.track.model.Track as DomainTrack
class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker {
@@ -37,7 +38,7 @@ class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker {
override fun getScoreList(): ImmutableList = SCORE_LIST
- override fun displayScore(track: Track): String {
+ override fun displayScore(track: DomainTrack): String {
return track.score.toInt().toString()
}
@@ -59,8 +60,8 @@ class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker {
return api.updateLibManga(track, getUsername())
}
- override suspend fun delete(track: Track): Track {
- return api.deleteLibManga(track)
+ override suspend fun delete(track: DomainTrack) {
+ api.deleteLibManga(track)
}
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt
index 0a5bba772..dca1c290d 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt
@@ -27,6 +27,7 @@ import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody
import tachiyomi.core.util.lang.withIOContext
import uy.kohesive.injekt.injectLazy
+import tachiyomi.domain.track.model.Track as DomainTrack
class ShikimoriApi(
private val trackId: Long,
@@ -44,7 +45,7 @@ class ShikimoriApi(
val payload = buildJsonObject {
putJsonObject("user_rate") {
put("user_id", userId)
- put("target_id", track.media_id)
+ put("target_id", track.remote_id)
put("target_type", "Manga")
put("chapters", track.last_chapter_read.toInt())
put("score", track.score.toInt())
@@ -69,14 +70,11 @@ class ShikimoriApi(
suspend fun updateLibManga(track: Track, userId: String): Track = addLibManga(track, userId)
- suspend fun deleteLibManga(track: Track): Track {
- return withIOContext {
- authClient.newCall(
- DELETE(
- "$apiUrl/v2/user_rates/${track.library_id}",
- ),
- ).awaitSuccess()
- track
+ suspend fun deleteLibManga(track: DomainTrack) {
+ withIOContext {
+ authClient
+ .newCall(DELETE("$apiUrl/v2/user_rates/${track.libraryId}"))
+ .awaitSuccess()
}
}
@@ -102,7 +100,7 @@ class ShikimoriApi(
private fun jsonToSearch(obj: JsonObject): TrackSearch {
return TrackSearch.create(trackId).apply {
- media_id = obj["id"]!!.jsonPrimitive.long
+ remote_id = obj["id"]!!.jsonPrimitive.long
title = obj["name"]!!.jsonPrimitive.content
total_chapters = obj["chapters"]!!.jsonPrimitive.int
cover_url = baseUrl + obj["image"]!!.jsonObject["preview"]!!.jsonPrimitive.content
@@ -118,7 +116,7 @@ class ShikimoriApi(
private fun jsonToTrack(obj: JsonObject, mangas: JsonObject): Track {
return Track.create(trackId).apply {
title = mangas["name"]!!.jsonPrimitive.content
- media_id = obj["id"]!!.jsonPrimitive.long
+ remote_id = obj["id"]!!.jsonPrimitive.long
total_chapters = mangas["chapters"]!!.jsonPrimitive.int
library_id = obj["id"]!!.jsonPrimitive.long
last_chapter_read = obj["chapters"]!!.jsonPrimitive.float
@@ -131,7 +129,7 @@ class ShikimoriApi(
suspend fun findLibManga(track: Track, userId: String): Track? {
return withIOContext {
val urlMangas = "$apiUrl/mangas".toUri().buildUpon()
- .appendPath(track.media_id.toString())
+ .appendPath(track.remote_id.toString())
.build()
val mangas = with(json) {
authClient.newCall(GET(urlMangas.toString()))
@@ -141,7 +139,7 @@ class ShikimoriApi(
val url = "$apiUrl/v2/user_rates".toUri().buildUpon()
.appendQueryParameter("user_id", userId)
- .appendQueryParameter("target_id", track.media_id.toString())
+ .appendQueryParameter("target_id", track.remote_id.toString())
.appendQueryParameter("target_type", "Manga")
.build()
with(json) {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriInterceptor.kt
index 5ecea52e6..84c64462f 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriInterceptor.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriInterceptor.kt
@@ -5,7 +5,7 @@ import okhttp3.Interceptor
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
-class ShikimoriInterceptor(val shikimori: Shikimori) : Interceptor {
+class ShikimoriInterceptor(private val shikimori: Shikimori) : Interceptor {
private val json: Json by injectLazy()
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/Suwayomi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/Suwayomi.kt
index dedafe486..d8d1ba975 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/Suwayomi.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/Suwayomi.kt
@@ -45,7 +45,7 @@ class Suwayomi(id: Long) : BaseTracker(id, "Suwayomi"), EnhancedTracker {
override fun getScoreList(): ImmutableList = persistentListOf()
- override fun displayScore(track: Track): String = ""
+ override fun displayScore(track: DomainTrack): String = ""
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
if (track.status != COMPLETED) {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt b/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt
index c95e73017..77676004d 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt
@@ -23,6 +23,7 @@ import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.AndroidSourceManager
import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
import kotlinx.serialization.json.Json
+import kotlinx.serialization.protobuf.ProtoBuf
import nl.adaptivity.xmlutil.XmlDeclMode
import nl.adaptivity.xmlutil.core.XmlVersion
import nl.adaptivity.xmlutil.serialization.XML
@@ -107,8 +108,11 @@ class AppModule(val app: Application) : InjektModule {
xmlVersion = XmlVersion.XML10
}
}
+ addSingletonFactory {
+ ProtoBuf
+ }
- addSingletonFactory { ChapterCache(app) }
+ addSingletonFactory { ChapterCache(app, get()) }
addSingletonFactory { CoverCache(app) }
addSingletonFactory { NetworkHelper(app, get()) }
diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt
index c43af2fdc..c40d78157 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt
@@ -141,6 +141,11 @@ internal object ExtensionLoader {
?.asSequence()
?.filter { it.isFile && it.extension == PRIVATE_EXTENSION_EXTENSION }
?.mapNotNull {
+ // Just in case, since Android 14+ requires them to be read-only
+ if (it.canWrite()) {
+ it.setReadOnly()
+ }
+
val path = it.absolutePath
pkgManager.getPackageArchiveInfo(path, PACKAGE_FLAGS)
?.apply { applicationInfo.fixBasePaths(path) }
@@ -277,7 +282,12 @@ internal object ExtensionLoader {
val hasReadme = appInfo.metaData.getInt(METADATA_HAS_README, 0) == 1
val hasChangelog = appInfo.metaData.getInt(METADATA_HAS_CHANGELOG, 0) == 1
- val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
+ val classLoader = try {
+ PathClassLoader(appInfo.sourceDir, null, context.classLoader)
+ } catch (e: Exception) {
+ logcat(LogPriority.ERROR, e) { "Extension load error: $extName ($pkgName)" }
+ return LoadResult.Error
+ }
val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!!
.split(";")
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt
index 72fa1cc6c..a19753a84 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt
@@ -217,7 +217,7 @@ class LibraryScreenModel(
if (isNotLoggedInAnyTrack || trackFiltersIsIgnored) return@tracking true
val mangaTracks = trackMap
- .mapValues { entry -> entry.value.map { it.syncId } }[item.libraryManga.id]
+ .mapValues { entry -> entry.value.map { it.trackerId } }[item.libraryManga.id]
.orEmpty()
val isExcluded = excludedTracks.isNotEmpty() && mangaTracks.fastAny { it in excludedTracks }
@@ -257,7 +257,7 @@ class LibraryScreenModel(
entry.value.isEmpty() -> null
else ->
entry.value
- .mapNotNull { trackerMap[it.syncId]?.get10PointScore(it) }
+ .mapNotNull { trackerMap[it.trackerId]?.get10PointScore(it) }
.average()
}
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt
index a6541fe30..1c36d19d9 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt
@@ -983,7 +983,7 @@ class MangaScreenModel(
.map { tracks ->
loggedInTrackers
// Map to TrackItem
- .map { service -> TrackItem(tracks.find { it.syncId == service.id }, service) }
+ .map { service -> TrackItem(tracks.find { it.trackerId == service.id }, service) }
// Show only if the service supports this manga's source
.filter { (it.tracker as? EnhancedTracker)?.accept(source!!) ?: true }
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt
index ce6f27686..9ad9f5bb2 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt
@@ -244,7 +244,7 @@ data class TrackInfoDialogHomeScreen(
val source = Injekt.get().getOrStub(sourceId)
return loggedInTrackers
// Map to TrackItem
- .map { service -> TrackItem(find { it.syncId == service.id }, service) }
+ .map { service -> TrackItem(find { it.trackerId == service.id }, service) }
// Show only if the service supports this manga's source
.filter { (it.tracker as? EnhancedTracker)?.accept(source) ?: true }
}
@@ -399,7 +399,7 @@ private data class TrackScoreSelectorScreen(
private class Model(
private val track: Track,
private val tracker: Tracker,
- ) : StateScreenModel(State(tracker.displayScore(track.toDbTrack()))) {
+ ) : StateScreenModel(State(tracker.displayScore(track))) {
fun getSelections(): ImmutableList {
return tracker.getScoreList()
@@ -816,7 +816,7 @@ private data class TrackerRemoveScreen(
fun deleteMangaFromService() {
screenModelScope.launchNonCancellable {
- (tracker as DeletableTracker).delete(track.toDbTrack())
+ (tracker as DeletableTracker).delete(track)
}
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/OnboardingScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/more/OnboardingScreen.kt
index bc211445e..a624c4730 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/OnboardingScreen.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/more/OnboardingScreen.kt
@@ -8,8 +8,11 @@ import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.domain.base.BasePreferences
import eu.kanade.presentation.more.onboarding.OnboardingScreen
+import eu.kanade.presentation.more.settings.screen.SearchableSettings
+import eu.kanade.presentation.more.settings.screen.SettingsDataScreen
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.ui.setting.SettingsScreen
+import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@@ -28,6 +31,8 @@ class OnboardingScreen : Screen() {
navigator.pop()
}
+ val restoreSettingKey = stringResource(SettingsDataScreen.restorePreferenceKeyString)
+
BackHandler(
enabled = !shownOnboardingFlow,
onBack = {
@@ -39,6 +44,7 @@ class OnboardingScreen : Screen() {
onComplete = finishOnboarding,
onRestoreBackup = {
finishOnboarding()
+ SearchableSettings.highlightKey = restoreSettingKey
navigator.push(SettingsScreen(SettingsScreen.Destination.DataAndStorage))
},
)
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderChapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderChapter.kt
index af32c11b3..c0cdfdd7c 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderChapter.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderChapter.kt
@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.ui.reader.model
+import eu.kanade.domain.chapter.model.toDbChapter
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader
import kotlinx.coroutines.flow.MutableStateFlow
@@ -23,6 +24,8 @@ data class ReaderChapter(val chapter: Chapter) {
private var references = 0
+ constructor(chapter: tachiyomi.domain.chapter.model.Chapter) : this(chapter.toDbChapter())
+
fun ref() {
references++
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsScreenModel.kt
index e925c7dcb..9ed04cd78 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsScreenModel.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsScreenModel.kt
@@ -118,7 +118,7 @@ class StatsScreenModel(
val loggedInTrackerIds = loggedInTrackers.map { it.id }.toHashSet()
return libraryManga.associate { manga ->
val tracks = getTracks.await(manga.id)
- .fastFilter { it.syncId in loggedInTrackerIds }
+ .fastFilter { it.trackerId in loggedInTrackerIds }
manga.id to tracks
}
@@ -144,7 +144,7 @@ class StatsScreenModel(
}
private fun get10PointScore(track: Track): Double {
- val service = trackerManager.get(track.syncId)!!
+ val service = trackerManager.get(track.trackerId)!!
return service.get10PointScore(track)
}
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/BackupUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/BackupUtil.kt
deleted file mode 100644
index 05401603f..000000000
--- a/app/src/main/java/eu/kanade/tachiyomi/util/BackupUtil.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-package eu.kanade.tachiyomi.util
-
-import android.content.Context
-import android.net.Uri
-import eu.kanade.tachiyomi.data.backup.create.BackupCreator
-import eu.kanade.tachiyomi.data.backup.models.Backup
-import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
-import okio.buffer
-import okio.gzip
-import okio.source
-
-object BackupUtil {
- /**
- * Decode a potentially-gzipped backup.
- */
- fun decodeBackup(context: Context, uri: Uri): Backup {
- val backupCreator = BackupCreator(context)
-
- val backupStringSource = context.contentResolver.openInputStream(uri)!!.source().buffer()
-
- val peeked = backupStringSource.peek()
- peeked.require(2)
- val id1id2 = peeked.readShort()
- val backupString = if (id1id2.toInt() == 0x1f8b) { // 0x1f8b is gzip magic bytes
- backupStringSource.gzip().buffer()
- } else {
- backupStringSource
- }.use { it.readByteArray() }
-
- return backupCreator.parser.decodeFromByteArray(BackupSerializer, backupString)
- }
-}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/AnimationExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/AnimationExtensions.kt
index 01c6b581b..3e0761854 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/util/system/AnimationExtensions.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/AnimationExtensions.kt
@@ -2,9 +2,6 @@ package eu.kanade.tachiyomi.util.system
import android.content.Context
import android.provider.Settings
-import android.view.ViewPropertyAnimator
-import android.view.animation.Animation
-import androidx.constraintlayout.motion.widget.MotionScene.Transition
/**
* Gets the duration multiplier for general animations on the device
@@ -12,19 +9,3 @@ import androidx.constraintlayout.motion.widget.MotionScene.Transition
*/
val Context.animatorDurationScale: Float
get() = Settings.Global.getFloat(this.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f)
-
-/** Scale the duration of this [Animation] by [Context.animatorDurationScale] */
-fun Animation.applySystemAnimatorScale(context: Context) {
- this.duration = (this.duration * context.animatorDurationScale).toLong()
-}
-
-/** Scale the duration of this [Transition] by [Context.animatorDurationScale] */
-fun Transition.applySystemAnimatorScale(context: Context) {
- // End layout of cover expanding animation tends to break when the transition is less than ~25ms
- this.duration = (this.duration * context.animatorDurationScale).toInt().coerceAtLeast(25)
-}
-
-/** Scale the duration of this [ViewPropertyAnimator] by [Context.animatorDurationScale] */
-fun ViewPropertyAnimator.applySystemAnimatorScale(context: Context): ViewPropertyAnimator = apply {
- this.duration = (this.duration * context.animatorDurationScale).toLong()
-}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/DisplayExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/DisplayExtensions.kt
index 3e2834fd2..f051ccbc8 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/util/system/DisplayExtensions.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/DisplayExtensions.kt
@@ -17,18 +17,10 @@ private const val TABLET_UI_MIN_SCREEN_WIDTH_PORTRAIT_DP = 700
// make sure icons on the nav rail fit
private const val TABLET_UI_MIN_SCREEN_WIDTH_LANDSCAPE_DP = 600
-fun Context.isTabletUi(): Boolean {
- return resources.configuration.isTabletUi()
-}
-
fun Configuration.isTabletUi(): Boolean {
return smallestScreenWidthDp >= TABLET_UI_REQUIRED_SCREEN_WIDTH_DP
}
-fun Configuration.isAutoTabletUiAvailable(): Boolean {
- return smallestScreenWidthDp >= TABLET_UI_MIN_SCREEN_WIDTH_LANDSCAPE_DP
-}
-
// TODO: move the logic to `isTabletUi()` when main activity is rewritten in Compose
fun Context.prepareTabletUiContext(): Context {
val configuration = resources.configuration
diff --git a/app/src/main/java/eu/kanade/test/DummyTracker.kt b/app/src/main/java/eu/kanade/test/DummyTracker.kt
index 10d9bd957..e8183310d 100644
--- a/app/src/main/java/eu/kanade/test/DummyTracker.kt
+++ b/app/src/main/java/eu/kanade/test/DummyTracker.kt
@@ -58,7 +58,7 @@ data class DummyTracker(
override fun indexToScore(index: Int): Float = getScoreList()[index].toFloat()
- override fun displayScore(track: eu.kanade.tachiyomi.data.database.models.Track): String =
+ override fun displayScore(track: Track): String =
track.score.toString()
override suspend fun update(
diff --git a/app/src/main/res/drawable/ic_done_24dp.xml b/app/src/main/res/drawable/ic_done_24dp.xml
new file mode 100644
index 000000000..f6887725c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_done_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout/source_preferences_controller.xml b/app/src/main/res/layout/source_preferences_controller.xml
deleted file mode 100644
index 7bf61a875..000000000
--- a/app/src/main/res/layout/source_preferences_controller.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
diff --git a/core/src/main/java/eu/kanade/tachiyomi/network/interceptor/SpecificHostRateLimitInterceptor.kt b/core/src/main/java/eu/kanade/tachiyomi/network/interceptor/SpecificHostRateLimitInterceptor.kt
index c86f780d9..9f860faab 100644
--- a/core/src/main/java/eu/kanade/tachiyomi/network/interceptor/SpecificHostRateLimitInterceptor.kt
+++ b/core/src/main/java/eu/kanade/tachiyomi/network/interceptor/SpecificHostRateLimitInterceptor.kt
@@ -51,6 +51,7 @@ fun OkHttpClient.Builder.rateLimitHost(
* @param permits [Int] Number of requests allowed within a period of units.
* @param period [Duration] The limiting duration. Defaults to 1.seconds.
*/
+@Suppress("UNUSED")
fun OkHttpClient.Builder.rateLimitHost(
httpUrl: HttpUrl,
permits: Int,
@@ -71,5 +72,6 @@ fun OkHttpClient.Builder.rateLimitHost(
* @param permits [Int] Number of requests allowed within a period of units.
* @param period [Duration] The limiting duration. Defaults to 1.seconds.
*/
+@Suppress("UNUSED")
fun OkHttpClient.Builder.rateLimitHost(url: String, permits: Int, period: Duration = 1.seconds) =
addInterceptor(RateLimitInterceptor(url.toHttpUrlOrNull()?.host, permits, period))
diff --git a/core/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt b/core/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt
index fc0e81e65..6d5d2ffb6 100644
--- a/core/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt
+++ b/core/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt
@@ -3,13 +3,32 @@ package eu.kanade.tachiyomi.util.storage
import android.content.Context
import android.media.MediaScannerConnection
import android.net.Uri
+import android.os.Environment
import android.os.StatFs
+import androidx.core.content.ContextCompat
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.util.lang.Hash
import java.io.File
object DiskUtil {
+ /**
+ * Returns the root folders of all the available external storages.
+ */
+ fun getExternalStorages(context: Context): List {
+ return ContextCompat.getExternalFilesDirs(context, null)
+ .filterNotNull()
+ .mapNotNull {
+ val file = File(it.absolutePath.substringBefore("/Android/"))
+ val state = Environment.getExternalStorageState(file)
+ if (state == Environment.MEDIA_MOUNTED || state == Environment.MEDIA_MOUNTED_READ_ONLY) {
+ file
+ } else {
+ null
+ }
+ }
+ }
+
fun hashKeyForDisk(key: String): String {
return Hash.md5(key)
}
diff --git a/core/src/main/java/tachiyomi/core/i18n/Localize.kt b/core/src/main/java/tachiyomi/core/i18n/Localize.kt
index 3c0c7e216..d6f8f3c16 100644
--- a/core/src/main/java/tachiyomi/core/i18n/Localize.kt
+++ b/core/src/main/java/tachiyomi/core/i18n/Localize.kt
@@ -10,17 +10,21 @@ import dev.icerock.moko.resources.desc.ResourceFormatted
import dev.icerock.moko.resources.desc.StringDesc
fun Context.stringResource(resource: StringResource): String {
- return StringDesc.Resource(resource).toString(this)
+ return StringDesc.Resource(resource).toString(this).fixed()
}
fun Context.stringResource(resource: StringResource, vararg args: Any): String {
- return StringDesc.ResourceFormatted(resource, *args).toString(this)
+ return StringDesc.ResourceFormatted(resource, *args).toString(this).fixed()
}
fun Context.pluralStringResource(resource: PluralsResource, count: Int): String {
- return StringDesc.Plural(resource, count).toString(this)
+ return StringDesc.Plural(resource, count).toString(this).fixed()
}
fun Context.pluralStringResource(resource: PluralsResource, count: Int, vararg args: Any): String {
- return StringDesc.PluralFormatted(resource, count, *args).toString(this)
+ return StringDesc.PluralFormatted(resource, count, *args).toString(this).fixed()
}
+
+// TODO: janky workaround for https://github.com/icerockdev/moko-resources/issues/337
+private fun String.fixed() =
+ this.replace("""\""", """"""")
diff --git a/core/src/main/java/tachiyomi/core/preference/InMemoryPreferenceStore.kt b/core/src/main/java/tachiyomi/core/preference/InMemoryPreferenceStore.kt
index 83106999f..2fb3ee9ec 100644
--- a/core/src/main/java/tachiyomi/core/preference/InMemoryPreferenceStore.kt
+++ b/core/src/main/java/tachiyomi/core/preference/InMemoryPreferenceStore.kt
@@ -51,6 +51,7 @@ class InMemoryPreferenceStore(
TODO("Not yet implemented")
}
+ @Suppress("UNCHECKED_CAST")
override fun getObject(
key: String,
defaultValue: T,
@@ -59,7 +60,7 @@ class InMemoryPreferenceStore(
): Preference {
val default = InMemoryPreference(key, null, defaultValue)
val data: T? = preferences[key]?.get() as? T
- return if (data == null) default else InMemoryPreference(key, data, defaultValue)
+ return if (data == null) default else InMemoryPreference(key, data, defaultValue)
}
override fun getAll(): Map {
diff --git a/core/src/main/java/tachiyomi/core/storage/UniFileExtensions.kt b/core/src/main/java/tachiyomi/core/storage/UniFileExtensions.kt
index c5c2bbbc8..65846ff6e 100644
--- a/core/src/main/java/tachiyomi/core/storage/UniFileExtensions.kt
+++ b/core/src/main/java/tachiyomi/core/storage/UniFileExtensions.kt
@@ -16,7 +16,7 @@ val UniFile.nameWithoutExtension: String?
fun UniFile.toTempFile(context: Context): File {
val inputStream = context.contentResolver.openInputStream(uri)!!
val tempFile = File.createTempFile(
- nameWithoutExtension.orEmpty(),
+ nameWithoutExtension.orEmpty().padEnd(3), // Prefix must be 3+ chars
null,
)
diff --git a/core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt b/core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt
index 0aa7f9f59..68da52505 100644
--- a/core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt
+++ b/core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt
@@ -31,6 +31,7 @@ import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.net.URLConnection
+import java.util.Locale
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
@@ -274,6 +275,7 @@ object ImageUtil {
}
private fun splitImageName(filenamePrefix: String, index: Int) = "${filenamePrefix}__${"%03d".format(
+ Locale.ENGLISH,
index + 1,
)}.jpg"
diff --git a/data/src/main/java/tachiyomi/data/track/TrackMapper.kt b/data/src/main/java/tachiyomi/data/track/TrackMapper.kt
index 8ac852731..ee941d49c 100644
--- a/data/src/main/java/tachiyomi/data/track/TrackMapper.kt
+++ b/data/src/main/java/tachiyomi/data/track/TrackMapper.kt
@@ -20,7 +20,7 @@ object TrackMapper {
): Track = Track(
id = id,
mangaId = mangaId,
- syncId = syncId,
+ trackerId = syncId,
remoteId = remoteId,
libraryId = libraryId,
title = title,
diff --git a/data/src/main/java/tachiyomi/data/track/TrackRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/track/TrackRepositoryImpl.kt
index 19a3daa02..6cd8396b1 100644
--- a/data/src/main/java/tachiyomi/data/track/TrackRepositoryImpl.kt
+++ b/data/src/main/java/tachiyomi/data/track/TrackRepositoryImpl.kt
@@ -31,11 +31,11 @@ class TrackRepositoryImpl(
}
}
- override suspend fun delete(mangaId: Long, syncId: Long) {
+ override suspend fun delete(mangaId: Long, trackerId: Long) {
handler.await {
manga_syncQueries.delete(
mangaId = mangaId,
- syncId = syncId,
+ syncId = trackerId,
)
}
}
@@ -53,7 +53,7 @@ class TrackRepositoryImpl(
tracks.forEach { mangaTrack ->
manga_syncQueries.insert(
mangaId = mangaTrack.mangaId,
- syncId = mangaTrack.syncId,
+ syncId = mangaTrack.trackerId,
remoteId = mangaTrack.remoteId,
libraryId = mangaTrack.libraryId,
title = mangaTrack.title,
diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts
index 4df15c79a..425551ca0 100644
--- a/domain/build.gradle.kts
+++ b/domain/build.gradle.kts
@@ -1,6 +1,7 @@
plugins {
id("com.android.library")
kotlin("android")
+ kotlin("plugin.serialization")
}
android {
@@ -18,6 +19,7 @@ dependencies {
implementation(platform(kotlinx.coroutines.bom))
implementation(kotlinx.bundles.coroutines)
+ implementation(kotlinx.bundles.serialization)
implementation(libs.unifile)
diff --git a/domain/src/main/java/tachiyomi/domain/manga/interactor/FetchInterval.kt b/domain/src/main/java/tachiyomi/domain/manga/interactor/FetchInterval.kt
index 0a9124d16..7570b2a11 100644
--- a/domain/src/main/java/tachiyomi/domain/manga/interactor/FetchInterval.kt
+++ b/domain/src/main/java/tachiyomi/domain/manga/interactor/FetchInterval.kt
@@ -46,6 +46,8 @@ class FetchInterval(
}
internal fun calculateInterval(chapters: List, zone: ZoneId): Int {
+ val chapterWindow = if (chapters.size <= 8) 3 else 10
+
val uploadDates = chapters.asSequence()
.filter { it.dateUpload > 0L }
.sortedByDescending { it.dateUpload }
@@ -55,7 +57,7 @@ class FetchInterval(
.atStartOfDay()
}
.distinct()
- .take(10)
+ .take(chapterWindow)
.toList()
val fetchDates = chapters.asSequence()
@@ -66,7 +68,7 @@ class FetchInterval(
.atStartOfDay()
}
.distinct()
- .take(10)
+ .take(chapterWindow)
.toList()
val interval = when {
diff --git a/domain/src/main/java/tachiyomi/domain/track/interactor/DeleteTrack.kt b/domain/src/main/java/tachiyomi/domain/track/interactor/DeleteTrack.kt
index 9672a6586..2a30e0ffc 100644
--- a/domain/src/main/java/tachiyomi/domain/track/interactor/DeleteTrack.kt
+++ b/domain/src/main/java/tachiyomi/domain/track/interactor/DeleteTrack.kt
@@ -8,9 +8,9 @@ class DeleteTrack(
private val trackRepository: TrackRepository,
) {
- suspend fun await(mangaId: Long, syncId: Long) {
+ suspend fun await(mangaId: Long, trackerId: Long) {
try {
- trackRepository.delete(mangaId, syncId)
+ trackRepository.delete(mangaId, trackerId)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
}
diff --git a/domain/src/main/java/tachiyomi/domain/track/model/Track.kt b/domain/src/main/java/tachiyomi/domain/track/model/Track.kt
index 91ac2c833..1a656fcad 100644
--- a/domain/src/main/java/tachiyomi/domain/track/model/Track.kt
+++ b/domain/src/main/java/tachiyomi/domain/track/model/Track.kt
@@ -3,7 +3,7 @@ package tachiyomi.domain.track.model
data class Track(
val id: Long,
val mangaId: Long,
- val syncId: Long,
+ val trackerId: Long,
val remoteId: Long,
val libraryId: Long?,
val title: String,
diff --git a/domain/src/main/java/tachiyomi/domain/track/repository/TrackRepository.kt b/domain/src/main/java/tachiyomi/domain/track/repository/TrackRepository.kt
index 1edd76fdf..1814a7a08 100644
--- a/domain/src/main/java/tachiyomi/domain/track/repository/TrackRepository.kt
+++ b/domain/src/main/java/tachiyomi/domain/track/repository/TrackRepository.kt
@@ -13,7 +13,7 @@ interface TrackRepository {
fun getTracksByMangaIdAsFlow(mangaId: Long): Flow>
- suspend fun delete(mangaId: Long, syncId: Long)
+ suspend fun delete(mangaId: Long, trackerId: Long)
suspend fun insert(track: Track)
diff --git a/domain/src/test/java/tachiyomi/domain/manga/interactor/FetchIntervalTest.kt b/domain/src/test/java/tachiyomi/domain/manga/interactor/FetchIntervalTest.kt
index 468d7eb2d..ccaaf24da 100644
--- a/domain/src/test/java/tachiyomi/domain/manga/interactor/FetchIntervalTest.kt
+++ b/domain/src/test/java/tachiyomi/domain/manga/interactor/FetchIntervalTest.kt
@@ -54,6 +54,21 @@ class FetchIntervalTest {
fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 1
}
+ @Test
+ fun `returns interval based on smaller subset of recent chapters if very few chapters`() {
+ val oldChapters = (1..3).map {
+ chapterWithTime(chapter, (it * 7).days)
+ }
+ // Significant gap between chapters
+ val newChapters = (1..3).map {
+ chapterWithTime(chapter, oldChapters.lastUploadDate() + 365.days + (it * 7).days)
+ }
+
+ val chapters = oldChapters + newChapters
+
+ fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 7
+ }
+
@Test
fun `returns interval of 7 days when multiple chapters in 1 day`() {
val chapters = (1..10).map {
diff --git a/gradle/compose.versions.toml b/gradle/compose.versions.toml
index 1fbf9f375..28d9ca5a7 100644
--- a/gradle/compose.versions.toml
+++ b/gradle/compose.versions.toml
@@ -1,5 +1,5 @@
[versions]
-compiler = "1.5.6"
+compiler = "1.5.7"
compose-bom = "2023.12.00-alpha04"
accompanist = "0.33.2-alpha"
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index c5a2e9b6f..cdbcddfd7 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -40,7 +40,7 @@ preferencektx = "androidx.preference:preference-ktx:1.2.1"
injekt-core = "com.github.inorichi.injekt:injekt-core:65b0440"
-coil-bom = { module = "io.coil-kt:coil-bom", version = "2.4.0" }
+coil-bom = { module = "io.coil-kt:coil-bom", version = "2.5.0" }
coil-core = { module = "io.coil-kt:coil" }
coil-gif = { module = "io.coil-kt:coil-gif" }
coil-compose = { module = "io.coil-kt:coil-compose" }
diff --git a/i18n/src/commonMain/resources/MR/base/strings.xml b/i18n/src/commonMain/resources/MR/base/strings.xml
index 7f0c7e36e..bab4b34a9 100644
--- a/i18n/src/commonMain/resources/MR/base/strings.xml
+++ b/i18n/src/commonMain/resources/MR/base/strings.xml
@@ -355,7 +355,7 @@
Double tap to zoom
Show content in cutout area
Animate page transitions
- Flash white on page change
+ Flash on page change
Reduces ghosting on e-ink displays
Double tap animation speed
Show page number
@@ -524,9 +524,10 @@
Last automatically backed up: %s
Data
Storage usage
+ Available: %1$s / Total: %2$s
Clear chapter cache
Used: %1$s
- Cache cleared. %1$d files have been deleted
+ Cache cleared, %1$d files deleted
Error occurred while clearing
Clear chapter cache on app launch
@@ -872,7 +873,6 @@
Chapter %1$s and %2$d more
Chapters %1$s
%1$d update(s) failed
- %1$d update(s) skipped
Tap to learn more
Failed to update cover
Please add the entry to your library before doing this
@@ -938,7 +938,6 @@
Progress
Complete
Errors
- Skipped
Chapter updates
App updates
Extension updates
diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/AdaptiveSheet.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/AdaptiveSheet.kt
index 51089cf52..d36e2593f 100644
--- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/AdaptiveSheet.kt
+++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/AdaptiveSheet.kt
@@ -11,7 +11,6 @@ import androidx.compose.foundation.gestures.anchoredDraggable
import androidx.compose.foundation.gestures.animateTo
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxSize
@@ -78,7 +77,7 @@ fun AdaptiveSheet(
onDismissRequest()
}
}
- BoxWithConstraints(
+ Box(
modifier = Modifier
.clickable(
enabled = true,
diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/Badges.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/Badges.kt
index ee2a05dac..448ee64e2 100644
--- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/Badges.kt
+++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/Badges.kt
@@ -21,6 +21,7 @@ import androidx.compose.ui.text.PlaceholderVerticalAlign
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
+import kotlinx.collections.immutable.persistentMapOf
@Composable
fun BadgeGroup(
@@ -66,7 +67,7 @@ fun Badge(
val text = buildAnnotatedString {
appendInlineContent(iconContentPlaceholder)
}
- val inlineContent = mapOf(
+ val inlineContent = persistentMapOf(
Pair(
iconContentPlaceholder,
InlineTextContent(
diff --git a/presentation-core/src/main/res/values/colors.xml b/presentation-core/src/main/res/values/colors.xml
index 3d66ea077..551dcc0a7 100644
--- a/presentation-core/src/main/res/values/colors.xml
+++ b/presentation-core/src/main/res/values/colors.xml
@@ -1,5 +1,5 @@
-
+
@color/accent_blue
#1F888888
@@ -20,21 +20,7 @@
#000000
- #000000
- #DE000000
- #8A000000
- #61000000
#1F000000
- #14000000
- #0F000000
-
- #FFFFFFFF
- #B3FFFFFF
- #8AFFFFFF
- #80FFFFFF
- #33FFFFFF
#1FFFFFFF
- #14FFFFFF
- #0FFFFFFF
diff --git a/presentation-widget/src/main/java/tachiyomi/presentation/widget/UpdatesGridGlanceWidget.kt b/presentation-widget/src/main/java/tachiyomi/presentation/widget/UpdatesGridGlanceWidget.kt
index 2de89ecce..20cb4279f 100644
--- a/presentation-widget/src/main/java/tachiyomi/presentation/widget/UpdatesGridGlanceWidget.kt
+++ b/presentation-widget/src/main/java/tachiyomi/presentation/widget/UpdatesGridGlanceWidget.kt
@@ -1,10 +1,12 @@
package tachiyomi.presentation.widget
+import android.annotation.SuppressLint
import androidx.compose.ui.unit.dp
import androidx.glance.ImageProvider
import androidx.glance.unit.ColorProvider
class UpdatesGridGlanceWidget : BaseUpdatesGridGlanceWidget() {
+ @SuppressLint("RestrictedApi")
override val foreground = ColorProvider(R.color.appwidget_on_secondary_container)
override val background = ImageProvider(R.drawable.appwidget_background)
override val topPadding = 0.dp
diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/SManga.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/SManga.kt
index f0a014e2a..490540aba 100644
--- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/SManga.kt
+++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/SManga.kt
@@ -29,36 +29,6 @@ interface SManga : Serializable {
return genre?.split(", ")?.map { it.trim() }?.filterNot { it.isBlank() }?.distinct()
}
- fun copyFrom(other: SManga) {
- if (other.author != null) {
- author = other.author
- }
-
- if (other.artist != null) {
- artist = other.artist
- }
-
- if (other.description != null) {
- description = other.description
- }
-
- if (other.genre != null) {
- genre = other.genre
- }
-
- if (other.thumbnail_url != null) {
- thumbnail_url = other.thumbnail_url
- }
-
- status = other.status
-
- update_strategy = other.update_strategy
-
- if (!initialized) {
- initialized = other.initialized
- }
- }
-
fun copy() = create().also {
it.url = url
it.title = title
diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/UpdateStrategy.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/UpdateStrategy.kt
index 91b5f5e29..d76a7dd00 100644
--- a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/UpdateStrategy.kt
+++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/UpdateStrategy.kt
@@ -6,6 +6,7 @@ package eu.kanade.tachiyomi.source.model
*
* @since extensions-lib 1.4
*/
+@Suppress("UNUSED")
enum class UpdateStrategy {
/**
* Series marked as always update will be included in the library