mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-27 11:37:51 +02:00
Cleanup migrate manga dialog and related code (#2156)
This commit is contained in:
@ -35,6 +35,7 @@ import mihon.domain.extensionrepo.interactor.ReplaceExtensionRepo
|
|||||||
import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo
|
import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo
|
||||||
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
|
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
|
||||||
import mihon.domain.extensionrepo.service.ExtensionRepoService
|
import mihon.domain.extensionrepo.service.ExtensionRepoService
|
||||||
|
import mihon.domain.migration.usecases.MigrateMangaUseCase
|
||||||
import mihon.domain.upcoming.interactor.GetUpcomingManga
|
import mihon.domain.upcoming.interactor.GetUpcomingManga
|
||||||
import tachiyomi.data.category.CategoryRepositoryImpl
|
import tachiyomi.data.category.CategoryRepositoryImpl
|
||||||
import tachiyomi.data.chapter.ChapterRepositoryImpl
|
import tachiyomi.data.chapter.ChapterRepositoryImpl
|
||||||
@ -134,6 +135,11 @@ class DomainModule : InjektModule {
|
|||||||
addFactory { SetMangaCategories(get()) }
|
addFactory { SetMangaCategories(get()) }
|
||||||
addFactory { GetExcludedScanlators(get()) }
|
addFactory { GetExcludedScanlators(get()) }
|
||||||
addFactory { SetExcludedScanlators(get()) }
|
addFactory { SetExcludedScanlators(get()) }
|
||||||
|
addFactory {
|
||||||
|
MigrateMangaUseCase(
|
||||||
|
get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), get()) }
|
addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), get()) }
|
||||||
addFactory { GetApplicationRelease(get(), get()) }
|
addFactory { GetApplicationRelease(get(), get()) }
|
||||||
|
@ -2,6 +2,7 @@ package eu.kanade.domain.source.service
|
|||||||
|
|
||||||
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
|
import mihon.domain.migration.models.MigrationFlag
|
||||||
import tachiyomi.core.common.preference.Preference
|
import tachiyomi.core.common.preference.Preference
|
||||||
import tachiyomi.core.common.preference.PreferenceStore
|
import tachiyomi.core.common.preference.PreferenceStore
|
||||||
import tachiyomi.core.common.preference.getEnum
|
import tachiyomi.core.common.preference.getEnum
|
||||||
@ -12,7 +13,7 @@ class SourcePreferences(
|
|||||||
private val preferenceStore: PreferenceStore,
|
private val preferenceStore: PreferenceStore,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun sourceDisplayMode() = preferenceStore.getObject(
|
fun sourceDisplayMode() = preferenceStore.getObjectFromString(
|
||||||
"pref_display_mode_catalogue",
|
"pref_display_mode_catalogue",
|
||||||
LibraryDisplayMode.default,
|
LibraryDisplayMode.default,
|
||||||
LibraryDisplayMode.Serializer::serialize,
|
LibraryDisplayMode.Serializer::serialize,
|
||||||
@ -58,4 +59,11 @@ class SourcePreferences(
|
|||||||
Preference.appStateKey("has_filters_toggle_state"),
|
Preference.appStateKey("has_filters_toggle_state"),
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun migrationFlags() = preferenceStore.getObjectFromInt(
|
||||||
|
key = "migrate_flags",
|
||||||
|
defaultValue = MigrationFlag.entries.toSet(),
|
||||||
|
serializer = { MigrationFlag.toBit(it) },
|
||||||
|
deserializer = { value: Int -> MigrationFlag.fromBit(value) },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,89 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.migration
|
|
||||||
|
|
||||||
import dev.icerock.moko.resources.StringResource
|
|
||||||
import eu.kanade.domain.manga.model.hasCustomCover
|
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadCache
|
|
||||||
import tachiyomi.domain.manga.model.Manga
|
|
||||||
import tachiyomi.i18n.MR
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
data class MigrationFlag(
|
|
||||||
val flag: Int,
|
|
||||||
val isDefaultSelected: Boolean,
|
|
||||||
val titleId: StringResource,
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
fun create(flag: Int, defaultSelectionMap: Int, titleId: StringResource): MigrationFlag {
|
|
||||||
return MigrationFlag(
|
|
||||||
flag = flag,
|
|
||||||
isDefaultSelected = defaultSelectionMap and flag != 0,
|
|
||||||
titleId = titleId,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object MigrationFlags {
|
|
||||||
|
|
||||||
private const val CHAPTERS = 0b00001
|
|
||||||
private const val CATEGORIES = 0b00010
|
|
||||||
private const val CUSTOM_COVER = 0b01000
|
|
||||||
private const val DELETE_DOWNLOADED = 0b10000
|
|
||||||
private const val NOTES = 0b100000
|
|
||||||
|
|
||||||
private val coverCache: CoverCache by injectLazy()
|
|
||||||
private val downloadCache: DownloadCache by injectLazy()
|
|
||||||
|
|
||||||
fun hasChapters(value: Int): Boolean {
|
|
||||||
return value and CHAPTERS != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hasCategories(value: Int): Boolean {
|
|
||||||
return value and CATEGORIES != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hasCustomCover(value: Int): Boolean {
|
|
||||||
return value and CUSTOM_COVER != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hasDeleteDownloaded(value: Int): Boolean {
|
|
||||||
return value and DELETE_DOWNLOADED != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hasNotes(value: Int): Boolean {
|
|
||||||
return value and NOTES != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns information about applicable flags with default selections. */
|
|
||||||
fun getFlags(manga: Manga?, defaultSelectedBitMap: Int): List<MigrationFlag> {
|
|
||||||
val flags = mutableListOf<MigrationFlag>()
|
|
||||||
flags += MigrationFlag.create(CHAPTERS, defaultSelectedBitMap, MR.strings.chapters)
|
|
||||||
flags += MigrationFlag.create(CATEGORIES, defaultSelectedBitMap, MR.strings.categories)
|
|
||||||
|
|
||||||
if (manga != null) {
|
|
||||||
if (manga.hasCustomCover(coverCache)) {
|
|
||||||
flags += MigrationFlag.create(CUSTOM_COVER, defaultSelectedBitMap, MR.strings.custom_cover)
|
|
||||||
}
|
|
||||||
if (downloadCache.getDownloadCount(manga) > 0) {
|
|
||||||
flags += MigrationFlag.create(DELETE_DOWNLOADED, defaultSelectedBitMap, MR.strings.delete_downloaded)
|
|
||||||
}
|
|
||||||
if (manga.notes.isNotBlank()) {
|
|
||||||
flags += MigrationFlag.create(NOTES, defaultSelectedBitMap, MR.strings.action_notes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return flags
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns a bit map of selected flags. */
|
|
||||||
fun getSelectedFlagsBitMap(
|
|
||||||
selectedFlags: List<Boolean>,
|
|
||||||
flags: List<MigrationFlag>,
|
|
||||||
): Int {
|
|
||||||
return selectedFlags
|
|
||||||
.zip(flags)
|
|
||||||
.filter { (isSelected, _) -> isSelected }
|
|
||||||
.map { (_, flag) -> flag.flag }
|
|
||||||
.reduceOrNull { acc, mask -> acc or mask } ?: 0
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,312 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.migration.search
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.Immutable
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.toMutableStateList
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
|
||||||
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
|
|
||||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
|
||||||
import eu.kanade.domain.manga.model.hasCustomCover
|
|
||||||
import eu.kanade.domain.manga.model.toSManga
|
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
|
||||||
import eu.kanade.tachiyomi.data.track.EnhancedTracker
|
|
||||||
import eu.kanade.tachiyomi.data.track.TrackerManager
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
|
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import tachiyomi.core.common.preference.Preference
|
|
||||||
import tachiyomi.core.common.preference.PreferenceStore
|
|
||||||
import tachiyomi.core.common.util.lang.launchIO
|
|
||||||
import tachiyomi.core.common.util.lang.withUIContext
|
|
||||||
import tachiyomi.domain.category.interactor.GetCategories
|
|
||||||
import tachiyomi.domain.category.interactor.SetMangaCategories
|
|
||||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
|
||||||
import tachiyomi.domain.chapter.interactor.UpdateChapter
|
|
||||||
import tachiyomi.domain.chapter.model.toChapterUpdate
|
|
||||||
import tachiyomi.domain.manga.model.Manga
|
|
||||||
import tachiyomi.domain.manga.model.MangaUpdate
|
|
||||||
import tachiyomi.domain.source.service.SourceManager
|
|
||||||
import tachiyomi.domain.track.interactor.GetTracks
|
|
||||||
import tachiyomi.domain.track.interactor.InsertTrack
|
|
||||||
import tachiyomi.i18n.MR
|
|
||||||
import tachiyomi.presentation.core.components.LabeledCheckbox
|
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
|
||||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.time.Instant
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
internal fun MigrateDialog(
|
|
||||||
oldManga: Manga,
|
|
||||||
newManga: Manga,
|
|
||||||
screenModel: MigrateDialogScreenModel,
|
|
||||||
onDismissRequest: () -> Unit,
|
|
||||||
onClickTitle: () -> Unit,
|
|
||||||
onPopScreen: () -> Unit,
|
|
||||||
) {
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val state by screenModel.state.collectAsState()
|
|
||||||
|
|
||||||
val flags = remember { MigrationFlags.getFlags(oldManga, screenModel.migrateFlags.get()) }
|
|
||||||
val selectedFlags = remember { flags.map { it.isDefaultSelected }.toMutableStateList() }
|
|
||||||
|
|
||||||
if (state.isMigrating) {
|
|
||||||
LoadingScreen(
|
|
||||||
modifier = Modifier
|
|
||||||
.background(MaterialTheme.colorScheme.background.copy(alpha = 0.7f)),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
title = {
|
|
||||||
Text(text = stringResource(MR.strings.migration_dialog_what_to_include))
|
|
||||||
},
|
|
||||||
text = {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
|
||||||
) {
|
|
||||||
flags.forEachIndexed { index, flag ->
|
|
||||||
LabeledCheckbox(
|
|
||||||
label = stringResource(flag.titleId),
|
|
||||||
checked = selectedFlags[index],
|
|
||||||
onCheckedChange = { selectedFlags[index] = it },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
FlowRow(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
|
||||||
) {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
onDismissRequest()
|
|
||||||
onClickTitle()
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Text(text = stringResource(MR.strings.action_show_manga))
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
scope.launchIO {
|
|
||||||
screenModel.migrateManga(
|
|
||||||
oldManga,
|
|
||||||
newManga,
|
|
||||||
false,
|
|
||||||
MigrationFlags.getSelectedFlagsBitMap(selectedFlags, flags),
|
|
||||||
)
|
|
||||||
withUIContext { onPopScreen() }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Text(text = stringResource(MR.strings.copy))
|
|
||||||
}
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
scope.launchIO {
|
|
||||||
screenModel.migrateManga(
|
|
||||||
oldManga,
|
|
||||||
newManga,
|
|
||||||
true,
|
|
||||||
MigrationFlags.getSelectedFlagsBitMap(selectedFlags, flags),
|
|
||||||
)
|
|
||||||
|
|
||||||
withUIContext { onPopScreen() }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Text(text = stringResource(MR.strings.migrate))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class MigrateDialogScreenModel(
|
|
||||||
private val sourceManager: SourceManager = Injekt.get(),
|
|
||||||
private val downloadManager: DownloadManager = Injekt.get(),
|
|
||||||
private val updateManga: UpdateManga = Injekt.get(),
|
|
||||||
private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(),
|
|
||||||
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(),
|
|
||||||
private val updateChapter: UpdateChapter = Injekt.get(),
|
|
||||||
private val getCategories: GetCategories = Injekt.get(),
|
|
||||||
private val setMangaCategories: SetMangaCategories = Injekt.get(),
|
|
||||||
private val getTracks: GetTracks = Injekt.get(),
|
|
||||||
private val insertTrack: InsertTrack = Injekt.get(),
|
|
||||||
private val coverCache: CoverCache = Injekt.get(),
|
|
||||||
private val preferenceStore: PreferenceStore = Injekt.get(),
|
|
||||||
) : StateScreenModel<MigrateDialogScreenModel.State>(State()) {
|
|
||||||
|
|
||||||
val migrateFlags: Preference<Int> by lazy {
|
|
||||||
preferenceStore.getInt("migrate_flags", Int.MAX_VALUE)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val enhancedServices by lazy {
|
|
||||||
Injekt.get<TrackerManager>().trackers.filterIsInstance<EnhancedTracker>()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun migrateManga(
|
|
||||||
oldManga: Manga,
|
|
||||||
newManga: Manga,
|
|
||||||
replace: Boolean,
|
|
||||||
flags: Int,
|
|
||||||
) {
|
|
||||||
migrateFlags.set(flags)
|
|
||||||
val source = sourceManager.get(newManga.source) ?: return
|
|
||||||
val prevSource = sourceManager.get(oldManga.source)
|
|
||||||
|
|
||||||
mutableState.update { it.copy(isMigrating = true) }
|
|
||||||
|
|
||||||
try {
|
|
||||||
val chapters = source.getChapterList(newManga.toSManga())
|
|
||||||
|
|
||||||
migrateMangaInternal(
|
|
||||||
oldSource = prevSource,
|
|
||||||
newSource = source,
|
|
||||||
oldManga = oldManga,
|
|
||||||
newManga = newManga,
|
|
||||||
sourceChapters = chapters,
|
|
||||||
replace = replace,
|
|
||||||
flags = flags,
|
|
||||||
)
|
|
||||||
} catch (_: Throwable) {
|
|
||||||
// Explicitly stop if an error occurred; the dialog normally gets popped at the end
|
|
||||||
// anyway
|
|
||||||
mutableState.update { it.copy(isMigrating = false) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun migrateMangaInternal(
|
|
||||||
oldSource: Source?,
|
|
||||||
newSource: Source,
|
|
||||||
oldManga: Manga,
|
|
||||||
newManga: Manga,
|
|
||||||
sourceChapters: List<SChapter>,
|
|
||||||
replace: Boolean,
|
|
||||||
flags: Int,
|
|
||||||
) {
|
|
||||||
val migrateChapters = MigrationFlags.hasChapters(flags)
|
|
||||||
val migrateCategories = MigrationFlags.hasCategories(flags)
|
|
||||||
val migrateCustomCover = MigrationFlags.hasCustomCover(flags)
|
|
||||||
val deleteDownloaded = MigrationFlags.hasDeleteDownloaded(flags)
|
|
||||||
val migrateNotes = MigrationFlags.hasNotes(flags)
|
|
||||||
|
|
||||||
try {
|
|
||||||
syncChaptersWithSource.await(sourceChapters, newManga, newSource)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
// Worst case, chapters won't be synced
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update chapters read, bookmark and dateFetch
|
|
||||||
if (migrateChapters) {
|
|
||||||
val prevMangaChapters = getChaptersByMangaId.await(oldManga.id)
|
|
||||||
val mangaChapters = getChaptersByMangaId.await(newManga.id)
|
|
||||||
|
|
||||||
val maxChapterRead = prevMangaChapters
|
|
||||||
.filter { it.read }
|
|
||||||
.maxOfOrNull { it.chapterNumber }
|
|
||||||
|
|
||||||
val updatedMangaChapters = mangaChapters.map { mangaChapter ->
|
|
||||||
var updatedChapter = mangaChapter
|
|
||||||
if (updatedChapter.isRecognizedNumber) {
|
|
||||||
val prevChapter = prevMangaChapters
|
|
||||||
.find { it.isRecognizedNumber && it.chapterNumber == updatedChapter.chapterNumber }
|
|
||||||
|
|
||||||
if (prevChapter != null) {
|
|
||||||
updatedChapter = updatedChapter.copy(
|
|
||||||
dateFetch = prevChapter.dateFetch,
|
|
||||||
bookmark = prevChapter.bookmark,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maxChapterRead != null && updatedChapter.chapterNumber <= maxChapterRead) {
|
|
||||||
updatedChapter = updatedChapter.copy(read = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedChapter
|
|
||||||
}
|
|
||||||
|
|
||||||
val chapterUpdates = updatedMangaChapters.map { it.toChapterUpdate() }
|
|
||||||
updateChapter.awaitAll(chapterUpdates)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update categories
|
|
||||||
if (migrateCategories) {
|
|
||||||
val categoryIds = getCategories.await(oldManga.id).map { it.id }
|
|
||||||
setMangaCategories.await(newManga.id, categoryIds)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update track
|
|
||||||
getTracks.await(oldManga.id).mapNotNull { track ->
|
|
||||||
val updatedTrack = track.copy(mangaId = newManga.id)
|
|
||||||
|
|
||||||
val service = enhancedServices
|
|
||||||
.firstOrNull { it.isTrackFrom(updatedTrack, oldManga, oldSource) }
|
|
||||||
|
|
||||||
if (service != null) {
|
|
||||||
service.migrateTrack(updatedTrack, newManga, newSource)
|
|
||||||
} else {
|
|
||||||
updatedTrack
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.takeIf { it.isNotEmpty() }
|
|
||||||
?.let { insertTrack.awaitAll(it) }
|
|
||||||
|
|
||||||
// Delete downloaded
|
|
||||||
if (deleteDownloaded) {
|
|
||||||
if (oldSource != null) {
|
|
||||||
downloadManager.deleteManga(oldManga, oldSource)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (replace) {
|
|
||||||
updateManga.awaitUpdateFavorite(oldManga.id, favorite = false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update custom cover (recheck if custom cover exists)
|
|
||||||
if (migrateCustomCover && oldManga.hasCustomCover()) {
|
|
||||||
coverCache.setCustomCoverToCache(newManga, coverCache.getCustomCoverFile(oldManga.id).inputStream())
|
|
||||||
}
|
|
||||||
|
|
||||||
updateManga.await(
|
|
||||||
MangaUpdate(
|
|
||||||
id = newManga.id,
|
|
||||||
favorite = true,
|
|
||||||
chapterFlags = oldManga.chapterFlags,
|
|
||||||
viewerFlags = oldManga.viewerFlags,
|
|
||||||
dateAdded = if (replace) oldManga.dateAdded else Instant.now().toEpochMilli(),
|
|
||||||
notes = if (migrateNotes) oldManga.notes else null,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Immutable
|
|
||||||
data class State(
|
|
||||||
val isMigrating: Boolean = false,
|
|
||||||
)
|
|
||||||
}
|
|
@ -8,7 +8,9 @@ import cafe.adriel.voyager.navigator.LocalNavigator
|
|||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import eu.kanade.presentation.browse.MigrateSearchScreen
|
import eu.kanade.presentation.browse.MigrateSearchScreen
|
||||||
import eu.kanade.presentation.util.Screen
|
import eu.kanade.presentation.util.Screen
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
||||||
|
import mihon.feature.migration.dialog.MigrateMangaDialog
|
||||||
|
|
||||||
class MigrateSearchScreen(private val mangaId: Long) : Screen() {
|
class MigrateSearchScreen(private val mangaId: Long) : Screen() {
|
||||||
|
|
||||||
@ -19,42 +21,35 @@ class MigrateSearchScreen(private val mangaId: Long) : Screen() {
|
|||||||
val screenModel = rememberScreenModel { MigrateSearchScreenModel(mangaId = mangaId) }
|
val screenModel = rememberScreenModel { MigrateSearchScreenModel(mangaId = mangaId) }
|
||||||
val state by screenModel.state.collectAsState()
|
val state by screenModel.state.collectAsState()
|
||||||
|
|
||||||
val dialogScreenModel = rememberScreenModel { MigrateSearchScreenDialogScreenModel(mangaId = mangaId) }
|
|
||||||
val dialogState by dialogScreenModel.state.collectAsState()
|
|
||||||
|
|
||||||
MigrateSearchScreen(
|
MigrateSearchScreen(
|
||||||
state = state,
|
state = state,
|
||||||
fromSourceId = dialogState.manga?.source,
|
fromSourceId = state.from?.source,
|
||||||
navigateUp = navigator::pop,
|
navigateUp = navigator::pop,
|
||||||
onChangeSearchQuery = screenModel::updateSearchQuery,
|
onChangeSearchQuery = screenModel::updateSearchQuery,
|
||||||
onSearch = { screenModel.search() },
|
onSearch = { screenModel.search() },
|
||||||
getManga = { screenModel.getManga(it) },
|
getManga = { screenModel.getManga(it) },
|
||||||
onChangeSearchFilter = screenModel::setSourceFilter,
|
onChangeSearchFilter = screenModel::setSourceFilter,
|
||||||
onToggleResults = screenModel::toggleFilterResults,
|
onToggleResults = screenModel::toggleFilterResults,
|
||||||
onClickSource = {
|
onClickSource = { navigator.push(MigrateSourceSearchScreen(state.from!!, it.id, state.searchQuery)) },
|
||||||
navigator.push(SourceSearchScreen(dialogState.manga!!, it.id, state.searchQuery))
|
onClickItem = { screenModel.setMigrateDialog(mangaId, it) },
|
||||||
},
|
|
||||||
onClickItem = { dialogScreenModel.setDialog(MigrateSearchScreenDialogScreenModel.Dialog.Migrate(it)) },
|
|
||||||
onLongClickItem = { navigator.push(MangaScreen(it.id, true)) },
|
onLongClickItem = { navigator.push(MangaScreen(it.id, true)) },
|
||||||
)
|
)
|
||||||
|
|
||||||
when (val dialog = dialogState.dialog) {
|
when (val dialog = state.dialog) {
|
||||||
is MigrateSearchScreenDialogScreenModel.Dialog.Migrate -> {
|
is SearchScreenModel.Dialog.Migrate -> {
|
||||||
MigrateDialog(
|
MigrateMangaDialog(
|
||||||
oldManga = dialogState.manga!!,
|
current = dialog.current,
|
||||||
newManga = dialog.manga,
|
target = dialog.target,
|
||||||
screenModel = rememberScreenModel { MigrateDialogScreenModel() },
|
// Initiated from the context of [dialog.current] so we show [dialog.target].
|
||||||
onDismissRequest = { dialogScreenModel.setDialog(null) },
|
onClickTitle = { navigator.push(MangaScreen(dialog.target.id, true)) },
|
||||||
onClickTitle = {
|
onDismissRequest = { screenModel.clearDialog() },
|
||||||
navigator.push(MangaScreen(dialog.manga.id, true))
|
onComplete = {
|
||||||
},
|
|
||||||
onPopScreen = {
|
|
||||||
if (navigator.lastItem is MangaScreen) {
|
if (navigator.lastItem is MangaScreen) {
|
||||||
val lastItem = navigator.lastItem
|
val lastItem = navigator.lastItem
|
||||||
navigator.popUntil { navigator.items.contains(lastItem) }
|
navigator.popUntil { navigator.items.contains(lastItem) }
|
||||||
navigator.push(MangaScreen(dialog.manga.id))
|
navigator.push(MangaScreen(dialog.target.id))
|
||||||
} else {
|
} else {
|
||||||
navigator.replace(MangaScreen(dialog.manga.id))
|
navigator.replace(MangaScreen(dialog.target.id))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -1,43 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.browse.migration.search
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
|
||||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
|
||||||
import cafe.adriel.voyager.core.model.screenModelScope
|
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import tachiyomi.domain.manga.interactor.GetManga
|
|
||||||
import tachiyomi.domain.manga.model.Manga
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
class MigrateSearchScreenDialogScreenModel(
|
|
||||||
val mangaId: Long,
|
|
||||||
getManga: GetManga = Injekt.get(),
|
|
||||||
) : StateScreenModel<MigrateSearchScreenDialogScreenModel.State>(State()) {
|
|
||||||
|
|
||||||
init {
|
|
||||||
screenModelScope.launch {
|
|
||||||
val manga = getManga.await(mangaId)!!
|
|
||||||
|
|
||||||
mutableState.update {
|
|
||||||
it.copy(manga = manga)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setDialog(dialog: Dialog?) {
|
|
||||||
mutableState.update {
|
|
||||||
it.copy(dialog = dialog)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Immutable
|
|
||||||
data class State(
|
|
||||||
val manga: Manga? = null,
|
|
||||||
val dialog: Dialog? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
sealed interface Dialog {
|
|
||||||
data class Migrate(val manga: Manga) : Dialog
|
|
||||||
}
|
|
||||||
}
|
|
@ -33,7 +33,7 @@ class MigrateSearchScreenModel(
|
|||||||
val manga = getManga.await(mangaId)!!
|
val manga = getManga.await(mangaId)!!
|
||||||
mutableState.update {
|
mutableState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
fromSourceId = manga.source,
|
from = manga,
|
||||||
searchQuery = manga.title,
|
searchQuery = manga.title,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ import eu.kanade.tachiyomi.ui.home.HomeScreen
|
|||||||
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
||||||
import eu.kanade.tachiyomi.ui.webview.WebViewScreen
|
import eu.kanade.tachiyomi.ui.webview.WebViewScreen
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import mihon.feature.migration.dialog.MigrateMangaDialog
|
||||||
import mihon.presentation.core.util.collectAsLazyPagingItems
|
import mihon.presentation.core.util.collectAsLazyPagingItems
|
||||||
import tachiyomi.core.common.Constants
|
import tachiyomi.core.common.Constants
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
@ -38,8 +39,8 @@ import tachiyomi.presentation.core.i18n.stringResource
|
|||||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
import tachiyomi.source.local.LocalSource
|
import tachiyomi.source.local.LocalSource
|
||||||
|
|
||||||
data class SourceSearchScreen(
|
data class MigrateSourceSearchScreen(
|
||||||
private val oldManga: Manga,
|
private val currentManga: Manga,
|
||||||
private val sourceId: Long,
|
private val sourceId: Long,
|
||||||
private val query: String?,
|
private val query: String?,
|
||||||
) : Screen() {
|
) : Screen() {
|
||||||
@ -82,7 +83,7 @@ data class SourceSearchScreen(
|
|||||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
val openMigrateDialog: (Manga) -> Unit = {
|
val openMigrateDialog: (Manga) -> Unit = {
|
||||||
screenModel.setDialog(BrowseSourceScreenModel.Dialog.Migrate(newManga = it, oldManga = oldManga))
|
screenModel.setDialog(BrowseSourceScreenModel.Dialog.Migrate(target = it, current = currentManga))
|
||||||
}
|
}
|
||||||
BrowseSourceContent(
|
BrowseSourceContent(
|
||||||
source = screenModel.source,
|
source = screenModel.source,
|
||||||
@ -120,17 +121,17 @@ data class SourceSearchScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
is BrowseSourceScreenModel.Dialog.Migrate -> {
|
is BrowseSourceScreenModel.Dialog.Migrate -> {
|
||||||
MigrateDialog(
|
MigrateMangaDialog(
|
||||||
oldManga = oldManga,
|
current = currentManga,
|
||||||
newManga = dialog.newManga,
|
target = dialog.target,
|
||||||
screenModel = rememberScreenModel { MigrateDialogScreenModel() },
|
// Initiated from the context of [currentManga] so we show [dialog.target].
|
||||||
|
onClickTitle = { navigator.push(MangaScreen(dialog.target.id)) },
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
onClickTitle = { navigator.push(MangaScreen(dialog.newManga.id)) },
|
onComplete = {
|
||||||
onPopScreen = {
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
navigator.popUntilRoot()
|
navigator.popUntilRoot()
|
||||||
HomeScreen.openTab(HomeScreen.Tab.Browse())
|
HomeScreen.openTab(HomeScreen.Tab.Browse())
|
||||||
navigator.push(MangaScreen(dialog.newManga.id))
|
navigator.push(MangaScreen(dialog.target.id))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
@ -47,8 +47,6 @@ import eu.kanade.presentation.util.Screen
|
|||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesScreen
|
import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesScreen
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialog
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialogScreenModel
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listing
|
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listing
|
||||||
import eu.kanade.tachiyomi.ui.category.CategoryScreen
|
import eu.kanade.tachiyomi.ui.category.CategoryScreen
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
||||||
@ -56,6 +54,7 @@ import eu.kanade.tachiyomi.ui.webview.WebViewScreen
|
|||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
|
import mihon.feature.migration.dialog.MigrateMangaDialog
|
||||||
import mihon.presentation.core.util.collectAsLazyPagingItems
|
import mihon.presentation.core.util.collectAsLazyPagingItems
|
||||||
import tachiyomi.core.common.Constants
|
import tachiyomi.core.common.Constants
|
||||||
import tachiyomi.core.common.util.lang.launchIO
|
import tachiyomi.core.common.util.lang.launchIO
|
||||||
@ -260,15 +259,12 @@ data class BrowseSourceScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
is BrowseSourceScreenModel.Dialog.Migrate -> {
|
is BrowseSourceScreenModel.Dialog.Migrate -> {
|
||||||
MigrateDialog(
|
MigrateMangaDialog(
|
||||||
oldManga = dialog.oldManga,
|
current = dialog.current,
|
||||||
newManga = dialog.newManga,
|
target = dialog.target,
|
||||||
screenModel = MigrateDialogScreenModel(),
|
// Initiated from the context of [dialog.target] so we show [dialog.current].
|
||||||
|
onClickTitle = { navigator.push(MangaScreen(dialog.current.id)) },
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
onClickTitle = { navigator.push(MangaScreen(dialog.oldManga.id)) },
|
|
||||||
onPopScreen = {
|
|
||||||
onDismissRequest()
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is BrowseSourceScreenModel.Dialog.RemoveManga -> {
|
is BrowseSourceScreenModel.Dialog.RemoveManga -> {
|
||||||
|
@ -341,7 +341,7 @@ class BrowseSourceScreenModel(
|
|||||||
val manga: Manga,
|
val manga: Manga,
|
||||||
val initialSelection: ImmutableList<CheckboxState.State<Category>>,
|
val initialSelection: ImmutableList<CheckboxState.State<Category>>,
|
||||||
) : Dialog
|
) : Dialog
|
||||||
data class Migrate(val newManga: Manga, val oldManga: Manga) : Dialog
|
data class Migrate(val target: Manga, val current: Manga) : Dialog
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
@ -25,6 +25,7 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import mihon.domain.manga.model.toDomainManga
|
import mihon.domain.manga.model.toDomainManga
|
||||||
import tachiyomi.core.common.preference.toggle
|
import tachiyomi.core.common.preference.toggle
|
||||||
|
import tachiyomi.core.common.util.lang.launchIO
|
||||||
import tachiyomi.domain.manga.interactor.GetManga
|
import tachiyomi.domain.manga.interactor.GetManga
|
||||||
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
@ -201,18 +202,34 @@ abstract class SearchScreenModel(
|
|||||||
updateItems(newItems)
|
updateItems(newItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setMigrateDialog(currentId: Long, target: Manga) {
|
||||||
|
screenModelScope.launchIO {
|
||||||
|
val current = getManga.await(currentId) ?: return@launchIO
|
||||||
|
mutableState.update { it.copy(dialog = Dialog.Migrate(target, current)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearDialog() {
|
||||||
|
mutableState.update { it.copy(dialog = null) }
|
||||||
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class State(
|
data class State(
|
||||||
val fromSourceId: Long? = null,
|
val from: Manga? = null,
|
||||||
val searchQuery: String? = null,
|
val searchQuery: String? = null,
|
||||||
val sourceFilter: SourceFilter = SourceFilter.PinnedOnly,
|
val sourceFilter: SourceFilter = SourceFilter.PinnedOnly,
|
||||||
val onlyShowHasResults: Boolean = false,
|
val onlyShowHasResults: Boolean = false,
|
||||||
val items: PersistentMap<CatalogueSource, SearchItemResult> = persistentMapOf(),
|
val items: PersistentMap<CatalogueSource, SearchItemResult> = persistentMapOf(),
|
||||||
|
val dialog: Dialog? = null,
|
||||||
) {
|
) {
|
||||||
val progress: Int = items.count { it.value !is SearchItemResult.Loading }
|
val progress: Int = items.count { it.value !is SearchItemResult.Loading }
|
||||||
val total: Int = items.size
|
val total: Int = items.size
|
||||||
val filteredItems = items.filter { (_, result) -> result.isVisible(onlyShowHasResults) }
|
val filteredItems = items.filter { (_, result) -> result.isVisible(onlyShowHasResults) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sealed interface Dialog {
|
||||||
|
data class Migrate(val target: Manga, val current: Manga) : Dialog
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class SourceFilter {
|
enum class SourceFilter {
|
||||||
|
@ -216,9 +216,9 @@ class HistoryScreenModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showMigrateDialog(currentManga: Manga, duplicate: Manga) {
|
fun showMigrateDialog(target: Manga, current: Manga) {
|
||||||
mutableState.update { currentState ->
|
mutableState.update { currentState ->
|
||||||
currentState.copy(dialog = Dialog.Migrate(newManga = currentManga, oldManga = duplicate))
|
currentState.copy(dialog = Dialog.Migrate(target = target, current = current))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,7 +252,7 @@ class HistoryScreenModel(
|
|||||||
val manga: Manga,
|
val manga: Manga,
|
||||||
val initialSelection: ImmutableList<CheckboxState<Category>>,
|
val initialSelection: ImmutableList<CheckboxState<Category>>,
|
||||||
) : Dialog
|
) : Dialog
|
||||||
data class Migrate(val newManga: Manga, val oldManga: Manga) : Dialog
|
data class Migrate(val target: Manga, val current: Manga) : Dialog
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface Event {
|
sealed interface Event {
|
||||||
|
@ -23,8 +23,6 @@ import eu.kanade.presentation.history.components.HistoryDeleteDialog
|
|||||||
import eu.kanade.presentation.manga.DuplicateMangaDialog
|
import eu.kanade.presentation.manga.DuplicateMangaDialog
|
||||||
import eu.kanade.presentation.util.Tab
|
import eu.kanade.presentation.util.Tab
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialog
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialogScreenModel
|
|
||||||
import eu.kanade.tachiyomi.ui.category.CategoryScreen
|
import eu.kanade.tachiyomi.ui.category.CategoryScreen
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
||||||
@ -32,6 +30,7 @@ import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
|||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
|
import mihon.feature.migration.dialog.MigrateMangaDialog
|
||||||
import tachiyomi.core.common.i18n.stringResource
|
import tachiyomi.core.common.i18n.stringResource
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
@ -100,9 +99,7 @@ data object HistoryTab : Tab {
|
|||||||
DuplicateMangaDialog(
|
DuplicateMangaDialog(
|
||||||
duplicates = dialog.duplicates,
|
duplicates = dialog.duplicates,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
onConfirm = {
|
onConfirm = { screenModel.addFavorite(dialog.manga) },
|
||||||
screenModel.addFavorite(dialog.manga)
|
|
||||||
},
|
|
||||||
onOpenManga = { navigator.push(MangaScreen(it.id)) },
|
onOpenManga = { navigator.push(MangaScreen(it.id)) },
|
||||||
onMigrate = { screenModel.showMigrateDialog(dialog.manga, it) },
|
onMigrate = { screenModel.showMigrateDialog(dialog.manga, it) },
|
||||||
)
|
)
|
||||||
@ -118,13 +115,12 @@ data object HistoryTab : Tab {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
is HistoryScreenModel.Dialog.Migrate -> {
|
is HistoryScreenModel.Dialog.Migrate -> {
|
||||||
MigrateDialog(
|
MigrateMangaDialog(
|
||||||
oldManga = dialog.oldManga,
|
current = dialog.current,
|
||||||
newManga = dialog.newManga,
|
target = dialog.target,
|
||||||
screenModel = MigrateDialogScreenModel(),
|
// Initiated from the context of [dialog.target] so we show [dialog.current].
|
||||||
|
onClickTitle = { navigator.push(MangaScreen(dialog.current.id)) },
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
onClickTitle = { navigator.push(MangaScreen(dialog.oldManga.id)) },
|
|
||||||
onPopScreen = onDismissRequest,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
null -> {}
|
null -> {}
|
||||||
|
@ -43,8 +43,6 @@ import eu.kanade.presentation.util.isTabletUi
|
|||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.isLocalOrStub
|
import eu.kanade.tachiyomi.source.isLocalOrStub
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialog
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateDialogScreenModel
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
|
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen
|
||||||
import eu.kanade.tachiyomi.ui.category.CategoryScreen
|
import eu.kanade.tachiyomi.ui.category.CategoryScreen
|
||||||
@ -60,6 +58,7 @@ import eu.kanade.tachiyomi.util.system.toast
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import mihon.feature.migration.config.MigrationConfigScreen
|
import mihon.feature.migration.config.MigrationConfigScreen
|
||||||
|
import mihon.feature.migration.dialog.MigrateMangaDialog
|
||||||
import tachiyomi.core.common.i18n.stringResource
|
import tachiyomi.core.common.i18n.stringResource
|
||||||
import tachiyomi.core.common.util.lang.withIOContext
|
import tachiyomi.core.common.util.lang.withIOContext
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
@ -212,13 +211,12 @@ class MangaScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
is MangaScreenModel.Dialog.Migrate -> {
|
is MangaScreenModel.Dialog.Migrate -> {
|
||||||
MigrateDialog(
|
MigrateMangaDialog(
|
||||||
oldManga = dialog.oldManga,
|
current = dialog.current,
|
||||||
newManga = dialog.newManga,
|
target = dialog.target,
|
||||||
screenModel = MigrateDialogScreenModel(),
|
// Initiated from the context of [dialog.target] so we show [dialog.current].
|
||||||
|
onClickTitle = { navigator.push(MangaScreen(dialog.current.id)) },
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
onClickTitle = { navigator.push(MangaScreen(dialog.oldManga.id)) },
|
|
||||||
onPopScreen = onDismissRequest,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
MangaScreenModel.Dialog.SettingsSheet -> ChapterSettingsDialog(
|
MangaScreenModel.Dialog.SettingsSheet -> ChapterSettingsDialog(
|
||||||
|
@ -1071,7 +1071,7 @@ class MangaScreenModel(
|
|||||||
) : Dialog
|
) : Dialog
|
||||||
data class DeleteChapters(val chapters: List<Chapter>) : Dialog
|
data class DeleteChapters(val chapters: List<Chapter>) : Dialog
|
||||||
data class DuplicateManga(val manga: Manga, val duplicates: List<MangaWithChapterCount>) : Dialog
|
data class DuplicateManga(val manga: Manga, val duplicates: List<MangaWithChapterCount>) : Dialog
|
||||||
data class Migrate(val newManga: Manga, val oldManga: Manga) : Dialog
|
data class Migrate(val target: Manga, val current: Manga) : Dialog
|
||||||
data class SetFetchInterval(val manga: Manga) : Dialog
|
data class SetFetchInterval(val manga: Manga) : Dialog
|
||||||
data object SettingsSheet : Dialog
|
data object SettingsSheet : Dialog
|
||||||
data object TrackSheet : Dialog
|
data object TrackSheet : Dialog
|
||||||
@ -1100,7 +1100,7 @@ class MangaScreenModel(
|
|||||||
|
|
||||||
fun showMigrateDialog(duplicate: Manga) {
|
fun showMigrateDialog(duplicate: Manga) {
|
||||||
val manga = successState?.manga ?: return
|
val manga = successState?.manga ?: return
|
||||||
updateSuccessState { it.copy(dialog = Dialog.Migrate(newManga = manga, oldManga = duplicate)) }
|
updateSuccessState { it.copy(dialog = Dialog.Migrate(target = manga, current = duplicate)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setExcludedScanlators(excludedScanlators: Set<String>) {
|
fun setExcludedScanlators(excludedScanlators: Set<String>) {
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
package mihon.domain.migration.models
|
||||||
|
|
||||||
|
enum class MigrationFlag(val flag: Int) {
|
||||||
|
CHAPTER(0b00001),
|
||||||
|
CATEGORY(0b00010),
|
||||||
|
|
||||||
|
// 0b00100 was used for manga trackers
|
||||||
|
CUSTOM_COVER(0b01000),
|
||||||
|
NOTES(0b100000),
|
||||||
|
REMOVE_DOWNLOAD(0b10000),
|
||||||
|
;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromBit(bit: Int): Set<MigrationFlag> {
|
||||||
|
return buildSet {
|
||||||
|
entries.forEach { entry ->
|
||||||
|
if (bit and entry.flag != 0) add(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toBit(flags: Set<MigrationFlag>): Int {
|
||||||
|
return flags.map { it.flag }
|
||||||
|
.reduceOrNull { acc, mask -> acc or mask }
|
||||||
|
?: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,145 @@
|
|||||||
|
package mihon.domain.migration.usecases
|
||||||
|
|
||||||
|
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
|
||||||
|
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||||
|
import eu.kanade.domain.manga.model.hasCustomCover
|
||||||
|
import eu.kanade.domain.manga.model.toSManga
|
||||||
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
|
import eu.kanade.tachiyomi.data.track.EnhancedTracker
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import mihon.domain.migration.models.MigrationFlag
|
||||||
|
import tachiyomi.domain.category.interactor.GetCategories
|
||||||
|
import tachiyomi.domain.category.interactor.SetMangaCategories
|
||||||
|
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||||
|
import tachiyomi.domain.chapter.interactor.UpdateChapter
|
||||||
|
import tachiyomi.domain.chapter.model.toChapterUpdate
|
||||||
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
import tachiyomi.domain.manga.model.MangaUpdate
|
||||||
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
|
import tachiyomi.domain.track.interactor.GetTracks
|
||||||
|
import tachiyomi.domain.track.interactor.InsertTrack
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
class MigrateMangaUseCase(
|
||||||
|
private val sourcePreferences: SourcePreferences,
|
||||||
|
private val trackerManager: TrackerManager,
|
||||||
|
private val sourceManager: SourceManager,
|
||||||
|
private val downloadManager: DownloadManager,
|
||||||
|
private val updateManga: UpdateManga,
|
||||||
|
private val getChaptersByMangaId: GetChaptersByMangaId,
|
||||||
|
private val syncChaptersWithSource: SyncChaptersWithSource,
|
||||||
|
private val updateChapter: UpdateChapter,
|
||||||
|
private val getCategories: GetCategories,
|
||||||
|
private val setMangaCategories: SetMangaCategories,
|
||||||
|
private val getTracks: GetTracks,
|
||||||
|
private val insertTrack: InsertTrack,
|
||||||
|
private val coverCache: CoverCache,
|
||||||
|
) {
|
||||||
|
private val enhancedServices by lazy { trackerManager.trackers.filterIsInstance<EnhancedTracker>() }
|
||||||
|
|
||||||
|
suspend operator fun invoke(current: Manga, target: Manga, replace: Boolean) {
|
||||||
|
val targetSource = sourceManager.get(target.source) ?: return
|
||||||
|
val currentSource = sourceManager.get(current.source)
|
||||||
|
val flags = sourcePreferences.migrationFlags().get()
|
||||||
|
|
||||||
|
try {
|
||||||
|
val chapters = targetSource.getChapterList(target.toSManga())
|
||||||
|
|
||||||
|
try {
|
||||||
|
syncChaptersWithSource.await(chapters, target, targetSource)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
// Worst case, chapters won't be synced
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update chapters read, bookmark and dateFetch
|
||||||
|
if (MigrationFlag.CHAPTER in flags) {
|
||||||
|
val prevMangaChapters = getChaptersByMangaId.await(current.id)
|
||||||
|
val mangaChapters = getChaptersByMangaId.await(target.id)
|
||||||
|
|
||||||
|
val maxChapterRead = prevMangaChapters
|
||||||
|
.filter { it.read }
|
||||||
|
.maxOfOrNull { it.chapterNumber }
|
||||||
|
|
||||||
|
val updatedMangaChapters = mangaChapters.map { mangaChapter ->
|
||||||
|
var updatedChapter = mangaChapter
|
||||||
|
if (updatedChapter.isRecognizedNumber) {
|
||||||
|
val prevChapter = prevMangaChapters
|
||||||
|
.find { it.isRecognizedNumber && it.chapterNumber == updatedChapter.chapterNumber }
|
||||||
|
|
||||||
|
if (prevChapter != null) {
|
||||||
|
updatedChapter = updatedChapter.copy(
|
||||||
|
dateFetch = prevChapter.dateFetch,
|
||||||
|
bookmark = prevChapter.bookmark,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxChapterRead != null && updatedChapter.chapterNumber <= maxChapterRead) {
|
||||||
|
updatedChapter = updatedChapter.copy(read = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedChapter
|
||||||
|
}
|
||||||
|
|
||||||
|
val chapterUpdates = updatedMangaChapters.map { it.toChapterUpdate() }
|
||||||
|
updateChapter.awaitAll(chapterUpdates)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update categories
|
||||||
|
if (MigrationFlag.CHAPTER in flags) {
|
||||||
|
val categoryIds = getCategories.await(current.id).map { it.id }
|
||||||
|
setMangaCategories.await(target.id, categoryIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update track
|
||||||
|
getTracks.await(current.id).mapNotNull { track ->
|
||||||
|
val updatedTrack = track.copy(mangaId = target.id)
|
||||||
|
|
||||||
|
val service = enhancedServices
|
||||||
|
.firstOrNull { it.isTrackFrom(updatedTrack, current, currentSource) }
|
||||||
|
|
||||||
|
if (service != null) {
|
||||||
|
service.migrateTrack(updatedTrack, target, targetSource)
|
||||||
|
} else {
|
||||||
|
updatedTrack
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.takeIf { it.isNotEmpty() }
|
||||||
|
?.let { insertTrack.awaitAll(it) }
|
||||||
|
|
||||||
|
// Delete downloaded
|
||||||
|
if (MigrationFlag.REMOVE_DOWNLOAD in flags && currentSource != null) {
|
||||||
|
downloadManager.deleteManga(current, currentSource)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update custom cover (recheck if custom cover exists)
|
||||||
|
if (MigrationFlag.CUSTOM_COVER in flags && current.hasCustomCover()) {
|
||||||
|
coverCache.setCustomCoverToCache(target, coverCache.getCustomCoverFile(current.id).inputStream())
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentMangaUpdate = MangaUpdate(
|
||||||
|
id = current.id,
|
||||||
|
favorite = false,
|
||||||
|
dateAdded = 0,
|
||||||
|
)
|
||||||
|
.takeIf { replace }
|
||||||
|
val targetMangaUpdate = MangaUpdate(
|
||||||
|
id = target.id,
|
||||||
|
favorite = true,
|
||||||
|
chapterFlags = current.chapterFlags,
|
||||||
|
viewerFlags = current.viewerFlags,
|
||||||
|
dateAdded = if (replace) current.dateAdded else Instant.now().toEpochMilli(),
|
||||||
|
notes = if (MigrationFlag.NOTES in flags) current.notes else null,
|
||||||
|
)
|
||||||
|
|
||||||
|
updateManga.awaitAll(listOfNotNull(currentMangaUpdate, targetMangaUpdate))
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
if (e is CancellationException) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
package mihon.feature.common.utils
|
||||||
|
|
||||||
|
import dev.icerock.moko.resources.StringResource
|
||||||
|
import mihon.domain.migration.models.MigrationFlag
|
||||||
|
import tachiyomi.i18n.MR
|
||||||
|
|
||||||
|
fun MigrationFlag.getLabel(): StringResource {
|
||||||
|
return when (this) {
|
||||||
|
MigrationFlag.CHAPTER -> MR.strings.chapters
|
||||||
|
MigrationFlag.CATEGORY -> MR.strings.categories
|
||||||
|
MigrationFlag.CUSTOM_COVER -> MR.strings.custom_cover
|
||||||
|
MigrationFlag.NOTES -> MR.strings.action_notes
|
||||||
|
MigrationFlag.REMOVE_DOWNLOAD -> MR.strings.delete_downloaded
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,167 @@
|
|||||||
|
package mihon.feature.migration.dialog
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.util.fastForEach
|
||||||
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
|
import cafe.adriel.voyager.core.screen.Screen
|
||||||
|
import eu.kanade.domain.manga.model.hasCustomCover
|
||||||
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import mihon.domain.migration.models.MigrationFlag
|
||||||
|
import mihon.domain.migration.usecases.MigrateMangaUseCase
|
||||||
|
import mihon.feature.common.utils.getLabel
|
||||||
|
import tachiyomi.core.common.util.lang.launchIO
|
||||||
|
import tachiyomi.core.common.util.lang.withUIContext
|
||||||
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
import tachiyomi.i18n.MR
|
||||||
|
import tachiyomi.presentation.core.components.LabeledCheckbox
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun Screen.MigrateMangaDialog(
|
||||||
|
current: Manga,
|
||||||
|
target: Manga,
|
||||||
|
onClickTitle: () -> Unit,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onComplete: () -> Unit = onDismissRequest,
|
||||||
|
) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val screenModel = rememberScreenModel { MigrateDialogScreenModel(current, target) }
|
||||||
|
val state by screenModel.state.collectAsState()
|
||||||
|
|
||||||
|
if (state.isMigrating) {
|
||||||
|
LoadingScreen(
|
||||||
|
modifier = Modifier.background(MaterialTheme.colorScheme.background.copy(alpha = 0.7f)),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
title = {
|
||||||
|
Text(text = stringResource(MR.strings.migration_dialog_what_to_include))
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||||
|
) {
|
||||||
|
state.applicableFlags.fastForEach { flag ->
|
||||||
|
LabeledCheckbox(
|
||||||
|
label = stringResource(flag.getLabel()),
|
||||||
|
checked = flag in state.selectedFlags,
|
||||||
|
onCheckedChange = { screenModel.toggleSelection(flag) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
FlowRow(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
onDismissRequest()
|
||||||
|
onClickTitle()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(MR.strings.action_show_manga))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
scope.launchIO {
|
||||||
|
screenModel.migrateManga(replace = false)
|
||||||
|
withUIContext { onComplete() }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(MR.strings.copy))
|
||||||
|
}
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
scope.launchIO {
|
||||||
|
screenModel.migrateManga(replace = true)
|
||||||
|
withUIContext { onComplete() }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(MR.strings.migrate))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MigrateDialogScreenModel(
|
||||||
|
private val current: Manga,
|
||||||
|
private val target: Manga,
|
||||||
|
private val sourcePreference: SourcePreferences = Injekt.get(),
|
||||||
|
private val coverCache: CoverCache = Injekt.get(),
|
||||||
|
private val downloadManager: DownloadManager = Injekt.get(),
|
||||||
|
private val migrateManga: MigrateMangaUseCase = Injekt.get(),
|
||||||
|
) : StateScreenModel<MigrateDialogScreenModel.State>(State()) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
val applicableFlags = buildList {
|
||||||
|
MigrationFlag.entries.forEach {
|
||||||
|
val applicable = when (it) {
|
||||||
|
MigrationFlag.CHAPTER -> true
|
||||||
|
MigrationFlag.CATEGORY -> true
|
||||||
|
MigrationFlag.CUSTOM_COVER -> current.hasCustomCover(coverCache)
|
||||||
|
MigrationFlag.NOTES -> current.notes.isNotBlank()
|
||||||
|
MigrationFlag.REMOVE_DOWNLOAD -> downloadManager.getDownloadCount(current) > 0
|
||||||
|
}
|
||||||
|
if (applicable) add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val selectedFlags = sourcePreference.migrationFlags().get()
|
||||||
|
mutableState.update { it.copy(applicableFlags = applicableFlags, selectedFlags = selectedFlags) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleSelection(flag: MigrationFlag) {
|
||||||
|
mutableState.update {
|
||||||
|
val selectedFlags = it.selectedFlags.toMutableSet()
|
||||||
|
.apply { if (contains(flag)) remove(flag) else add(flag) }
|
||||||
|
.toSet()
|
||||||
|
it.copy(selectedFlags = selectedFlags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun migrateManga(replace: Boolean) {
|
||||||
|
sourcePreference.migrationFlags().set(state.value.selectedFlags)
|
||||||
|
mutableState.update { it.copy(isMigrating = true) }
|
||||||
|
migrateManga(current, target, replace)
|
||||||
|
mutableState.update { it.copy(isMigrating = false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
data class State(
|
||||||
|
val applicableFlags: List<MigrationFlag> = emptyList(),
|
||||||
|
val selectedFlags: Set<MigrationFlag> = emptySet(),
|
||||||
|
val isMigrating: Boolean = false,
|
||||||
|
)
|
||||||
|
}
|
@ -171,13 +171,13 @@ sealed class AndroidPreference<T>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Object<T>(
|
class ObjectAsString<T>(
|
||||||
preferences: SharedPreferences,
|
preferences: SharedPreferences,
|
||||||
keyFlow: Flow<String?>,
|
keyFlow: Flow<String?>,
|
||||||
key: String,
|
key: String,
|
||||||
defaultValue: T,
|
defaultValue: T,
|
||||||
val serializer: (T) -> String,
|
private val serializer: (T) -> String,
|
||||||
val deserializer: (String) -> T,
|
private val deserializer: (String) -> T,
|
||||||
) : AndroidPreference<T>(preferences, keyFlow, key, defaultValue) {
|
) : AndroidPreference<T>(preferences, keyFlow, key, defaultValue) {
|
||||||
override fun read(preferences: SharedPreferences, key: String, defaultValue: T): T {
|
override fun read(preferences: SharedPreferences, key: String, defaultValue: T): T {
|
||||||
return try {
|
return try {
|
||||||
@ -191,4 +191,25 @@ sealed class AndroidPreference<T>(
|
|||||||
putString(key, serializer(value))
|
putString(key, serializer(value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ObjectAsInt<T>(
|
||||||
|
preferences: SharedPreferences,
|
||||||
|
keyFlow: Flow<String?>,
|
||||||
|
key: String,
|
||||||
|
defaultValue: T,
|
||||||
|
private val serializer: (T) -> Int,
|
||||||
|
private val deserializer: (Int) -> T,
|
||||||
|
) : AndroidPreference<T>(preferences, keyFlow, key, defaultValue) {
|
||||||
|
override fun read(preferences: SharedPreferences, key: String, defaultValue: T): T {
|
||||||
|
return try {
|
||||||
|
if (preferences.contains(key)) preferences.getInt(key, 0).let(deserializer) else defaultValue
|
||||||
|
} catch (e: Exception) {
|
||||||
|
defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun write(key: String, value: T): Editor.() -> Unit = {
|
||||||
|
putInt(key, serializer(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,8 @@ import tachiyomi.core.common.preference.AndroidPreference.BooleanPrimitive
|
|||||||
import tachiyomi.core.common.preference.AndroidPreference.FloatPrimitive
|
import tachiyomi.core.common.preference.AndroidPreference.FloatPrimitive
|
||||||
import tachiyomi.core.common.preference.AndroidPreference.IntPrimitive
|
import tachiyomi.core.common.preference.AndroidPreference.IntPrimitive
|
||||||
import tachiyomi.core.common.preference.AndroidPreference.LongPrimitive
|
import tachiyomi.core.common.preference.AndroidPreference.LongPrimitive
|
||||||
import tachiyomi.core.common.preference.AndroidPreference.Object
|
import tachiyomi.core.common.preference.AndroidPreference.ObjectAsInt
|
||||||
|
import tachiyomi.core.common.preference.AndroidPreference.ObjectAsString
|
||||||
import tachiyomi.core.common.preference.AndroidPreference.StringPrimitive
|
import tachiyomi.core.common.preference.AndroidPreference.StringPrimitive
|
||||||
import tachiyomi.core.common.preference.AndroidPreference.StringSetPrimitive
|
import tachiyomi.core.common.preference.AndroidPreference.StringSetPrimitive
|
||||||
|
|
||||||
@ -44,13 +45,29 @@ class AndroidPreferenceStore(
|
|||||||
return StringSetPrimitive(sharedPreferences, keyFlow, key, defaultValue)
|
return StringSetPrimitive(sharedPreferences, keyFlow, key, defaultValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun <T> getObject(
|
override fun <T> getObjectFromString(
|
||||||
key: String,
|
key: String,
|
||||||
defaultValue: T,
|
defaultValue: T,
|
||||||
serializer: (T) -> String,
|
serializer: (T) -> String,
|
||||||
deserializer: (String) -> T,
|
deserializer: (String) -> T,
|
||||||
): Preference<T> {
|
): Preference<T> {
|
||||||
return Object(
|
return ObjectAsString(
|
||||||
|
preferences = sharedPreferences,
|
||||||
|
keyFlow = keyFlow,
|
||||||
|
key = key,
|
||||||
|
defaultValue = defaultValue,
|
||||||
|
serializer = serializer,
|
||||||
|
deserializer = deserializer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun <T> getObjectFromInt(
|
||||||
|
key: String,
|
||||||
|
defaultValue: T,
|
||||||
|
serializer: (T) -> Int,
|
||||||
|
deserializer: (Int) -> T,
|
||||||
|
): Preference<T> {
|
||||||
|
return ObjectAsInt(
|
||||||
preferences = sharedPreferences,
|
preferences = sharedPreferences,
|
||||||
keyFlow = keyFlow,
|
keyFlow = keyFlow,
|
||||||
key = key,
|
key = key,
|
||||||
|
@ -52,7 +52,7 @@ class InMemoryPreferenceStore(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun <T> getObject(
|
override fun <T> getObjectFromString(
|
||||||
key: String,
|
key: String,
|
||||||
defaultValue: T,
|
defaultValue: T,
|
||||||
serializer: (T) -> String,
|
serializer: (T) -> String,
|
||||||
@ -63,6 +63,18 @@ class InMemoryPreferenceStore(
|
|||||||
return if (data == null) default else InMemoryPreference(key, data, defaultValue)
|
return if (data == null) default else InMemoryPreference(key, data, defaultValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun <T> getObjectFromInt(
|
||||||
|
key: String,
|
||||||
|
defaultValue: T,
|
||||||
|
serializer: (T) -> Int,
|
||||||
|
deserializer: (Int) -> T,
|
||||||
|
): Preference<T> {
|
||||||
|
val default = InMemoryPreference(key, null, defaultValue)
|
||||||
|
val data: T? = preferences[key]?.get() as? T
|
||||||
|
return if (data == null) default else InMemoryPreference(key, data, defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getAll(): Map<String, *> {
|
override fun getAll(): Map<String, *> {
|
||||||
return preferences
|
return preferences
|
||||||
}
|
}
|
||||||
|
@ -14,13 +14,20 @@ interface PreferenceStore {
|
|||||||
|
|
||||||
fun getStringSet(key: String, defaultValue: Set<String> = emptySet()): Preference<Set<String>>
|
fun getStringSet(key: String, defaultValue: Set<String> = emptySet()): Preference<Set<String>>
|
||||||
|
|
||||||
fun <T> getObject(
|
fun <T> getObjectFromString(
|
||||||
key: String,
|
key: String,
|
||||||
defaultValue: T,
|
defaultValue: T,
|
||||||
serializer: (T) -> String,
|
serializer: (T) -> String,
|
||||||
deserializer: (String) -> T,
|
deserializer: (String) -> T,
|
||||||
): Preference<T>
|
): Preference<T>
|
||||||
|
|
||||||
|
fun <T> getObjectFromInt(
|
||||||
|
key: String,
|
||||||
|
defaultValue: T,
|
||||||
|
serializer: (T) -> Int,
|
||||||
|
deserializer: (Int) -> T,
|
||||||
|
): Preference<T>
|
||||||
|
|
||||||
fun getAll(): Map<String, *>
|
fun getAll(): Map<String, *>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,7 +35,7 @@ fun PreferenceStore.getLongArray(
|
|||||||
key: String,
|
key: String,
|
||||||
defaultValue: List<Long>,
|
defaultValue: List<Long>,
|
||||||
): Preference<List<Long>> {
|
): Preference<List<Long>> {
|
||||||
return getObject(
|
return getObjectFromString(
|
||||||
key = key,
|
key = key,
|
||||||
defaultValue = defaultValue,
|
defaultValue = defaultValue,
|
||||||
serializer = { it.joinToString(",") },
|
serializer = { it.joinToString(",") },
|
||||||
@ -40,7 +47,7 @@ inline fun <reified T : Enum<T>> PreferenceStore.getEnum(
|
|||||||
key: String,
|
key: String,
|
||||||
defaultValue: T,
|
defaultValue: T,
|
||||||
): Preference<T> {
|
): Preference<T> {
|
||||||
return getObject(
|
return getObjectFromString(
|
||||||
key = key,
|
key = key,
|
||||||
defaultValue = defaultValue,
|
defaultValue = defaultValue,
|
||||||
serializer = { it.name },
|
serializer = { it.name },
|
||||||
|
@ -12,14 +12,14 @@ class LibraryPreferences(
|
|||||||
private val preferenceStore: PreferenceStore,
|
private val preferenceStore: PreferenceStore,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun displayMode() = preferenceStore.getObject(
|
fun displayMode() = preferenceStore.getObjectFromString(
|
||||||
"pref_display_mode_library",
|
"pref_display_mode_library",
|
||||||
LibraryDisplayMode.default,
|
LibraryDisplayMode.default,
|
||||||
LibraryDisplayMode.Serializer::serialize,
|
LibraryDisplayMode.Serializer::serialize,
|
||||||
LibraryDisplayMode.Serializer::deserialize,
|
LibraryDisplayMode.Serializer::deserialize,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun sortingMode() = preferenceStore.getObject(
|
fun sortingMode() = preferenceStore.getObjectFromString(
|
||||||
"library_sorting_mode",
|
"library_sorting_mode",
|
||||||
LibrarySort.default,
|
LibrarySort.default,
|
||||||
LibrarySort.Serializer::serialize,
|
LibrarySort.Serializer::serialize,
|
||||||
|
Reference in New Issue
Block a user