Rewrite Migrations (#577)

* Rewrite Migrations

* Fix Detekt errors

* Do migrations synchronous

* Filter and sort migrations

* Review changes

* Review changes 2

* Fix Detekt errors

(cherry picked from commit 666d6aa117756f0a9a57b31f91b7acb0ee5d7409)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/Migrations.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
This commit is contained in:
Andreas 2024-03-25 18:26:19 +01:00 committed by Jobobby04
parent ceff887a10
commit 90d5104bdc
51 changed files with 1670 additions and 954 deletions

View File

@ -1,71 +0,0 @@
package eu.kanade.tachiyomi
import android.content.Context
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import logcat.LogPriority
import mihon.domain.extensionrepo.exception.SaveExtensionRepoException
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.system.logcat
object Migrations {
// TODO NATIVE TACHIYOMI MIGRATIONS ARE FUCKED UP DUE TO DIFFERING VERSION NUMBERS
/**
* Performs a migration when the application is updated.
*
* @return true if a migration is performed, false otherwise.
*/
@Suppress("SameReturnValue", "MagicNumber")
fun upgrade(
context: Context,
preferenceStore: PreferenceStore,
sourcePreferences: SourcePreferences,
extensionRepoRepository: ExtensionRepoRepository,
): Boolean {
val lastVersionCode = preferenceStore.getInt(Preference.appStateKey("last_version_code"), 0)
val oldVersion = lastVersionCode.get()
if (oldVersion < BuildConfig.VERSION_CODE) {
lastVersionCode.set(BuildConfig.VERSION_CODE)
// Always set up background tasks to ensure they're running
LibraryUpdateJob.setupTask(context)
BackupCreateJob.setupTask(context)
// Fresh install
if (oldVersion == 0) {
return false
}
val coroutineScope = CoroutineScope(Dispatchers.IO)
if (oldVersion < 6) {
coroutineScope.launchIO {
for ((index, source) in sourcePreferences.extensionRepos().get().withIndex()) {
try {
extensionRepoRepository.upsertRepo(
source,
"Repo #${index + 1}",
null,
source,
"NOFINGERPRINT-${index + 1}",
)
} catch (e: SaveExtensionRepoException) {
logcat(LogPriority.ERROR, e) { "Error Migrating Extension Repo with baseUrl: $source" }
}
}
sourcePreferences.extensionRepos().delete()
}
}
}
return false
}
}

View File

@ -50,8 +50,6 @@ import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.google.firebase.analytics.ktx.analytics import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.ktx.Firebase import com.google.firebase.ktx.Firebase
import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.components.AppStateBanners import eu.kanade.presentation.components.AppStateBanners
import eu.kanade.presentation.components.DownloadedOnlyBannerBackgroundColor import eu.kanade.presentation.components.DownloadedOnlyBannerBackgroundColor
import eu.kanade.presentation.components.IncognitoModeBannerBackgroundColor import eu.kanade.presentation.components.IncognitoModeBannerBackgroundColor
@ -80,7 +78,6 @@ import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.util.system.isNavigationBarNeedsScrim import eu.kanade.tachiyomi.util.system.isNavigationBarNeedsScrim
import eu.kanade.tachiyomi.util.system.isPreviewBuildType import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import eu.kanade.tachiyomi.util.view.setComposeContent import eu.kanade.tachiyomi.util.view.setComposeContent
import exh.EXHMigrations
import exh.debug.DebugToggles import exh.debug.DebugToggles
import exh.eh.EHentaiUpdateWorker import exh.eh.EHentaiUpdateWorker
import exh.log.DebugModeOverlay import exh.log.DebugModeOverlay
@ -97,7 +94,11 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import logcat.LogPriority import logcat.LogPriority
import mihon.core.migration.Migrator
import mihon.core.migration.migrations.migrations
import tachiyomi.core.common.Constants import tachiyomi.core.common.Constants
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.launchIO
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.UnsortedPreferences import tachiyomi.domain.UnsortedPreferences
@ -113,9 +114,7 @@ import androidx.compose.ui.graphics.Color.Companion as ComposeColor
class MainActivity : BaseActivity() { class MainActivity : BaseActivity() {
private val sourcePreferences: SourcePreferences by injectLazy()
private val libraryPreferences: LibraryPreferences by injectLazy() private val libraryPreferences: LibraryPreferences by injectLazy()
private val uiPreferences: UiPreferences by injectLazy()
private val preferences: BasePreferences by injectLazy() private val preferences: BasePreferences by injectLazy()
// SY --> // SY -->
@ -165,21 +164,7 @@ class MainActivity : BaseActivity() {
val didMigration = if (isLaunch) { val didMigration = if (isLaunch) {
addAnalytics() addAnalytics()
EXHMigrations.upgrade( migrate()
context = applicationContext,
basePreferences = preferences,
uiPreferences = uiPreferences,
preferenceStore = Injekt.get(),
networkPreferences = Injekt.get(),
sourcePreferences = sourcePreferences,
securityPreferences = Injekt.get(),
libraryPreferences = libraryPreferences,
readerPreferences = Injekt.get(),
backupPreferences = Injekt.get(),
trackerManager = Injekt.get(),
pagePreviewCache = Injekt.get(),
extensionRepoRepository = Injekt.get(),
)
} else { } else {
false false
} }
@ -423,6 +408,23 @@ class MainActivity : BaseActivity() {
} }
} }
private fun migrate(): Boolean {
val preferenceStore = Injekt.get<PreferenceStore>()
// SY -->
val preference = preferenceStore.getInt(Preference.appStateKey("eh_last_version_code"), 0)
// SY <--
logcat { "Migration from ${preference.get()} to ${BuildConfig.VERSION_CODE}" }
return Migrator.migrate(
old = preference.get(),
new = BuildConfig.VERSION_CODE,
migrations = migrations,
onMigrationComplete = {
logcat { "Updating last version to ${BuildConfig.VERSION_CODE}" }
preference.set(BuildConfig.VERSION_CODE)
},
)
}
/** /**
* Sets custom splash screen exit animation on devices prior to Android 12. * Sets custom splash screen exit animation on devices prior to Android 12.
* *

View File

@ -1,723 +1,15 @@
package exh package exh
import android.content.Context
import android.widget.Toast
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
import eu.kanade.tachiyomi.data.cache.PagePreviewCache
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.sync.SyncDataJob
import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.network.NetworkPreferences
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.online.all.NHentai import eu.kanade.tachiyomi.source.online.all.NHentai
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.system.workManager
import exh.eh.EHentaiUpdateWorker
import exh.log.xLogE
import exh.source.BlacklistedSources import exh.source.BlacklistedSources
import exh.source.EH_SOURCE_ID import exh.source.EH_SOURCE_ID
import exh.source.HBROWSE_SOURCE_ID import exh.source.HBROWSE_SOURCE_ID
import exh.source.MERGED_SOURCE_ID
import exh.source.TSUMINO_SOURCE_ID import exh.source.TSUMINO_SOURCE_ID
import exh.util.nullIfBlank
import exh.util.under
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonPrimitive
import logcat.LogPriority
import mihon.domain.extensionrepo.exception.SaveExtensionRepoException
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.core.common.preference.TriState
import tachiyomi.core.common.preference.getAndSet
import tachiyomi.core.common.preference.getEnum
import tachiyomi.core.common.preference.minusAssign
import tachiyomi.core.common.util.system.logcat
import tachiyomi.data.DatabaseHandler
import tachiyomi.data.category.CategoryMapper
import tachiyomi.data.chapter.ChapterMapper
import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.chapter.interactor.DeleteChapters
import tachiyomi.domain.chapter.interactor.UpdateChapter
import tachiyomi.domain.chapter.model.ChapterUpdate
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_COMPLETED
import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.interactor.GetMangaBySource
import tachiyomi.domain.manga.interactor.InsertMergedReference
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaUpdate
import tachiyomi.domain.manga.model.MergedMangaReference
import tachiyomi.domain.source.interactor.InsertFeedSavedSearch
import tachiyomi.domain.source.interactor.InsertSavedSearch
import tachiyomi.domain.source.model.FeedSavedSearch
import tachiyomi.domain.source.model.SavedSearch
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.net.URI import java.net.URI
import java.net.URISyntaxException import java.net.URISyntaxException
object EXHMigrations { object EXHMigrations {
private val handler: DatabaseHandler by injectLazy()
private val sourceManager: SourceManager by injectLazy()
private val getManga: GetManga by injectLazy()
private val getMangaBySource: GetMangaBySource by injectLazy()
private val updateManga: UpdateManga by injectLazy()
private val updateChapter: UpdateChapter by injectLazy()
private val deleteChapters: DeleteChapters by injectLazy()
private val insertMergedReference: InsertMergedReference by injectLazy()
private val insertSavedSearch: InsertSavedSearch by injectLazy()
private val insertFeedSavedSearch: InsertFeedSavedSearch by injectLazy()
/**
* Performs a migration when the application is updated.
*
* @return true if a migration is performed, false otherwise.
*/
fun upgrade(
context: Context,
preferenceStore: PreferenceStore,
basePreferences: BasePreferences,
uiPreferences: UiPreferences,
networkPreferences: NetworkPreferences,
sourcePreferences: SourcePreferences,
securityPreferences: SecurityPreferences,
libraryPreferences: LibraryPreferences,
readerPreferences: ReaderPreferences,
backupPreferences: BackupPreferences,
trackerManager: TrackerManager,
pagePreviewCache: PagePreviewCache,
extensionRepoRepository: ExtensionRepoRepository,
): Boolean {
val lastVersionCode = preferenceStore.getInt("eh_last_version_code", 0)
val oldVersion = lastVersionCode.get()
try {
if (oldVersion < BuildConfig.VERSION_CODE) {
lastVersionCode.set(BuildConfig.VERSION_CODE)
LibraryUpdateJob.setupTask(context)
BackupCreateJob.setupTask(context)
EHentaiUpdateWorker.scheduleBackground(context)
SyncDataJob.setupTask(context)
// Fresh install
if (oldVersion == 0) {
return false
}
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
if (oldVersion under 4) {
updateSourceId(HBROWSE_SOURCE_ID, 6912)
// Migrate BHrowse URLs
val hBrowseManga = runBlocking { getMangaBySource.await(HBROWSE_SOURCE_ID) }
val mangaUpdates = hBrowseManga.map {
MangaUpdate(it.id, url = it.url + "/c00001/")
}
runBlocking {
updateManga.awaitAll(mangaUpdates)
}
}
if (oldVersion under 6) {
updateSourceId(NHentai.otherId, 6907)
}
if (oldVersion under 7) {
val mergedMangas = runBlocking { getMangaBySource.await(MERGED_SOURCE_ID) }
if (mergedMangas.isNotEmpty()) {
val mangaConfigs = mergedMangas.mapNotNull { mergedManga ->
readMangaConfig(mergedManga)?.let { mergedManga to it }
}
if (mangaConfigs.isNotEmpty()) {
val mangaToUpdate = mutableListOf<MangaUpdate>()
val mergedMangaReferences = mutableListOf<MergedMangaReference>()
mangaConfigs.onEach { mergedManga ->
val newFirst = mergedManga.second.children.firstOrNull()?.url?.let {
if (runBlocking { getManga.await(it, MERGED_SOURCE_ID) } != null) return@onEach
mangaToUpdate += MangaUpdate(id = mergedManga.first.id, url = it)
mergedManga.first.copy(url = it)
} ?: mergedManga.first
mergedMangaReferences += MergedMangaReference(
id = -1,
isInfoManga = false,
getChapterUpdates = false,
chapterSortMode = 0,
chapterPriority = 0,
downloadChapters = false,
mergeId = newFirst.id,
mergeUrl = newFirst.url,
mangaId = newFirst.id,
mangaUrl = newFirst.url,
mangaSourceId = MERGED_SOURCE_ID,
)
mergedManga.second.children.distinct().forEachIndexed { index, mangaSource ->
val load = mangaSource.load() ?: return@forEachIndexed
mergedMangaReferences += MergedMangaReference(
id = -1,
isInfoManga = index == 0,
getChapterUpdates = true,
chapterSortMode = 0,
chapterPriority = 0,
downloadChapters = true,
mergeId = newFirst.id,
mergeUrl = newFirst.url,
mangaId = load.manga.id,
mangaUrl = load.manga.url,
mangaSourceId = load.source.id,
)
}
}
runBlocking {
updateManga.awaitAll(mangaToUpdate)
insertMergedReference.awaitAll(mergedMangaReferences)
}
val loadedMangaList = mangaConfigs
.map { it.second.children }
.flatten()
.mapNotNull { it.load() }
.distinct()
val chapters =
runBlocking {
handler.awaitList {
ehQueries.getChaptersByMangaIds(
mergedMangas.map { it.id },
ChapterMapper::mapChapter,
)
}
}
val mergedMangaChapters =
runBlocking {
handler.awaitList {
ehQueries.getChaptersByMangaIds(
loadedMangaList.map { it.manga.id },
ChapterMapper::mapChapter,
)
}
}
val mergedMangaChaptersMatched = mergedMangaChapters.mapNotNull { chapter ->
loadedMangaList.firstOrNull {
it.manga.id == chapter.id
}?.let { it to chapter }
}
val parsedChapters = chapters.filter {
it.read || it.lastPageRead != 0L
}.mapNotNull { chapter -> readUrlConfig(chapter.url)?.let { chapter to it } }
val chaptersToUpdate = mutableListOf<ChapterUpdate>()
parsedChapters.forEach { parsedChapter ->
mergedMangaChaptersMatched.firstOrNull {
it.second.url == parsedChapter.second.url &&
it.first.source.id == parsedChapter.second.source &&
it.first.manga.url == parsedChapter.second.mangaUrl
}?.let {
chaptersToUpdate += ChapterUpdate(
it.second.id,
read = parsedChapter.first.read,
lastPageRead = parsedChapter.first.lastPageRead,
)
}
}
runBlocking {
deleteChapters.await(mergedMangaChapters.map { it.id })
updateChapter.awaitAll(chaptersToUpdate)
}
}
}
}
if (oldVersion under 12) {
// Force MAL log out due to login flow change
trackerManager.myAnimeList.logout()
}
if (oldVersion under 14) {
// Migrate DNS over HTTPS setting
val wasDohEnabled = prefs.getBoolean("enable_doh", false)
if (wasDohEnabled) {
prefs.edit {
putInt(networkPreferences.dohProvider().key(), PREF_DOH_CLOUDFLARE)
remove("enable_doh")
}
}
}
if (oldVersion under 16) {
// Reset rotation to Free after replacing Lock
if (prefs.contains("pref_rotation_type_key")) {
prefs.edit {
putInt("pref_rotation_type_key", 1)
}
}
// Disable update check for Android 5.x users
// if (BuildConfig.INCLUDE_UPDATER && Build.VERSION.SDK_INT under Build.VERSION_CODES.M) {
// UpdaterJob.cancelTask(context)
// }
}
if (oldVersion under 17) {
// Migrate Rotation and Viewer values to default values for viewer_flags
val newOrientation = when (prefs.getInt("pref_rotation_type_key", 1)) {
1 -> ReaderOrientation.FREE.flagValue
2 -> ReaderOrientation.PORTRAIT.flagValue
3 -> ReaderOrientation.LANDSCAPE.flagValue
4 -> ReaderOrientation.LOCKED_PORTRAIT.flagValue
5 -> ReaderOrientation.LOCKED_LANDSCAPE.flagValue
else -> ReaderOrientation.FREE.flagValue
}
// Reading mode flag and prefValue is the same value
val newReadingMode = prefs.getInt("pref_default_viewer_key", 1)
prefs.edit {
putInt("pref_default_orientation_type_key", newOrientation)
remove("pref_rotation_type_key")
putInt("pref_default_reading_mode_key", newReadingMode)
remove("pref_default_viewer_key")
}
// Delete old mangadex trackers
runBlocking {
handler.await { ehQueries.deleteBySyncId(6) }
}
}
if (oldVersion under 18) {
val readerTheme = readerPreferences.readerTheme().get()
if (readerTheme == 4) {
readerPreferences.readerTheme().set(3)
}
val updateInterval = libraryPreferences.autoUpdateInterval().get()
if (updateInterval == 1 || updateInterval == 2) {
libraryPreferences.autoUpdateInterval().set(3)
}
}
if (oldVersion under 20) {
try {
val oldSortingMode = prefs.getInt(libraryPreferences.sortingMode().key(), 0 /* ALPHABETICAL */)
val oldSortingDirection = prefs.getBoolean("library_sorting_ascending", true)
val newSortingMode = when (oldSortingMode) {
0 -> "ALPHABETICAL"
1 -> "LAST_READ"
2 -> "LAST_MANGA_UPDATE"
3 -> "UNREAD_COUNT"
4 -> "TOTAL_CHAPTERS"
6 -> "LATEST_CHAPTER"
7 -> "DRAG_AND_DROP"
8 -> "DATE_ADDED"
9 -> "TAG_LIST"
10 -> "CHAPTER_FETCH_DATE"
else -> "ALPHABETICAL"
}
val newSortingDirection = when (oldSortingDirection) {
true -> "ASCENDING"
else -> "DESCENDING"
}
prefs.edit(commit = true) {
remove(libraryPreferences.sortingMode().key())
remove("library_sorting_ascending")
}
prefs.edit {
putString(libraryPreferences.sortingMode().key(), newSortingMode)
putString("library_sorting_ascending", newSortingDirection)
}
} catch (e: Exception) {
logcat(throwable = e) { "Already done migration" }
}
}
if (oldVersion under 22) {
// Handle removed every 3, 4, 6, and 8 hour library updates
val updateInterval = libraryPreferences.autoUpdateInterval().get()
if (updateInterval in listOf(3, 4, 6, 8)) {
libraryPreferences.autoUpdateInterval().set(12)
}
}
if (oldVersion under 23) {
val oldUpdateOngoingOnly = prefs.getBoolean("pref_update_only_non_completed_key", true)
if (!oldUpdateOngoingOnly) {
libraryPreferences.autoUpdateMangaRestrictions() -= MANGA_NON_COMPLETED
}
}
if (oldVersion under 24) {
try {
sequenceOf(
"fav-sync",
"fav-sync.management",
"fav-sync.lock",
"fav-sync.note",
).map {
File(context.filesDir, it)
}.filter(File::exists).forEach {
if (it.isDirectory) {
it.deleteRecursively()
} else {
it.delete()
}
}
} catch (e: Exception) {
xLogE("Failed to delete old favorites database", e)
}
}
if (oldVersion under 27) {
val oldSecureScreen = prefs.getBoolean("secure_screen", false)
if (oldSecureScreen) {
securityPreferences.secureScreen().set(SecurityPreferences.SecureScreenMode.ALWAYS)
}
if (
DeviceUtil.isMiui &&
basePreferences.extensionInstaller().get() == BasePreferences.ExtensionInstaller
.PACKAGEINSTALLER
) {
basePreferences.extensionInstaller().set(BasePreferences.ExtensionInstaller.LEGACY)
}
}
if (oldVersion under 28) {
if (prefs.getString("pref_display_mode_library", null) == "NO_TITLE_GRID") {
prefs.edit(commit = true) {
putString("pref_display_mode_library", "COVER_ONLY_GRID")
}
}
}
if (oldVersion under 29) {
if (prefs.getString("pref_display_mode_catalogue", null) == "NO_TITLE_GRID") {
prefs.edit(commit = true) {
putString("pref_display_mode_catalogue", "COMPACT_GRID")
}
}
}
if (oldVersion under 31) {
runBlocking {
val savedSearch = prefs.getStringSet("eh_saved_searches", emptySet())?.mapNotNull {
runCatching {
val content = Json.decodeFromString<JsonObject>(it.substringAfter(':'))
SavedSearch(
id = -1,
source = it.substringBefore(':').toLongOrNull()
?: return@runCatching null,
name = content["name"]!!.jsonPrimitive.content,
query = content["query"]!!.jsonPrimitive.contentOrNull?.nullIfBlank(),
filtersJson = Json.encodeToString(content["filters"]!!.jsonArray),
)
}.getOrNull()
}
if (!savedSearch.isNullOrEmpty()) {
insertSavedSearch.awaitAll(savedSearch)
}
val feedSavedSearch = prefs.getStringSet("latest_tab_sources", emptySet())?.map {
FeedSavedSearch(
id = -1,
source = it.toLong(),
savedSearch = null,
global = true,
)
}
if (!feedSavedSearch.isNullOrEmpty()) {
insertFeedSavedSearch.awaitAll(feedSavedSearch)
}
}
prefs.edit(commit = true) {
remove("eh_saved_searches")
remove("latest_tab_sources")
}
}
if (oldVersion under 32) {
val oldReaderTap = prefs.getBoolean("reader_tap", false)
if (!oldReaderTap) {
readerPreferences.navigationModePager().set(5)
readerPreferences.navigationModeWebtoon().set(5)
}
}
if (oldVersion under 38) {
// Handle renamed enum values
val newSortingMode = when (
val oldSortingMode = prefs.getString(libraryPreferences.sortingMode().key(), "ALPHABETICAL")
) {
"LAST_CHECKED" -> "LAST_MANGA_UPDATE"
"UNREAD" -> "UNREAD_COUNT"
"DATE_FETCHED" -> "CHAPTER_FETCH_DATE"
"DRAG_AND_DROP" -> "ALPHABETICAL"
else -> oldSortingMode
}
prefs.edit {
putString(libraryPreferences.sortingMode().key(), newSortingMode)
}
runBlocking {
handler.await(true) {
categoriesQueries.getCategories(CategoryMapper::mapCategory).executeAsList()
.filter { (it.flags and 0b00111100L) == 0b00100000L }
.forEach {
categoriesQueries.update(
categoryId = it.id,
flags = it.flags and 0b00111100L.inv(),
name = null,
order = null,
)
}
}
}
}
if (oldVersion under 39) {
prefs.edit {
val sort = prefs.getString(libraryPreferences.sortingMode().key(), null) ?: return@edit
val direction = prefs.getString("library_sorting_ascending", "ASCENDING")!!
putString(libraryPreferences.sortingMode().key(), "$sort,$direction")
remove("library_sorting_ascending")
}
}
if (oldVersion under 40) {
if (backupPreferences.backupInterval().get() == 0) {
backupPreferences.backupInterval().set(12)
}
}
if (oldVersion under 41) {
val preferences = listOf(
libraryPreferences.filterChapterByRead(),
libraryPreferences.filterChapterByDownloaded(),
libraryPreferences.filterChapterByBookmarked(),
libraryPreferences.sortChapterBySourceOrNumber(),
libraryPreferences.displayChapterByNameOrNumber(),
libraryPreferences.sortChapterByAscendingOrDescending(),
)
prefs.edit {
preferences.forEach { preference ->
val key = preference.key()
val value = prefs.getInt(key, Int.MIN_VALUE)
if (value == Int.MIN_VALUE) return@forEach
remove(key)
putLong(key, value.toLong())
}
}
}
if (oldVersion under 42) {
if (uiPreferences.themeMode().isSet()) {
prefs.edit {
val themeMode = prefs.getString(uiPreferences.themeMode().key(), null) ?: return@edit
putString(uiPreferences.themeMode().key(), themeMode.uppercase())
}
}
}
if (oldVersion under 43) {
if (preferenceStore.getBoolean("start_reading_button").get()) {
libraryPreferences.showContinueReadingButton().set(true)
}
}
if (oldVersion under 44) {
val trackingQueuePref = context.getSharedPreferences("tracking_queue", Context.MODE_PRIVATE)
trackingQueuePref.all.forEach {
val (_, lastChapterRead) = it.value.toString().split(":")
trackingQueuePref.edit {
remove(it.key)
putFloat(it.key, lastChapterRead.toFloat())
}
}
}
if (oldVersion under 45) {
// Force MangaDex log out due to login flow change
trackerManager.mdList.logout()
}
if (oldVersion under 52) {
// Removed background jobs
context.workManager.cancelAllWorkByTag("UpdateChecker")
context.workManager.cancelAllWorkByTag("ExtensionUpdate")
prefs.edit {
remove("automatic_ext_updates")
}
val prefKeys = listOf(
"pref_filter_library_downloaded",
"pref_filter_library_unread",
"pref_filter_library_started",
"pref_filter_library_bookmarked",
"pref_filter_library_completed",
"pref_filter_library_lewd",
) + trackerManager.trackers.map { "pref_filter_library_tracked_${it.id}" }
prefKeys.forEach { key ->
val pref = preferenceStore.getInt(key, 0)
prefs.edit {
remove(key)
val newValue = when (pref.get()) {
1 -> TriState.ENABLED_IS
2 -> TriState.ENABLED_NOT
else -> TriState.DISABLED
}
preferenceStore.getEnum("${key}_v2", TriState.DISABLED).set(newValue)
}
}
}
// if (oldVersion under 53) {
// // This was accidentally visible from the reader settings sheet, but should always
// // be disabled in release builds.
// if (isReleaseBuildType) {
// readerPreferences.longStripSplitWebtoon().set(false)
// }
// }
if (oldVersion under 56) {
val pref = libraryPreferences.autoUpdateDeviceRestrictions()
if (pref.isSet() && "battery_not_low" in pref.get()) {
pref.getAndSet { it - "battery_not_low" }
}
}
if (oldVersion under 57) {
val pref = preferenceStore.getInt("relative_time", 7)
if (pref.get() == 0) {
uiPreferences.relativeTime().set(false)
}
}
if (oldVersion under 58) {
pagePreviewCache.clear()
File(context.cacheDir, PagePreviewCache.PARAMETER_CACHE_DIRECTORY).listFiles()?.forEach {
if (it.name == "journal" || it.name.startsWith("journal.")) {
return@forEach
}
try {
it.delete()
} catch (e: Exception) {
logcat(LogPriority.WARN, e) { "Failed to remove file from cache" }
}
}
}
if (oldVersion under 59) {
val prefsToReplace = listOf(
"pref_download_only",
"incognito_mode",
"last_catalogue_source",
"trusted_signatures",
"last_app_closed",
"library_update_last_timestamp",
"library_unseen_updates_count",
"last_used_category",
"last_app_check",
"last_ext_check",
"last_version_code",
"skip_pre_migration",
"eh_auto_update_stats",
"storage_dir",
)
replacePreferences(
preferenceStore = preferenceStore,
filterPredicate = { it.key in prefsToReplace },
newKey = { Preference.appStateKey(it) },
)
val privatePrefsToReplace = listOf(
"sql_password",
"encrypt_database",
"cbz_password",
"password_protect_downloads",
"eh_ipb_member_id",
"enable_exhentai",
"eh_ipb_member_id",
"eh_ipb_pass_hash",
"eh_igneous",
"eh_ehSettingsProfile",
"eh_exhSettingsProfile",
"eh_settingsKey",
"eh_sessionCookie",
"eh_hathPerksCookie",
)
replacePreferences(
preferenceStore = preferenceStore,
filterPredicate = { it.key in privatePrefsToReplace },
newKey = { Preference.privateKey(it) },
)
// Deleting old download cache index files, but might as well clear it all out
context.cacheDir.deleteRecursively()
}
if (oldVersion under 60) {
sourcePreferences.extensionRepos().getAndSet {
it.map { "https://raw.githubusercontent.com/$it/repo" }.toSet()
}
replacePreferences(
preferenceStore = preferenceStore,
filterPredicate = { it.key.startsWith("pref_mangasync_") || it.key.startsWith("track_token_") },
newKey = { Preference.privateKey(it) },
)
prefs.edit {
remove(Preference.appStateKey("trusted_signatures"))
}
}
if (oldVersion under 66) {
val cacheImagesToDisk = prefs.getBoolean("cache_archive_manga_on_disk", false)
if (cacheImagesToDisk) {
readerPreferences.archiveReaderMode().set(ReaderPreferences.ArchiveReaderMode.CACHE_TO_DISK)
}
}
if (oldVersion under 66) {
if (prefs.getBoolean(Preference.privateKey("encrypt_database"), false)) {
context.toast(
"Restart the app to load your encrypted library",
Toast.LENGTH_LONG
)
}
val appStatePrefsToReplace = listOf(
"__PRIVATE_sql_password",
"__PRIVATE_encrypt_database",
"__PRIVATE_cbz_password",
)
replacePreferences(
preferenceStore = preferenceStore,
filterPredicate = { it.key in appStatePrefsToReplace },
newKey = { Preference.appStateKey(it.replace("__PRIVATE_", "").trim()) },
)
}
if (oldVersion under 67) {
runBlocking {
for ((index, source) in sourcePreferences.extensionRepos().get().withIndex()) {
try {
extensionRepoRepository.upsertRepo(
source,
"Repo #${index + 1}",
null,
source,
"NOFINGERPRINT-${index + 1}",
)
} catch (e: SaveExtensionRepoException) {
logcat(LogPriority.ERROR, e) { "Error Migrating Extension Repo with baseUrl: $source" }
}
}
sourcePreferences.extensionRepos().delete()
}
}
// if (oldVersion under 1) { } (1 is current release version)
// do stuff here when releasing changed crap
return true
}
} catch (e: Exception) {
xLogE("Failed to migrate app from $oldVersion -> ${BuildConfig.VERSION_CODE}!", e)
}
return false
}
fun migrateBackupEntry(manga: Manga): Manga { fun migrateBackupEntry(manga: Manga): Manga {
var newManga = manga var newManga = manga
@ -769,102 +61,4 @@ object EXHMigrations {
orig orig
} }
} }
@Serializable
private data class UrlConfig(
@SerialName("s")
val source: Long,
@SerialName("u")
val url: String,
@SerialName("m")
val mangaUrl: String,
)
@Serializable
private data class MangaConfig(
@SerialName("c")
val children: List<MangaSource>,
) {
companion object {
fun readFromUrl(url: String): MangaConfig? {
return try {
Json.decodeFromString(url)
} catch (e: Exception) {
null
}
}
}
}
private fun readMangaConfig(manga: Manga): MangaConfig? {
return MangaConfig.readFromUrl(manga.url)
}
@Serializable
private data class MangaSource(
@SerialName("s")
val source: Long,
@SerialName("u")
val url: String,
) {
fun load(): LoadedMangaSource? {
val manga = runBlocking { getManga.await(url, source) } ?: return null
val source = sourceManager.getOrStub(source)
return LoadedMangaSource(source, manga)
}
}
private fun readUrlConfig(url: String): UrlConfig? {
return try {
Json.decodeFromString(url)
} catch (e: Exception) {
null
}
}
private data class LoadedMangaSource(val source: Source, val manga: Manga)
private fun updateSourceId(newId: Long, oldId: Long) {
runBlocking {
handler.await { ehQueries.migrateSource(newId, oldId) }
}
}
@Suppress("UNCHECKED_CAST")
private fun replacePreferences(
preferenceStore: PreferenceStore,
filterPredicate: (Map.Entry<String, Any?>) -> Boolean,
newKey: (String) -> String,
) {
preferenceStore.getAll()
.filter(filterPredicate)
.forEach { (key, value) ->
when (value) {
is Int -> {
preferenceStore.getInt(newKey(key)).set(value)
preferenceStore.getInt(key).delete()
}
is Long -> {
preferenceStore.getLong(newKey(key)).set(value)
preferenceStore.getLong(key).delete()
}
is Float -> {
preferenceStore.getFloat(newKey(key)).set(value)
preferenceStore.getFloat(key).delete()
}
is String -> {
preferenceStore.getString(newKey(key)).set(value)
preferenceStore.getString(key).delete()
}
is Boolean -> {
preferenceStore.getBoolean(newKey(key)).set(value)
preferenceStore.getBoolean(key).delete()
}
is Set<*> -> (value as? Set<String>)?.let {
preferenceStore.getStringSet(newKey(key)).set(value)
preferenceStore.getStringSet(key).delete()
}
}
}
}
} }

View File

@ -1,23 +1,15 @@
package exh.debug package exh.debug
import android.app.Application import android.app.Application
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.toSManga import eu.kanade.domain.manga.model.toSManga
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.data.backup.models.Backup import eu.kanade.tachiyomi.data.backup.models.Backup
import eu.kanade.tachiyomi.data.cache.PagePreviewCache
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.sync.SyncDataJob import eu.kanade.tachiyomi.data.sync.SyncDataJob
import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.network.NetworkPreferences
import eu.kanade.tachiyomi.source.AndroidSourceManager import eu.kanade.tachiyomi.source.AndroidSourceManager
import eu.kanade.tachiyomi.source.online.all.NHentai import eu.kanade.tachiyomi.source.online.all.NHentai
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.util.system.workManager import eu.kanade.tachiyomi.util.system.workManager
import exh.EXHMigrations
import exh.eh.EHentaiThrottleManager import exh.eh.EHentaiThrottleManager
import exh.eh.EHentaiUpdateWorker import exh.eh.EHentaiUpdateWorker
import exh.metadata.metadata.EHentaiSearchMetadata import exh.metadata.metadata.EHentaiSearchMetadata
@ -27,11 +19,9 @@ import exh.source.nHentaiSourceIds
import exh.util.jobScheduler import exh.util.jobScheduler
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.serialization.protobuf.schema.ProtoBufSchemaGenerator import kotlinx.serialization.protobuf.schema.ProtoBufSchemaGenerator
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository import mihon.core.migration.Migrator
import tachiyomi.core.common.preference.PreferenceStore import mihon.core.migration.migrations.migrations
import tachiyomi.data.DatabaseHandler import tachiyomi.data.DatabaseHandler
import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.interactor.GetAllManga import tachiyomi.domain.manga.interactor.GetAllManga
import tachiyomi.domain.manga.interactor.GetExhFavoriteMangaWithMetadata import tachiyomi.domain.manga.interactor.GetExhFavoriteMangaWithMetadata
import tachiyomi.domain.manga.interactor.GetFavorites import tachiyomi.domain.manga.interactor.GetFavorites
@ -48,16 +38,6 @@ import java.util.UUID
object DebugFunctions { object DebugFunctions {
private val app: Application by injectLazy() private val app: Application by injectLazy()
private val handler: DatabaseHandler by injectLazy() private val handler: DatabaseHandler by injectLazy()
private val prefsStore: PreferenceStore by injectLazy()
private val basePrefs: BasePreferences by injectLazy()
private val uiPrefs: UiPreferences by injectLazy()
private val networkPrefs: NetworkPreferences by injectLazy()
private val sourcePrefs: SourcePreferences by injectLazy()
private val securityPrefs: SecurityPreferences by injectLazy()
private val libraryPrefs: LibraryPreferences by injectLazy()
private val readerPrefs: ReaderPreferences by injectLazy()
private val backupPrefs: BackupPreferences by injectLazy()
private val trackerManager: TrackerManager by injectLazy()
private val sourceManager: SourceManager by injectLazy() private val sourceManager: SourceManager by injectLazy()
private val updateManga: UpdateManga by injectLazy() private val updateManga: UpdateManga by injectLazy()
private val getFavorites: GetFavorites by injectLazy() private val getFavorites: GetFavorites by injectLazy()
@ -66,46 +46,22 @@ object DebugFunctions {
private val getExhFavoriteMangaWithMetadata: GetExhFavoriteMangaWithMetadata by injectLazy() private val getExhFavoriteMangaWithMetadata: GetExhFavoriteMangaWithMetadata by injectLazy()
private val getSearchMetadata: GetSearchMetadata by injectLazy() private val getSearchMetadata: GetSearchMetadata by injectLazy()
private val getAllManga: GetAllManga by injectLazy() private val getAllManga: GetAllManga by injectLazy()
private val pagePreviewCache: PagePreviewCache by injectLazy()
private val extensionRepoRepository: ExtensionRepoRepository by injectLazy()
fun forceUpgradeMigration() { fun forceUpgradeMigration() {
val lastVersionCode = prefsStore.getInt("eh_last_version_code", 0) Migrator.migrate(
lastVersionCode.set(1) old = 1,
EXHMigrations.upgrade( new = BuildConfig.VERSION_CODE,
context = app, migrations = migrations,
preferenceStore = prefsStore, onMigrationComplete = {}
basePreferences = basePrefs,
uiPreferences = uiPrefs,
networkPreferences = networkPrefs,
sourcePreferences = sourcePrefs,
securityPreferences = securityPrefs,
libraryPreferences = libraryPrefs,
readerPreferences = readerPrefs,
backupPreferences = backupPrefs,
trackerManager = trackerManager,
pagePreviewCache = pagePreviewCache,
extensionRepoRepository = extensionRepoRepository,
) )
} }
fun forceSetupJobs() { fun forceSetupJobs() {
val lastVersionCode = prefsStore.getInt("eh_last_version_code", 0) Migrator.migrate(
lastVersionCode.set(0) old = 0,
EXHMigrations.upgrade( new = BuildConfig.VERSION_CODE,
context = app, migrations = migrations,
preferenceStore = prefsStore, onMigrationComplete = {}
basePreferences = basePrefs,
uiPreferences = uiPrefs,
networkPreferences = networkPrefs,
sourcePreferences = sourcePrefs,
securityPreferences = securityPrefs,
libraryPreferences = libraryPrefs,
readerPreferences = readerPrefs,
backupPreferences = backupPrefs,
trackerManager = trackerManager,
pagePreviewCache = pagePreviewCache,
extensionRepoRepository = extensionRepoRepository,
) )
} }

View File

@ -0,0 +1,53 @@
package mihon.core.migration
import kotlinx.coroutines.runBlocking
import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.data.DatabaseHandler
object MigrateUtils {
fun updateSourceId(migrationContext: MigrationContext, newId: Long, oldId: Long) {
val handler = migrationContext.get<DatabaseHandler>() ?: return
runBlocking {
handler.await { ehQueries.migrateSource(newId, oldId) }
}
}
@Suppress("UNCHECKED_CAST")
fun replacePreferences(
preferenceStore: PreferenceStore,
filterPredicate: (Map.Entry<String, Any?>) -> Boolean,
newKey: (String) -> String,
) {
preferenceStore.getAll()
.filter(filterPredicate)
.forEach { (key, value) ->
when (value) {
is Int -> {
preferenceStore.getInt(newKey(key)).set(value)
preferenceStore.getInt(key).delete()
}
is Long -> {
preferenceStore.getLong(newKey(key)).set(value)
preferenceStore.getLong(key).delete()
}
is Float -> {
preferenceStore.getFloat(newKey(key)).set(value)
preferenceStore.getFloat(key).delete()
}
is String -> {
preferenceStore.getString(newKey(key)).set(value)
preferenceStore.getString(key).delete()
}
is Boolean -> {
preferenceStore.getBoolean(newKey(key)).set(value)
preferenceStore.getBoolean(key).delete()
}
is Set<*> -> (value as? Set<String>)?.let {
preferenceStore.getStringSet(newKey(key)).set(value)
preferenceStore.getStringSet(key).delete()
}
}
}
}
}

View File

@ -0,0 +1,19 @@
package mihon.core.migration
interface Migration {
val version: Float
suspend operator fun invoke(migrationContext: MigrationContext): Boolean
companion object {
const val ALWAYS = -1f
fun of(version: Float, action: suspend (MigrationContext) -> Boolean): Migration = object : Migration {
override val version: Float = version
override suspend operator fun invoke(migrationContext: MigrationContext): Boolean {
return action(migrationContext)
}
}
}
}

View File

@ -0,0 +1,10 @@
package mihon.core.migration
import uy.kohesive.injekt.Injekt
class MigrationContext {
inline fun <reified T> get(): T? {
return Injekt.getInstanceOrNull(T::class.java)
}
}

View File

@ -0,0 +1,53 @@
package mihon.core.migration
import kotlinx.coroutines.runBlocking
import tachiyomi.core.common.util.system.logcat
object Migrator {
@SuppressWarnings("ReturnCount")
fun migrate(
old: Int,
new: Int,
migrations: List<Migration>,
dryrun: Boolean = false,
onMigrationComplete: () -> Unit
): Boolean {
val migrationContext = MigrationContext()
if (old == 0) {
return migrationContext.migrate(
migrations = migrations.filter { it.isAlways() },
dryrun = dryrun
)
.also { onMigrationComplete() }
}
if (old >= new) {
return false
}
return migrationContext.migrate(
migrations = migrations.filter { it.isAlways() || it.version.toInt() in (old + 1)..new },
dryrun = dryrun
)
.also { onMigrationComplete() }
}
private fun Migration.isAlways() = version == Migration.ALWAYS
@SuppressWarnings("MaxLineLength")
private fun MigrationContext.migrate(migrations: List<Migration>, dryrun: Boolean): Boolean {
return migrations.sortedBy { it.version }
.map { migration ->
if (!dryrun) {
logcat { "Running migration: { name = ${migration::class.simpleName}, version = ${migration.version} }" }
runBlocking { migration(this@migrate) }
} else {
logcat { "(Dry-run) Running migration: { name = ${migration::class.simpleName}, version = ${migration.version} }" }
true
}
}
.reduce { acc, b -> acc || b }
}
}

View File

@ -0,0 +1,19 @@
package mihon.core.migration.migrations
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.domain.backup.service.BackupPreferences
class AlwaysBackupMigration : Migration {
override val version: Float = 40f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val backupPreferences = migrationContext.get<BackupPreferences>() ?: return@withIOContext false
if (backupPreferences.backupInterval().get() == 0) {
backupPreferences.backupInterval().set(12)
}
return@withIOContext true
}
}

View File

@ -0,0 +1,24 @@
package mihon.core.migration.migrations
import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.util.system.DeviceUtil
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.util.lang.withIOContext
class ChangeMiuiExtensionInstallerMigration : Migration {
override val version: Float = 27f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val basePreferences = migrationContext.get<BasePreferences>() ?: return@withIOContext false
if (
DeviceUtil.isMiui &&
basePreferences.extensionInstaller().get() == BasePreferences.ExtensionInstaller
.PACKAGEINSTALLER
) {
basePreferences.extensionInstaller().set(BasePreferences.ExtensionInstaller.LEGACY)
}
return@withIOContext true
}
}

View File

@ -0,0 +1,27 @@
package mihon.core.migration.migrations
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.tachiyomi.App
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.util.lang.withIOContext
class ChangeThemeModeToUppercaseMigration : Migration {
override val version: Float = 42f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val context = migrationContext.get<App>() ?: return@withIOContext false
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val uiPreferences = migrationContext.get<UiPreferences>() ?: return@withIOContext false
if (uiPreferences.themeMode().isSet()) {
prefs.edit {
val themeMode = prefs.getString(uiPreferences.themeMode().key(), null) ?: return@edit
putString(uiPreferences.themeMode().key(), themeMode.uppercase())
}
}
return@withIOContext true
}
}

View File

@ -0,0 +1,26 @@
package mihon.core.migration.migrations
import android.content.Context
import androidx.core.content.edit
import eu.kanade.tachiyomi.App
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.util.lang.withIOContext
class ChangeTrackingQueueTypeMigration : Migration {
override val version: Float = 44f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val context = migrationContext.get<App>() ?: return@withIOContext false
val trackingQueuePref = context.getSharedPreferences("tracking_queue", Context.MODE_PRIVATE)
trackingQueuePref.all.forEach {
val (_, lastChapterRead) = it.value.toString().split(":")
trackingQueuePref.edit {
remove(it.key)
putFloat(it.key, lastChapterRead.toFloat())
}
}
return@withIOContext true
}
}

View File

@ -0,0 +1,33 @@
package mihon.core.migration.migrations
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.data.cache.PagePreviewCache
import logcat.LogPriority
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.system.logcat
import java.io.File
class ClearBrokenPagePreviewCacheMigration : Migration {
override val version: Float = 58f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val context = migrationContext.get<App>() ?: return@withIOContext false
val pagePreviewCache = migrationContext.get<PagePreviewCache>() ?: return@withIOContext false
pagePreviewCache.clear()
File(context.cacheDir, PagePreviewCache.PARAMETER_CACHE_DIRECTORY).listFiles()?.forEach {
if (it.name == "journal" || it.name.startsWith("journal.")) {
return@forEach
}
try {
it.delete()
} catch (e: Exception) {
logcat(LogPriority.WARN, e) { "Failed to remove file from cache" }
}
}
return@withIOContext true
}
}

View File

@ -0,0 +1,28 @@
package mihon.core.migration.migrations
import eu.kanade.domain.manga.interactor.UpdateManga
import exh.source.HBROWSE_SOURCE_ID
import mihon.core.migration.MigrateUtils
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.domain.manga.interactor.GetMangaBySource
import tachiyomi.domain.manga.model.MangaUpdate
class DelegateHBrowseMigration : Migration {
override val version: Float = 4f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val getMangaBySource = migrationContext.get<GetMangaBySource>() ?: return@withIOContext false
val updateManga = migrationContext.get<UpdateManga>() ?: return@withIOContext false
MigrateUtils.updateSourceId(migrationContext, HBROWSE_SOURCE_ID, 6912)
// Migrate BHrowse URLs
val hBrowseManga = getMangaBySource.await(HBROWSE_SOURCE_ID)
val mangaUpdates = hBrowseManga.map {
MangaUpdate(it.id, url = it.url + "/c00001/")
}
updateManga.awaitAll(mangaUpdates)
return@withIOContext true
}
}

View File

@ -0,0 +1,17 @@
package mihon.core.migration.migrations
import eu.kanade.tachiyomi.source.online.all.NHentai
import mihon.core.migration.MigrateUtils
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.util.lang.withIOContext
class DelegateNHentaiMigration : Migration {
override val version: Float = 6f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
MigrateUtils.updateSourceId(migrationContext, NHentai.otherId, 6907)
return@withIOContext true
}
}

View File

@ -0,0 +1,36 @@
package mihon.core.migration.migrations
import eu.kanade.tachiyomi.App
import exh.log.xLogE
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.util.lang.withIOContext
import java.io.File
class DeleteOldEhFavoritesDatabaseMigration : Migration {
override val version: Float = 24f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val context = migrationContext.get<App>() ?: return@withIOContext false
try {
sequenceOf(
"fav-sync",
"fav-sync.management",
"fav-sync.lock",
"fav-sync.note",
).map {
File(context.filesDir, it)
}.filter(File::exists).forEach {
if (it.isDirectory) {
it.deleteRecursively()
} else {
it.delete()
}
}
} catch (e: Exception) {
xLogE("Failed to delete old favorites database", e)
}
return@withIOContext true
}
}

View File

@ -0,0 +1,17 @@
package mihon.core.migration.migrations
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.data.DatabaseHandler
class DeleteOldMangaDexTracksMigration : Migration {
override val version: Float = 17f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val handler = migrationContext.get<DatabaseHandler>() ?: return@withIOContext false
// Delete old mangadex trackers
handler.await { ehQueries.deleteBySyncId(6) }
return@withIOContext true
}
}

View File

@ -0,0 +1,17 @@
package mihon.core.migration.migrations
import eu.kanade.tachiyomi.data.track.TrackerManager
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.util.lang.withIOContext
class LogoutFromMALMigration : Migration {
override val version: Float = 12f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
// Force MAL log out due to login flow change
migrationContext.get<TrackerManager>()?.myAnimeList?.logout()
return@withIOContext true
}
}

View File

@ -0,0 +1,17 @@
package mihon.core.migration.migrations
import eu.kanade.tachiyomi.data.track.TrackerManager
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.util.lang.withIOContext
class LogoutFromMangaDexMigration : Migration {
override val version: Float = 45f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
// Force MangaDex log out due to login flow change
migrationContext.get<TrackerManager>()?.mdList?.logout()
return@withIOContext true
}
}

View File

@ -0,0 +1,191 @@
package mihon.core.migration.migrations
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.tachiyomi.source.Source
import exh.source.MERGED_SOURCE_ID
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.data.DatabaseHandler
import tachiyomi.data.chapter.ChapterMapper
import tachiyomi.domain.chapter.interactor.DeleteChapters
import tachiyomi.domain.chapter.interactor.UpdateChapter
import tachiyomi.domain.chapter.model.ChapterUpdate
import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.interactor.GetMangaBySource
import tachiyomi.domain.manga.interactor.InsertMergedReference
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaUpdate
import tachiyomi.domain.manga.model.MergedMangaReference
import tachiyomi.domain.source.service.SourceManager
class MergedMangaRewriteMigration : Migration {
override val version: Float = 7f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val handler = migrationContext.get<DatabaseHandler>() ?: return@withIOContext false
val getMangaBySource = migrationContext.get<GetMangaBySource>() ?: return@withIOContext false
val getManga = migrationContext.get<GetManga>() ?: return@withIOContext false
val updateManga = migrationContext.get<UpdateManga>() ?: return@withIOContext false
val insertMergedReference = migrationContext.get<InsertMergedReference>() ?: return@withIOContext false
val sourceManager = migrationContext.get<SourceManager>() ?: return@withIOContext false
val deleteChapters = migrationContext.get<DeleteChapters>() ?: return@withIOContext false
val updateChapter = migrationContext.get<UpdateChapter>() ?: return@withIOContext false
val mergedMangas = getMangaBySource.await(MERGED_SOURCE_ID)
if (mergedMangas.isNotEmpty()) {
val mangaConfigs = mergedMangas.mapNotNull { mergedManga ->
readMangaConfig(mergedManga)?.let { mergedManga to it }
}
if (mangaConfigs.isNotEmpty()) {
val mangaToUpdate = mutableListOf<MangaUpdate>()
val mergedMangaReferences = mutableListOf<MergedMangaReference>()
mangaConfigs.onEach { mergedManga ->
val newFirst = mergedManga.second.children.firstOrNull()?.url?.let {
if (getManga.await(it, MERGED_SOURCE_ID) != null) return@onEach
mangaToUpdate += MangaUpdate(id = mergedManga.first.id, url = it)
mergedManga.first.copy(url = it)
} ?: mergedManga.first
mergedMangaReferences += MergedMangaReference(
id = -1,
isInfoManga = false,
getChapterUpdates = false,
chapterSortMode = 0,
chapterPriority = 0,
downloadChapters = false,
mergeId = newFirst.id,
mergeUrl = newFirst.url,
mangaId = newFirst.id,
mangaUrl = newFirst.url,
mangaSourceId = MERGED_SOURCE_ID,
)
mergedManga.second.children.distinct().forEachIndexed { index, mangaSource ->
val load = mangaSource.load(getManga, sourceManager) ?: return@forEachIndexed
mergedMangaReferences += MergedMangaReference(
id = -1,
isInfoManga = index == 0,
getChapterUpdates = true,
chapterSortMode = 0,
chapterPriority = 0,
downloadChapters = true,
mergeId = newFirst.id,
mergeUrl = newFirst.url,
mangaId = load.manga.id,
mangaUrl = load.manga.url,
mangaSourceId = load.source.id,
)
}
}
updateManga.awaitAll(mangaToUpdate)
insertMergedReference.awaitAll(mergedMangaReferences)
val loadedMangaList = mangaConfigs
.map { it.second.children }
.flatten()
.mapNotNull { it.load(getManga, sourceManager) }
.distinct()
val chapters =
handler.awaitList {
ehQueries.getChaptersByMangaIds(
mergedMangas.map { it.id },
ChapterMapper::mapChapter,
)
}
val mergedMangaChapters =
handler.awaitList {
ehQueries.getChaptersByMangaIds(
loadedMangaList.map { it.manga.id },
ChapterMapper::mapChapter,
)
}
val mergedMangaChaptersMatched = mergedMangaChapters.mapNotNull { chapter ->
loadedMangaList.firstOrNull {
it.manga.id == chapter.id
}?.let { it to chapter }
}
val parsedChapters = chapters.filter {
it.read || it.lastPageRead != 0L
}.mapNotNull { chapter -> readUrlConfig(chapter.url)?.let { chapter to it } }
val chaptersToUpdate = mutableListOf<ChapterUpdate>()
parsedChapters.forEach { parsedChapter ->
mergedMangaChaptersMatched.firstOrNull {
it.second.url == parsedChapter.second.url &&
it.first.source.id == parsedChapter.second.source &&
it.first.manga.url == parsedChapter.second.mangaUrl
}?.let {
chaptersToUpdate += ChapterUpdate(
it.second.id,
read = parsedChapter.first.read,
lastPageRead = parsedChapter.first.lastPageRead,
)
}
}
deleteChapters.await(mergedMangaChapters.map { it.id })
updateChapter.awaitAll(chaptersToUpdate)
}
}
return@withIOContext true
}
@Serializable
private data class UrlConfig(
@SerialName("s")
val source: Long,
@SerialName("u")
val url: String,
@SerialName("m")
val mangaUrl: String,
)
@Serializable
private data class MangaConfig(
@SerialName("c")
val children: List<MangaSource>,
) {
companion object {
fun readFromUrl(url: String): MangaConfig? {
return try {
Json.decodeFromString(url)
} catch (e: Exception) {
null
}
}
}
}
private fun readMangaConfig(manga: Manga): MangaConfig? {
return MangaConfig.readFromUrl(manga.url)
}
@Serializable
private data class MangaSource(
@SerialName("s")
val source: Long,
@SerialName("u")
val url: String,
) {
suspend fun load(getManga: GetManga, sourceManager: SourceManager): LoadedMangaSource? {
val manga = getManga.await(url, source) ?: return null
val source = sourceManager.getOrStub(source)
return LoadedMangaSource(source, manga)
}
}
private fun readUrlConfig(url: String): UrlConfig? {
return try {
Json.decodeFromString(url)
} catch (e: Exception) {
null
}
}
private data class LoadedMangaSource(val source: Source, val manga: Manga)
}

View File

@ -0,0 +1,48 @@
package mihon.core.migration.migrations
import mihon.core.migration.Migration
val migrations: List<Migration>
get() = listOf(
SetupBackupCreateMigration(),
SetupLibraryUpdateMigration(),
SetupEHentaiUpdateMigration(),
SetupSyncDataMigration(),
DelegateHBrowseMigration(),
DelegateNHentaiMigration(),
MergedMangaRewriteMigration(),
LogoutFromMALMigration(),
MoveDOHSettingMigration(),
ResetRotationSettingMigration(),
ResetReaderSettingsMigration(),
DeleteOldMangaDexTracksMigration(),
RemoveOldReaderThemeMigration(),
RemoveShorterLibraryUpdatesMigration(),
MoveLibrarySortingSettingsMigration(),
RemoveShortLibraryUpdatesMigration(),
MoveLibraryNonCompleteSettingMigration(),
DeleteOldEhFavoritesDatabaseMigration(),
MoveSecureScreenSettingMigration(),
ChangeMiuiExtensionInstallerMigration(),
MoveCoverOnlyGridSettingMigration(),
MoveCatalogueCoverOnlyGridSettingMigration(),
MoveLatestToFeedMigration(),
MoveReaderTapSettingMigration(),
MoveSortingModeSettingsMigration(),
MoveSortingModeSettingMigration(),
AlwaysBackupMigration(),
ResetFilterAndSortSettingsMigration(),
ChangeThemeModeToUppercaseMigration(),
MoveReadingButtonSettingMigration(),
ChangeTrackingQueueTypeMigration(),
LogoutFromMangaDexMigration(),
RemoveUpdateCheckerJobsMigration(),
RemoveBatteryNotLowRestrictionMigration(),
MoveRelativeTimeSettingMigration(),
ClearBrokenPagePreviewCacheMigration(),
MoveSettingsToPrivateOrAppStateMigration(),
MoveExtensionRepoSettingsMigration(),
MoveCacheToDiskSettingMigration(),
MoveEncryptionSettingsToAppStateMigration(),
TrustExtensionRepositoryMigration(),
)

View File

@ -0,0 +1,24 @@
package mihon.core.migration.migrations
import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.util.lang.withIOContext
class MoveCacheToDiskSettingMigration : Migration {
override val version: Float = 66f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val context = migrationContext.get<App>() ?: return@withIOContext false
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val readerPreferences = migrationContext.get<ReaderPreferences>() ?: return@withIOContext false
val cacheImagesToDisk = prefs.getBoolean("cache_archive_manga_on_disk", false)
if (cacheImagesToDisk) {
readerPreferences.archiveReaderMode().set(ReaderPreferences.ArchiveReaderMode.CACHE_TO_DISK)
}
return@withIOContext true
}
}

View File

@ -0,0 +1,24 @@
package mihon.core.migration.migrations
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.App
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.util.lang.withIOContext
class MoveCatalogueCoverOnlyGridSettingMigration : Migration {
override val version: Float = 29f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val context = migrationContext.get<App>() ?: return@withIOContext false
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
if (prefs.getString("pref_display_mode_catalogue", null) == "NO_TITLE_GRID") {
prefs.edit(commit = true) {
putString("pref_display_mode_catalogue", "COMPACT_GRID")
}
}
return@withIOContext true
}
}

View File

@ -0,0 +1,24 @@
package mihon.core.migration.migrations
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.App
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.util.lang.withIOContext
class MoveCoverOnlyGridSettingMigration : Migration {
override val version: Float = 28f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val context = migrationContext.get<App>() ?: return@withIOContext false
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
if (prefs.getString("pref_display_mode_library", null) == "NO_TITLE_GRID") {
prefs.edit(commit = true) {
putString("pref_display_mode_library", "COVER_ONLY_GRID")
}
}
return@withIOContext true
}
}

View File

@ -0,0 +1,30 @@
package mihon.core.migration.migrations
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.network.NetworkPreferences
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.util.lang.withIOContext
class MoveDOHSettingMigration : Migration {
override val version: Float = 14f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val context = migrationContext.get<App>() ?: return@withIOContext false
val networkPreferences = migrationContext.get<NetworkPreferences>() ?: return@withIOContext false
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
// Migrate DNS over HTTPS setting
val wasDohEnabled = prefs.getBoolean("enable_doh", false)
if (wasDohEnabled) {
prefs.edit {
putInt(networkPreferences.dohProvider().key(), PREF_DOH_CLOUDFLARE)
remove("enable_doh")
}
}
return@withIOContext true
}
}

View File

@ -0,0 +1,45 @@
package mihon.core.migration.migrations
import android.widget.Toast
import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.util.system.toast
import mihon.core.migration.MigrateUtils
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.lang.withUIContext
class MoveEncryptionSettingsToAppStateMigration : Migration {
override val version: Float = 66f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val context = migrationContext.get<App>() ?: return@withIOContext false
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val preferenceStore = migrationContext.get<PreferenceStore>() ?: return@withIOContext false
if (prefs.getBoolean(Preference.privateKey("encrypt_database"), false)) {
withUIContext {
context.toast(
"Restart the app to load your encrypted library",
Toast.LENGTH_LONG
)
}
}
val appStatePrefsToReplace = listOf(
"__PRIVATE_sql_password",
"__PRIVATE_encrypt_database",
"__PRIVATE_cbz_password",
)
MigrateUtils.replacePreferences(
preferenceStore = preferenceStore,
filterPredicate = { it.key in appStatePrefsToReplace },
newKey = { Preference.appStateKey(it.replace("__PRIVATE_", "").trim()) },
)
return@withIOContext true
}
}

View File

@ -0,0 +1,37 @@
package mihon.core.migration.migrations
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.App
import mihon.core.migration.MigrateUtils
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.core.common.preference.getAndSet
import tachiyomi.core.common.util.lang.withIOContext
class MoveExtensionRepoSettingsMigration : Migration {
override val version: Float = 60f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val context = migrationContext.get<App>() ?: return@withIOContext false
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val preferenceStore = migrationContext.get<PreferenceStore>() ?: return@withIOContext false
val sourcePreferences = migrationContext.get<SourcePreferences>() ?: return@withIOContext false
sourcePreferences.extensionRepos().getAndSet {
it.map { "https://raw.githubusercontent.com/$it/repo" }.toSet()
}
MigrateUtils.replacePreferences(
preferenceStore = preferenceStore,
filterPredicate = { it.key.startsWith("pref_mangasync_") || it.key.startsWith("track_token_") },
newKey = { Preference.privateKey(it) },
)
prefs.edit {
remove(Preference.appStateKey("trusted_signatures"))
}
return@withIOContext true
}
}

View File

@ -0,0 +1,63 @@
package mihon.core.migration.migrations
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.App
import exh.util.nullIfBlank
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonPrimitive
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.domain.source.interactor.InsertFeedSavedSearch
import tachiyomi.domain.source.interactor.InsertSavedSearch
import tachiyomi.domain.source.model.FeedSavedSearch
import tachiyomi.domain.source.model.SavedSearch
class MoveLatestToFeedMigration : Migration {
override val version: Float = 31f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val context = migrationContext.get<App>() ?: return@withIOContext false
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val insertSavedSearch = migrationContext.get<InsertSavedSearch>() ?: return@withIOContext false
val insertFeedSavedSearch = migrationContext.get<InsertFeedSavedSearch>() ?: return@withIOContext false
val savedSearch = prefs.getStringSet("eh_saved_searches", emptySet())?.mapNotNull {
runCatching {
val content = Json.decodeFromString<JsonObject>(it.substringAfter(':'))
SavedSearch(
id = -1,
source = it.substringBefore(':').toLongOrNull()
?: return@runCatching null,
name = content["name"]!!.jsonPrimitive.content,
query = content["query"]!!.jsonPrimitive.contentOrNull?.nullIfBlank(),
filtersJson = Json.encodeToString(content["filters"]!!.jsonArray),
)
}.getOrNull()
}
if (!savedSearch.isNullOrEmpty()) {
insertSavedSearch.awaitAll(savedSearch)
}
val feedSavedSearch = prefs.getStringSet("latest_tab_sources", emptySet())?.map {
FeedSavedSearch(
id = -1,
source = it.toLong(),
savedSearch = null,
global = true,
)
}
if (!feedSavedSearch.isNullOrEmpty()) {
insertFeedSavedSearch.awaitAll(feedSavedSearch)
}
prefs.edit(commit = true) {
remove("eh_saved_searches")
remove("latest_tab_sources")
}
return@withIOContext true
}
}

View File

@ -0,0 +1,25 @@
package mihon.core.migration.migrations
import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.App
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.preference.minusAssign
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.domain.library.service.LibraryPreferences
class MoveLibraryNonCompleteSettingMigration : Migration {
override val version: Float = 23f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val context = migrationContext.get<App>() ?: return@withIOContext false
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val libraryPreferences = migrationContext.get<LibraryPreferences>() ?: return@withIOContext false
val oldUpdateOngoingOnly = prefs.getBoolean("pref_update_only_non_completed_key", true)
if (!oldUpdateOngoingOnly) {
libraryPreferences.autoUpdateMangaRestrictions() -= LibraryPreferences.MANGA_NON_COMPLETED
}
return@withIOContext true
}
}

View File

@ -0,0 +1,57 @@
package mihon.core.migration.migrations
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.App
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.library.service.LibraryPreferences
class MoveLibrarySortingSettingsMigration : Migration {
override val version: Float = 20f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val context = migrationContext.get<App>() ?: return@withIOContext false
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val libraryPreferences = migrationContext.get<LibraryPreferences>() ?: return@withIOContext false
try {
val oldSortingMode = prefs.getInt(libraryPreferences.sortingMode().key(), 0 /* ALPHABETICAL */)
val oldSortingDirection = prefs.getBoolean("library_sorting_ascending", true)
val newSortingMode = when (oldSortingMode) {
0 -> "ALPHABETICAL"
1 -> "LAST_READ"
2 -> "LAST_MANGA_UPDATE"
3 -> "UNREAD_COUNT"
4 -> "TOTAL_CHAPTERS"
6 -> "LATEST_CHAPTER"
7 -> "DRAG_AND_DROP"
8 -> "DATE_ADDED"
9 -> "TAG_LIST"
10 -> "CHAPTER_FETCH_DATE"
else -> "ALPHABETICAL"
}
val newSortingDirection = when (oldSortingDirection) {
true -> "ASCENDING"
else -> "DESCENDING"
}
prefs.edit(commit = true) {
remove(libraryPreferences.sortingMode().key())
remove("library_sorting_ascending")
}
prefs.edit {
putString(libraryPreferences.sortingMode().key(), newSortingMode)
putString("library_sorting_ascending", newSortingDirection)
}
} catch (e: Exception) {
logcat(throwable = e) { "Already done migration" }
}
return@withIOContext true
}
}

View File

@ -0,0 +1,25 @@
package mihon.core.migration.migrations
import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.util.lang.withIOContext
class MoveReaderTapSettingMigration : Migration {
override val version: Float = 32f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val context = migrationContext.get<App>() ?: return@withIOContext false
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val readerPreferences = migrationContext.get<ReaderPreferences>() ?: return@withIOContext false
val oldReaderTap = prefs.getBoolean("reader_tap", false)
if (!oldReaderTap) {
readerPreferences.navigationModePager().set(5)
readerPreferences.navigationModeWebtoon().set(5)
}
return@withIOContext true
}
}

View File

@ -0,0 +1,23 @@
package mihon.core.migration.migrations
import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.App
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.domain.library.service.LibraryPreferences
class MoveReadingButtonSettingMigration : Migration {
override val version: Float = 43f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val context = migrationContext.get<App>() ?: return@withIOContext false
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val libraryPreferences = migrationContext.get<LibraryPreferences>() ?: return@withIOContext false
if (prefs.getBoolean("start_reading_button", false)) {
libraryPreferences.showContinueReadingButton().set(true)
}
return@withIOContext true
}
}

View File

@ -0,0 +1,22 @@
package mihon.core.migration.migrations
import eu.kanade.domain.ui.UiPreferences
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.core.common.util.lang.withIOContext
class MoveRelativeTimeSettingMigration : Migration {
override val version: Float = 57f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val preferenceStore = migrationContext.get<PreferenceStore>() ?: return@withIOContext false
val uiPreferences = migrationContext.get<UiPreferences>() ?: return@withIOContext false
val pref = preferenceStore.getInt("relative_time", 7)
if (pref.get() == 0) {
uiPreferences.relativeTime().set(false)
}
return@withIOContext true
}
}

View File

@ -0,0 +1,24 @@
package mihon.core.migration.migrations
import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.core.security.SecurityPreferences
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.util.lang.withIOContext
class MoveSecureScreenSettingMigration : Migration {
override val version: Float = 27f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val context = migrationContext.get<App>() ?: return@withIOContext false
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val securityPreferences = migrationContext.get<SecurityPreferences>() ?: return@withIOContext false
val oldSecureScreen = prefs.getBoolean("secure_screen", false)
if (oldSecureScreen) {
securityPreferences.secureScreen().set(SecurityPreferences.SecureScreenMode.ALWAYS)
}
return@withIOContext true
}
}

View File

@ -0,0 +1,67 @@
package mihon.core.migration.migrations
import eu.kanade.tachiyomi.App
import mihon.core.migration.MigrateUtils
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.core.common.util.lang.withIOContext
class MoveSettingsToPrivateOrAppStateMigration : Migration {
override val version: Float = 59f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val context = migrationContext.get<App>() ?: return@withIOContext false
val preferenceStore = migrationContext.get<PreferenceStore>() ?: return@withIOContext false
val prefsToReplace = listOf(
"pref_download_only",
"incognito_mode",
"last_catalogue_source",
"trusted_signatures",
"last_app_closed",
"library_update_last_timestamp",
"library_unseen_updates_count",
"last_used_category",
"last_app_check",
"last_ext_check",
"last_version_code",
"skip_pre_migration",
"eh_auto_update_stats",
"storage_dir",
)
MigrateUtils.replacePreferences(
preferenceStore = preferenceStore,
filterPredicate = { it.key in prefsToReplace },
newKey = { Preference.appStateKey(it) },
)
val privatePrefsToReplace = listOf(
"sql_password",
"encrypt_database",
"cbz_password",
"password_protect_downloads",
"eh_ipb_member_id",
"enable_exhentai",
"eh_ipb_member_id",
"eh_ipb_pass_hash",
"eh_igneous",
"eh_ehSettingsProfile",
"eh_exhSettingsProfile",
"eh_settingsKey",
"eh_sessionCookie",
"eh_hathPerksCookie",
)
MigrateUtils.replacePreferences(
preferenceStore = preferenceStore,
filterPredicate = { it.key in privatePrefsToReplace },
newKey = { Preference.privateKey(it) },
)
// Deleting old download cache index files, but might as well clear it all out
context.cacheDir.deleteRecursively()
return@withIOContext true
}
}

View File

@ -0,0 +1,27 @@
package mihon.core.migration.migrations
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.App
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.domain.library.service.LibraryPreferences
class MoveSortingModeSettingMigration : Migration {
override val version: Float = 39f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val context = migrationContext.get<App>() ?: return@withIOContext false
val libraryPreferences = migrationContext.get<LibraryPreferences>() ?: return@withIOContext false
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
prefs.edit {
val sort = prefs.getString(libraryPreferences.sortingMode().key(), null) ?: return@edit
val direction = prefs.getString("library_sorting_ascending", "ASCENDING")!!
putString(libraryPreferences.sortingMode().key(), "$sort,$direction")
remove("library_sorting_ascending")
}
return@withIOContext true
}
}

View File

@ -0,0 +1,49 @@
package mihon.core.migration.migrations
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.App
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.data.DatabaseHandler
import tachiyomi.data.category.CategoryMapper
import tachiyomi.domain.library.service.LibraryPreferences
class MoveSortingModeSettingsMigration : Migration {
override val version: Float = 38f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val context = migrationContext.get<App>() ?: return@withIOContext false
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val libraryPreferences = migrationContext.get<LibraryPreferences>() ?: return@withIOContext false
val handler = migrationContext.get<DatabaseHandler>() ?: return@withIOContext false
// Handle renamed enum values
val newSortingMode = when (
val oldSortingMode = prefs.getString(libraryPreferences.sortingMode().key(), "ALPHABETICAL")
) {
"LAST_CHECKED" -> "LAST_MANGA_UPDATE"
"UNREAD" -> "UNREAD_COUNT"
"DATE_FETCHED" -> "CHAPTER_FETCH_DATE"
"DRAG_AND_DROP" -> "ALPHABETICAL"
else -> oldSortingMode
}
prefs.edit {
putString(libraryPreferences.sortingMode().key(), newSortingMode)
}
handler.await(true) {
categoriesQueries.getCategories(CategoryMapper::mapCategory).executeAsList()
.filter { (it.flags and 0b00111100L) == 0b00100000L }
.forEach {
categoriesQueries.update(
categoryId = it.id,
flags = it.flags and 0b00111100L.inv(),
name = null,
order = null,
)
}
}
return@withIOContext true
}
}

View File

@ -0,0 +1,21 @@
package mihon.core.migration.migrations
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.preference.getAndSet
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.domain.library.service.LibraryPreferences
class RemoveBatteryNotLowRestrictionMigration : Migration {
override val version: Float = 56f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val libraryPreferences = migrationContext.get<LibraryPreferences>() ?: return@withIOContext false
val pref = libraryPreferences.autoUpdateDeviceRestrictions()
if (pref.isSet() && "battery_not_low" in pref.get()) {
pref.getAndSet { it - "battery_not_low" }
}
return@withIOContext true
}
}

View File

@ -0,0 +1,20 @@
package mihon.core.migration.migrations
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.util.lang.withIOContext
class RemoveOldReaderThemeMigration : Migration {
override val version: Float = 18f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val readerPreferences = migrationContext.get<ReaderPreferences>() ?: return@withIOContext false
val readerTheme = readerPreferences.readerTheme().get()
if (readerTheme == 4) {
readerPreferences.readerTheme().set(3)
}
return@withIOContext true
}
}

View File

@ -0,0 +1,20 @@
package mihon.core.migration.migrations
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.domain.library.service.LibraryPreferences
class RemoveShortLibraryUpdatesMigration : Migration {
override val version: Float = 22f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val libraryPreferences = migrationContext.get<LibraryPreferences>() ?: return@withIOContext false
val updateInterval = libraryPreferences.autoUpdateInterval().get()
if (updateInterval in listOf(3, 4, 6, 8)) {
libraryPreferences.autoUpdateInterval().set(12)
}
return@withIOContext true
}
}

View File

@ -0,0 +1,20 @@
package mihon.core.migration.migrations
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.domain.library.service.LibraryPreferences
class RemoveShorterLibraryUpdatesMigration : Migration {
override val version: Float = 18f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val libraryPreferences = migrationContext.get<LibraryPreferences>() ?: return@withIOContext false
val updateInterval = libraryPreferences.autoUpdateInterval().get()
if (updateInterval == 1 || updateInterval == 2) {
libraryPreferences.autoUpdateInterval().set(3)
}
return@withIOContext true
}
}

View File

@ -0,0 +1,55 @@
package mihon.core.migration.migrations
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.util.system.workManager
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.core.common.preference.TriState
import tachiyomi.core.common.preference.getEnum
import tachiyomi.core.common.util.lang.withIOContext
class RemoveUpdateCheckerJobsMigration : Migration {
override val version: Float = 52f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val context = migrationContext.get<App>() ?: return@withIOContext false
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val preferenceStore = migrationContext.get<PreferenceStore>() ?: return@withIOContext false
val trackerManager = migrationContext.get<TrackerManager>() ?: return@withIOContext false
// Removed background jobs
context.workManager.cancelAllWorkByTag("UpdateChecker")
context.workManager.cancelAllWorkByTag("ExtensionUpdate")
prefs.edit {
remove("automatic_ext_updates")
}
val prefKeys = listOf(
"pref_filter_library_downloaded",
"pref_filter_library_unread",
"pref_filter_library_started",
"pref_filter_library_bookmarked",
"pref_filter_library_completed",
"pref_filter_library_lewd",
) + trackerManager.trackers.map { "pref_filter_library_tracked_${it.id}" }
prefKeys.forEach { key ->
val pref = prefs.getInt(key, 0)
prefs.edit {
remove(key)
val newValue = when (pref) {
1 -> TriState.ENABLED_IS
2 -> TriState.ENABLED_NOT
else -> TriState.DISABLED
}
preferenceStore.getEnum("${key}_v2", TriState.DISABLED).set(newValue)
}
}
return@withIOContext true
}
}

View File

@ -0,0 +1,39 @@
package mihon.core.migration.migrations
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.App
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.domain.library.service.LibraryPreferences
class ResetFilterAndSortSettingsMigration : Migration {
override val version: Float = 41f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val context = migrationContext.get<App>() ?: return@withIOContext false
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val libraryPreferences = migrationContext.get<LibraryPreferences>() ?: return@withIOContext false
val preferences = listOf(
libraryPreferences.filterChapterByRead(),
libraryPreferences.filterChapterByDownloaded(),
libraryPreferences.filterChapterByBookmarked(),
libraryPreferences.sortChapterBySourceOrNumber(),
libraryPreferences.displayChapterByNameOrNumber(),
libraryPreferences.sortChapterByAscendingOrDescending(),
)
prefs.edit {
preferences.forEach { preference ->
val key = preference.key()
val value = prefs.getInt(key, Int.MIN_VALUE)
if (value == Int.MIN_VALUE) return@forEach
remove(key)
putLong(key, value.toLong())
}
}
return@withIOContext true
}
}

View File

@ -0,0 +1,39 @@
package mihon.core.migration.migrations
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.util.lang.withIOContext
class ResetReaderSettingsMigration : Migration {
override val version: Float = 17f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val context = migrationContext.get<App>() ?: return@withIOContext false
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
// Migrate Rotation and Viewer values to default values for viewer_flags
val newOrientation = when (prefs.getInt("pref_rotation_type_key", 1)) {
1 -> ReaderOrientation.FREE.flagValue
2 -> ReaderOrientation.PORTRAIT.flagValue
3 -> ReaderOrientation.LANDSCAPE.flagValue
4 -> ReaderOrientation.LOCKED_PORTRAIT.flagValue
5 -> ReaderOrientation.LOCKED_LANDSCAPE.flagValue
else -> ReaderOrientation.FREE.flagValue
}
// Reading mode flag and prefValue is the same value
val newReadingMode = prefs.getInt("pref_default_viewer_key", 1)
prefs.edit {
putInt("pref_default_orientation_type_key", newOrientation)
remove("pref_rotation_type_key")
putInt("pref_default_reading_mode_key", newReadingMode)
remove("pref_default_viewer_key")
}
return@withIOContext true
}
}

View File

@ -0,0 +1,25 @@
package mihon.core.migration.migrations
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.App
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import tachiyomi.core.common.util.lang.withIOContext
class ResetRotationSettingMigration : Migration {
override val version: Float = 16f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val context = migrationContext.get<App>() ?: return@withIOContext false
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
// Reset rotation to Free after replacing Lock
if (prefs.contains("pref_rotation_type_key")) {
prefs.edit {
putInt("pref_rotation_type_key", 1)
}
}
return@withIOContext true
}
}

View File

@ -0,0 +1,16 @@
package mihon.core.migration.migrations
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
class SetupBackupCreateMigration : Migration {
override val version: Float = Migration.ALWAYS
override suspend fun invoke(migrationContext: MigrationContext): Boolean {
val context = migrationContext.get<App>() ?: return false
BackupCreateJob.setupTask(context)
return true
}
}

View File

@ -0,0 +1,16 @@
package mihon.core.migration.migrations
import eu.kanade.tachiyomi.App
import exh.eh.EHentaiUpdateWorker
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
class SetupEHentaiUpdateMigration : Migration {
override val version: Float = Migration.ALWAYS
override suspend fun invoke(migrationContext: MigrationContext): Boolean {
val context = migrationContext.get<App>() ?: return false
EHentaiUpdateWorker.scheduleBackground(context)
return true
}
}

View File

@ -0,0 +1,16 @@
package mihon.core.migration.migrations
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
class SetupLibraryUpdateMigration : Migration {
override val version: Float = Migration.ALWAYS
override suspend fun invoke(migrationContext: MigrationContext): Boolean {
val context = migrationContext.get<App>() ?: return false
LibraryUpdateJob.setupTask(context)
return true
}
}

View File

@ -0,0 +1,16 @@
package mihon.core.migration.migrations
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.data.sync.SyncDataJob
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
class SetupSyncDataMigration : Migration {
override val version: Float = Migration.ALWAYS
override suspend fun invoke(migrationContext: MigrationContext): Boolean {
val context = migrationContext.get<App>() ?: return false
SyncDataJob.setupTask(context)
return true
}
}

View File

@ -0,0 +1,35 @@
package mihon.core.migration.migrations
import eu.kanade.domain.source.service.SourcePreferences
import logcat.LogPriority
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
import mihon.domain.extensionrepo.exception.SaveExtensionRepoException
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.system.logcat
class TrustExtensionRepositoryMigration : Migration {
override val version: Float = 67f
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
val sourcePreferences = migrationContext.get<SourcePreferences>() ?: return@withIOContext false
val extensionRepositoryRepository =
migrationContext.get<ExtensionRepoRepository>() ?: return@withIOContext false
for ((index, source) in sourcePreferences.extensionRepos().get().withIndex()) {
try {
extensionRepositoryRepository.upsertRepo(
source,
"Repo #${index + 1}",
null,
source,
"NOFINGERPRINT-${index + 1}",
)
} catch (e: SaveExtensionRepoException) {
logcat(LogPriority.ERROR, e) { "Error Migrating Extension Repo with baseUrl: $source" }
}
}
sourcePreferences.extensionRepos().delete()
return@withIOContext true
}
}

View File

@ -0,0 +1,96 @@
package mihon.core.migration
import io.mockk.Called
import io.mockk.spyk
import io.mockk.verify
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
class MigratorTest {
@Test
fun initialVersion() {
val onMigrationComplete: () -> Unit = {}
val onMigrationCompleteSpy = spyk(onMigrationComplete)
val didMigration = Migrator.migrate(
old = 0,
new = 1,
migrations = listOf(Migration.of(Migration.ALWAYS) { true }, Migration.of(2f) { false }),
onMigrationComplete = onMigrationCompleteSpy
)
verify { onMigrationCompleteSpy() }
Assertions.assertTrue(didMigration)
}
@Test
fun sameVersion() {
val onMigrationComplete: () -> Unit = {}
val onMigrationCompleteSpy = spyk(onMigrationComplete)
val didMigration = Migrator.migrate(
old = 1,
new = 1,
migrations = listOf(Migration.of(Migration.ALWAYS) { true }, Migration.of(2f) { true }),
onMigrationComplete = onMigrationCompleteSpy
)
verify { onMigrationCompleteSpy wasNot Called }
Assertions.assertFalse(didMigration)
}
@Test
fun smallMigration() {
val onMigrationComplete: () -> Unit = {}
val onMigrationCompleteSpy = spyk(onMigrationComplete)
val didMigration = Migrator.migrate(
old = 1,
new = 2,
migrations = listOf(Migration.of(Migration.ALWAYS) { true }, Migration.of(2f) { true }),
onMigrationComplete = onMigrationCompleteSpy
)
verify { onMigrationCompleteSpy() }
Assertions.assertTrue(didMigration)
}
@Test
fun largeMigration() {
val onMigrationComplete: () -> Unit = {}
val onMigrationCompleteSpy = spyk(onMigrationComplete)
val input = listOf(
Migration.of(Migration.ALWAYS) { true },
Migration.of(2f) { true },
Migration.of(3f) { true },
Migration.of(4f) { true },
Migration.of(5f) { true },
Migration.of(6f) { true },
Migration.of(7f) { true },
Migration.of(8f) { true },
Migration.of(9f) { true },
Migration.of(10f) { true },
)
val didMigration = Migrator.migrate(
old = 1,
new = 10,
migrations = input,
onMigrationComplete = onMigrationCompleteSpy
)
verify { onMigrationCompleteSpy() }
Assertions.assertTrue(didMigration)
}
@Test
fun withinRangeMigration() {
val onMigrationComplete: () -> Unit = {}
val onMigrationCompleteSpy = spyk(onMigrationComplete)
val didMigration = Migrator.migrate(
old = 1,
new = 2,
migrations = listOf(
Migration.of(Migration.ALWAYS) { true },
Migration.of(2f) { true },
Migration.of(3f) { false }
),
onMigrationComplete = onMigrationCompleteSpy
)
verify { onMigrationCompleteSpy() }
Assertions.assertTrue(didMigration)
}
}