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 077c68ee6..8249713ae 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 @@ -34,12 +30,12 @@ import cafe.adriel.voyager.navigator.currentOrThrow 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.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 @@ -49,6 +45,8 @@ 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 @@ -67,6 +65,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 @@ -79,7 +79,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)), @@ -150,20 +150,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), @@ -184,142 +212,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() } @@ -327,9 +223,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), @@ -357,43 +263,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), @@ -404,236 +283,227 @@ object SettingsDataScreen : SearchableSettings { ), ) + getSyncServicePreferences(syncPreferences, syncService) } -} -@Composable -private fun getSyncServicePreferences(syncPreferences: SyncPreferences, syncService: Int): List { - val syncServiceType = SyncManager.SyncService.fromInt(syncService) - val basePreferences = getBasePreferences(syncServiceType, syncPreferences) + @Composable + private fun getSyncServicePreferences(syncPreferences: SyncPreferences, syncService: Int): List { + val syncServiceType = SyncManager.SyncService.fromInt(syncService) - return if (syncServiceType != SyncManager.SyncService.NONE) { - basePreferences + getAdditionalPreferences(syncPreferences) - } else { - basePreferences - } -} + val basePreferences = getBasePreferences(syncServiceType, syncPreferences) -@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 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 }, - ) + return if (syncServiceType != SyncManager.SyncService.NONE) { + basePreferences + getAdditionalPreferences(syncPreferences) + } else { + basePreferences + } } - return Preference.PreferenceItem.TextPreference( - title = stringResource(MR.strings.pref_google_drive_purge_sync_data), - onClick = { showPurgeDialog = true }, - ) -} - -@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 }, - ) + @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() + } } - return Preference.PreferenceGroup( - title = stringResource(MR.strings.pref_sync_now_group_title), - preferenceItems = listOf( + + @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_sync_now), - subtitle = stringResource(MR.strings.pref_sync_now_subtitle), + title = stringResource(MR.strings.pref_google_drive_sign_in), onClick = { - showDialog = true + val intent = googleDriveSync.getSignInIntent() + context.startActivity(intent) }, ), - ), - ) -} + getGoogleDrivePurge(), + ) + } -@Composable -private fun getAutomaticSyncGroup(syncPreferences: SyncPreferences): Preference.PreferenceGroup { - val context = LocalContext.current - val syncIntervalPref = syncPreferences.syncInterval() - val lastSync by syncPreferences.lastSyncTimestamp().collectAsState() + @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) } - 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), - ), - onValueChanged = { - SyncDataJob.setupTask(context, it) + 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 }, + ) + } + + return Preference.PreferenceItem.TextPreference( + title = stringResource(MR.strings.pref_google_drive_purge_sync_data), + onClick = { showPurgeDialog = true }, + ) + } + + @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.InfoPreference( - stringResource(MR.strings.last_synchronization, relativeTimeSpanString(lastSync)), + 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( + Preference.PreferenceItem.TextPreference( + title = stringResource(MR.strings.pref_sync_now), + subtitle = stringResource(MR.strings.pref_sync_now_subtitle), + onClick = { + showDialog = true + }, + ), + ), + ) + } + + @Composable + 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 = 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)), + ), + ), + ) + } + + @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..ddc4c586d 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() - } + private suspend fun backupCategories(options: Int): List { + if (options and BACKUP_CATEGORY != BACKUP_CATEGORY) return emptyList() - /** - * 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() - } + 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 + private 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/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 3bfd511e8..9fb2052fc 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 @@ -868,7 +869,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 @@ -934,7 +934,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