From a1d54880c374e4a74e2fdebb362751a1d6965f5c Mon Sep 17 00:00:00 2001 From: Jobobby04 Date: Sat, 5 Sep 2020 18:17:33 -0400 Subject: [PATCH] Merged manga implementation, man this took forever to make and bugfix, its not even done --- app/build.gradle | 2 +- .../tachiyomi/data/backup/BackupManager.kt | 121 ++++++-- .../data/backup/BackupRestoreService.kt | 11 +- .../tachiyomi/data/backup/models/Backup.kt | 1 + .../MergedMangaReferenceTypeAdapter.kt | 45 +++ .../data/database/queries/RawQueries.kt | 68 +++-- .../data/library/LibraryUpdateService.kt | 14 +- .../source/online/SuspendHttpSource.kt | 151 ++++++---- .../source/online/all/MergedSource.kt | 265 +++++++++--------- .../process/MigrationProcessHolder.kt | 10 +- .../ui/browse/source/SourceController.kt | 1 + .../tachiyomi/ui/library/LibraryPresenter.kt | 38 ++- .../tachiyomi/ui/manga/MangaController.kt | 24 +- .../tachiyomi/ui/manga/MangaPresenter.kt | 248 +++++++++++----- .../ui/manga/info/MangaInfoHeaderAdapter.kt | 12 +- .../ui/manga/merged/EditMergedMangaAdapter.kt | 36 +++ .../ui/manga/merged/EditMergedMangaHolder.kt | 83 ++++++ .../ui/manga/merged/EditMergedMangaItem.kt | 50 ++++ .../manga/merged/EditMergedSettingsDialog.kt | 184 ++++++++++++ .../merged/EditMergedSettingsHeaderAdapter.kt | 150 ++++++++++ .../tachiyomi/ui/reader/ReaderPresenter.kt | 10 +- .../ui/reader/loader/ChapterLoader.kt | 31 +- app/src/main/java/exh/EXHMigrations.kt | 161 +++++++++++ .../MergeMangaSettingsPutResolver.kt | 31 ++ .../MergedMangaSettingsPutResolver.kt | 34 +++ app/src/main/java/exh/util/RxUtil.kt | 52 +++- .../layout/edit_merged_settings_dialog.xml | 17 ++ .../layout/edit_merged_settings_header.xml | 65 +++++ .../res/layout/edit_merged_settings_item.xml | 138 +++++++++ app/src/main/res/layout/eh_smart_search.xml | 6 +- app/src/main/res/menu/manga.xml | 14 + app/src/main/res/values/strings_sy.xml | 19 ++ 32 files changed, 1753 insertions(+), 339 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/MergedMangaReferenceTypeAdapter.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/manga/merged/EditMergedMangaAdapter.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/manga/merged/EditMergedMangaHolder.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/manga/merged/EditMergedMangaItem.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/manga/merged/EditMergedSettingsDialog.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/manga/merged/EditMergedSettingsHeaderAdapter.kt create mode 100644 app/src/main/java/exh/merged/sql/resolvers/MergeMangaSettingsPutResolver.kt create mode 100644 app/src/main/java/exh/merged/sql/resolvers/MergedMangaSettingsPutResolver.kt create mode 100644 app/src/main/res/layout/edit_merged_settings_dialog.xml create mode 100644 app/src/main/res/layout/edit_merged_settings_header.xml create mode 100644 app/src/main/res/layout/edit_merged_settings_item.xml diff --git a/app/build.gradle b/app/build.gradle index fef0e5604..23bd88c59 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -43,7 +43,7 @@ android { minSdkVersion AndroidConfig.minSdk targetSdkVersion AndroidConfig.targetSdk testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - versionCode 6 + versionCode 7 versionName "1.2.0" buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\"" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt index cf1569729..f209af853 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt @@ -32,6 +32,7 @@ import eu.kanade.tachiyomi.data.backup.models.Backup.CURRENT_VERSION import eu.kanade.tachiyomi.data.backup.models.Backup.EXTENSIONS import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA +import eu.kanade.tachiyomi.data.backup.models.Backup.MERGEDMANGAREFERENCES import eu.kanade.tachiyomi.data.backup.models.Backup.SAVEDSEARCHES import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK import eu.kanade.tachiyomi.data.backup.models.DHistory @@ -39,6 +40,7 @@ import eu.kanade.tachiyomi.data.backup.serializer.CategoryTypeAdapter import eu.kanade.tachiyomi.data.backup.serializer.ChapterTypeAdapter import eu.kanade.tachiyomi.data.backup.serializer.HistoryTypeAdapter import eu.kanade.tachiyomi.data.backup.serializer.MangaTypeAdapter +import eu.kanade.tachiyomi.data.backup.serializer.MergedMangaReferenceTypeAdapter import eu.kanade.tachiyomi.data.backup.serializer.TrackTypeAdapter import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.CategoryImpl @@ -57,11 +59,17 @@ import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.online.all.EHentai +import eu.kanade.tachiyomi.source.online.all.MergedSource import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource import exh.EXHSavedSearch +import exh.MERGED_SOURCE_ID import exh.eh.EHentaiThrottleManager +import exh.merged.sql.models.MergedMangaReference +import exh.util.asObservable import java.lang.RuntimeException import kotlin.math.max +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking import rx.Observable import timber.log.Timber import uy.kohesive.injekt.Injekt @@ -106,6 +114,9 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { .registerTypeAdapter(CategoryTypeAdapter.build()) .registerTypeAdapter(HistoryTypeAdapter.build()) .registerTypeHierarchyAdapter(TrackTypeAdapter.build()) + // SY --> + .registerTypeAdapter(MergedMangaReferenceTypeAdapter.build()) + // SY <-- .create() else -> throw Exception("Json version unknown") } @@ -129,15 +140,21 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { // Create extension ID/name mapping val extensionEntries = JsonArray() + // Merged Manga References + val mergedMangaReferenceEntries = JsonArray() + // Add value's to root root[Backup.VERSION] = CURRENT_VERSION root[Backup.MANGAS] = mangaEntries root[CATEGORIES] = categoryEntries root[EXTENSIONS] = extensionEntries + // SY --> + root[MERGEDMANGAREFERENCES] = mergedMangaReferenceEntries + // SY <-- databaseHelper.inTransaction { // Get manga from database - val mangas = getFavoriteManga() + val mangas = getFavoriteManga() /* SY --> */ + getMergedManga() /* SY <-- */ val extensions: MutableSet = mutableSetOf() @@ -163,6 +180,8 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { // SY --> root[SAVEDSEARCHES] = Injekt.get().eh_savedSearches().get().joinToString(separator = "***") + + backupMergedMangaReferences(mergedMangaReferenceEntries) // SY <-- } @@ -212,6 +231,13 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { } } + // SY --> + private fun backupMergedMangaReferences(root: JsonArray) { + val mergedMangaReferences = databaseHelper.getMergedMangaReferences().executeAsBlocking() + mergedMangaReferences.forEach { root.add(parser.toJsonTree(it)) } + } + // SY <-- + /** * Backup the categories of library * @@ -317,29 +343,40 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { */ fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List, throttleManager: EHentaiThrottleManager): Observable, List>> { // SY --> - return ( - if (source is EHentai) { - source.fetchChapterList(manga, throttleManager::throttle) - } else { - source.fetchChapterList(manga) - } - ).map { - if (it.last().chapter_number == -99F) { - chapters.forEach { chapter -> - chapter.name = "Chapter ${chapter.chapter_number} restored by dummy source" - } - syncChaptersWithSource(databaseHelper, chapters, manga, source) - } else { - syncChaptersWithSource(databaseHelper, it, manga, source) - } - } - // SY <-- - .doOnNext { pair -> + if (source is MergedSource) { + val syncedChapters = runBlocking { source.fetchChaptersAndSync(manga, false) } + return syncedChapters.onEach { pair -> if (pair.first.isNotEmpty()) { chapters.forEach { it.manga_id = manga.id } insertChapters(chapters) } + }.asObservable() + } else { + return ( + if (source is EHentai) { + source.fetchChapterList(manga, throttleManager::throttle) + } else { + source.fetchChapterList(manga) + } + ).map { + if (it.last().chapter_number == -99F) { + chapters.forEach { chapter -> + chapter.name = + "Chapter ${chapter.chapter_number} restored by dummy source" + } + syncChaptersWithSource(databaseHelper, chapters, manga, source) + } else { + syncChaptersWithSource(databaseHelper, it, manga, source) + } } + // SY <-- + .doOnNext { pair -> + if (pair.first.isNotEmpty()) { + chapters.forEach { it.manga_id = manga.id } + insertChapters(chapters) + } + } + } } /** @@ -584,6 +621,49 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { } preferences.eh_savedSearches().set((otherSerialized + newSerialized).toSet()) } + + /** + * Restore the categories from Json + * + * @param jsonMergedMangaReferences array containing md manga references + */ + internal fun restoreMergedMangaReferences(jsonMergedMangaReferences: JsonArray) { + // Get merged manga references from file and from db + val dbMergedMangaReferences = databaseHelper.getMergedMangaReferences().executeAsBlocking() + val backupMergedMangaReferences = parser.fromJson>(jsonMergedMangaReferences) + var lastMergeManga: Manga? = null + + // Iterate over them + backupMergedMangaReferences.forEach { mergedMangaReference -> + // Used to know if the merged manga reference is already in the db + var found = false + for (dbMergedMangaReference in dbMergedMangaReferences) { + // If the mergedMangaReference is already in the db, assign the id to the file's mergedMangaReference + // and do nothing + if (mergedMangaReference.mergeUrl == dbMergedMangaReference.mergeUrl && mergedMangaReference.mangaUrl == dbMergedMangaReference.mangaUrl) { + mergedMangaReference.id = dbMergedMangaReference.id + mergedMangaReference.mergeId = dbMergedMangaReference.mergeId + mergedMangaReference.mangaId = dbMergedMangaReference.mangaId + found = true + break + } + } + // If the mergedMangaReference isn't in the db, remove the id and insert a new mergedMangaReference + // Store the inserted id in the mergedMangaReference + if (!found) { + // Let the db assign the id + val mergedManga = (if (mergedMangaReference.mergeUrl != lastMergeManga?.url) databaseHelper.getManga(mergedMangaReference.mergeUrl, MERGED_SOURCE_ID).executeAsBlocking() else lastMergeManga) ?: return@forEach + val manga = databaseHelper.getManga(mergedMangaReference.mangaUrl, mergedMangaReference.mangaSourceId).executeAsBlocking() ?: return@forEach + lastMergeManga = mergedManga + + mergedMangaReference.mergeId = mergedManga.id + mergedMangaReference.mangaId = manga.id + mergedMangaReference.id = null + val result = databaseHelper.insertMergedManga(mergedMangaReference).executeAsBlocking() + mergedMangaReference.id = result.insertedId() + } + } + } // SY <-- /** @@ -602,6 +682,9 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { internal fun getFavoriteManga(): List = databaseHelper.getFavoriteMangas().executeAsBlocking() + internal fun getMergedManga(): List = + databaseHelper.getMergedMangas().executeAsBlocking() + /** * Inserts manga and returns id * diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt index 75b87f8f6..35c21cb44 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt @@ -238,7 +238,7 @@ class BackupRestoreService : Service() { } totalAmount = mangasJson.size() - restoreAmount = validManga.count() + 1 // +1 for categories + restoreAmount = validManga.count() + 3 // +1 for categories, +1 for saved searches, +1 for merged manga references skippedAmount = mangasJson.size() - validManga.count() // SY <-- restoreProgress = 0 @@ -288,6 +288,15 @@ class BackupRestoreService : Service() { restoreProgress += 1 showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.saved_searches)) } + + private fun restoreMergedMangaReferences(mergedMangaReferencesJson: JsonElement) { + db.inTransaction { + backupManager.restoreMergedMangaReferences(mergedMangaReferencesJson.asJsonArray) + } + + restoreProgress += 1 + showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.categories)) + } // SY <-- private fun restoreManga(mangaJson: JsonObject) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt index 5031cd11d..3e017c916 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt @@ -19,6 +19,7 @@ object Backup { const val VERSION = "version" // SY --> const val SAVEDSEARCHES = "savedsearches" + const val MERGEDMANGAREFERENCES = "mergedmangareferences" // SY <-- fun getDefaultFilename(): String { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/MergedMangaReferenceTypeAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/MergedMangaReferenceTypeAdapter.kt new file mode 100644 index 000000000..90e464907 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/serializer/MergedMangaReferenceTypeAdapter.kt @@ -0,0 +1,45 @@ +package eu.kanade.tachiyomi.data.backup.serializer + +import com.github.salomonbrys.kotson.typeAdapter +import com.google.gson.TypeAdapter +import exh.merged.sql.models.MergedMangaReference + +/** + * JSON Serializer used to write / read [MergedMangaReference] to / from json + */ +object MergedMangaReferenceTypeAdapter { + + fun build(): TypeAdapter { + return typeAdapter { + write { + beginArray() + value(it.mangaUrl) + value(it.mergeUrl) + value(it.mangaSourceId) + value(it.chapterSortMode) + value(it.chapterPriority) + value(it.getChapterUpdates) + value(it.isInfoManga) + value(it.downloadChapters) + endArray() + } + + read { + beginArray() + MergedMangaReference( + id = null, + mangaUrl = nextString(), + mergeUrl = nextString(), + mangaSourceId = nextLong(), + chapterSortMode = nextInt(), + chapterPriority = nextInt(), + getChapterUpdates = nextBoolean(), + isInfoManga = nextBoolean(), + downloadChapters = nextBoolean(), + mangaId = null, + mergeId = null + ) + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt index 0fbaef8a2..16a114be8 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt @@ -5,6 +5,7 @@ import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter import eu.kanade.tachiyomi.data.database.tables.HistoryTable as History import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable as MangaCategory import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga +import exh.MERGED_SOURCE_ID import exh.merged.sql.tables.MergedTable as Merged // SY --> @@ -21,6 +22,9 @@ fun getMergedMangaQuery() = ON ${Manga.TABLE}.${Manga.COL_ID} = M.${Merged.COL_MANGA_ID} """ +/** + * Query to get all the manga that are merged into other manga + */ fun getAllMergedMangaQuery() = """ SELECT ${Manga.TABLE}.* @@ -56,7 +60,6 @@ fun getMergedChaptersQuery() = JOIN ${Chapter.TABLE} ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = M.${Merged.COL_MANGA_ID} """ -// SY <-- /** * Query to get the manga from the library, with their categories and unread count. @@ -66,29 +69,54 @@ val libraryQuery = SELECT M.*, COALESCE(MC.${MangaCategory.COL_CATEGORY_ID}, 0) AS ${Manga.COL_CATEGORY} FROM ( SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD}, COALESCE(R.read, 0) AS ${Manga.COL_READ} - FROM ${Manga.TABLE} - LEFT JOIN ( - SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS unread - FROM ${Chapter.TABLE} - WHERE ${Chapter.COL_READ} = 0 - GROUP BY ${Chapter.COL_MANGA_ID} - ) AS C - ON ${Manga.COL_ID} = C.${Chapter.COL_MANGA_ID} - LEFT JOIN ( - SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS read - FROM ${Chapter.TABLE} - WHERE ${Chapter.COL_READ} = 1 - GROUP BY ${Chapter.COL_MANGA_ID} - ) AS R - ON ${Manga.COL_ID} = R.${Chapter.COL_MANGA_ID} - WHERE ${Manga.COL_FAVORITE} = 1 - GROUP BY ${Manga.COL_ID} + FROM ${Manga.TABLE} + LEFT JOIN ( + SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}, COUNT(*) AS unread + FROM ${Chapter.TABLE} + WHERE ${Chapter.COL_READ} = 0 + GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} + ) AS C + ON ${Manga.TABLE}.${Manga.COL_ID} = C.${Chapter.COL_MANGA_ID} + LEFT JOIN ( + SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS read + FROM ${Chapter.TABLE} + WHERE ${Chapter.COL_READ} = 1 + GROUP BY ${Chapter.COL_MANGA_ID} + ) AS R + ON ${Manga.TABLE}.${Manga.COL_ID} = R.${Chapter.COL_MANGA_ID} + WHERE ${Manga.COL_FAVORITE} = 1 AND ${Manga.COL_SOURCE} <> $MERGED_SOURCE_ID + GROUP BY ${Manga.TABLE}.${Manga.COL_ID} + UNION + SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD}, COALESCE(R.read, 0) AS ${Manga.COL_READ} + FROM ${Manga.TABLE} + LEFT JOIN ( + SELECT ${Merged.TABLE}.${Merged.COL_MERGE_ID}, COUNT(*) as unread + FROM ${Merged.TABLE} + JOIN ${Chapter.TABLE} + ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ${Merged.TABLE}.${Merged.COL_MANGA_ID} + WHERE ${Chapter.TABLE}.${Chapter.COL_READ} = 0 + GROUP BY ${Merged.TABLE}.${Merged.COL_MERGE_ID} + ) AS C + ON ${Manga.TABLE}.${Manga.COL_ID} = C.${Merged.COL_MERGE_ID} + LEFT JOIN ( + SELECT ${Merged.TABLE}.${Merged.COL_MERGE_ID}, COUNT(*) as read + FROM ${Merged.TABLE} + JOIN ${Chapter.TABLE} + ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ${Merged.TABLE}.${Merged.COL_MANGA_ID} + WHERE ${Chapter.TABLE}.${Chapter.COL_READ} = 1 + GROUP BY ${Merged.TABLE}.${Merged.COL_MERGE_ID} + ) AS R + ON ${Manga.TABLE}.${Manga.COL_ID} = R.${Merged.COL_MERGE_ID} + WHERE ${Manga.COL_FAVORITE} = 1 AND ${Manga.COL_SOURCE} = $MERGED_SOURCE_ID + GROUP BY ${Manga.TABLE}.${Manga.COL_ID} ORDER BY ${Manga.COL_TITLE} ) AS M LEFT JOIN ( - SELECT * FROM ${MangaCategory.TABLE}) AS MC - ON MC.${MangaCategory.COL_MANGA_ID} = M.${Manga.COL_ID} + SELECT * FROM ${MangaCategory.TABLE} + ) AS MC + ON MC.${MangaCategory.COL_MANGA_ID} = M.${Manga.COL_ID}; """ +// SY <-- /** * Query to get the recent chapters of manga from the library up to a date. diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt index bd024b27f..c54c1163d 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt @@ -22,6 +22,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.online.all.MergedSource import eu.kanade.tachiyomi.ui.library.LibraryGroup import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource import eu.kanade.tachiyomi.util.prepUpdateCover @@ -30,9 +31,12 @@ import eu.kanade.tachiyomi.util.storage.getUriCompat import eu.kanade.tachiyomi.util.system.acquireWakeLock import eu.kanade.tachiyomi.util.system.isServiceRunning import exh.LIBRARY_UPDATE_EXCLUDED_SOURCES +import exh.MERGED_SOURCE_ID +import exh.util.asObservable import exh.util.nullIfBlank import java.io.File import java.util.concurrent.atomic.AtomicInteger +import kotlinx.coroutines.runBlocking import rx.Observable import rx.Subscription import rx.schedulers.Schedulers @@ -385,7 +389,12 @@ class LibraryUpdateService( private fun downloadChapters(manga: Manga, chapters: List) { // We don't want to start downloading while the library is updating, because websites // may don't like it and they could ban the user. - downloadManager.downloadChapters(manga, chapters, false) + // SY --> + val chapterFilter = if (manga.source == MERGED_SOURCE_ID) { + db.getMergedMangaReferences(manga.id!!).executeAsBlocking().filterNot { it.downloadChapters }.mapNotNull { it.mangaId } + } else emptyList() + // SY <-- + downloadManager.downloadChapters(manga, /* SY --> */ chapters.filter { it.manga_id !in chapterFilter } /* SY <-- */, false) } /** @@ -417,7 +426,8 @@ class LibraryUpdateService( .subscribe() } - return source.fetchChapterList(manga) + return /* SY --> */ if (source is MergedSource) runBlocking { source.fetchChaptersAndSync(manga, false).asObservable() } + else /* SY <-- */ source.fetchChapterList(manga) .map { syncChaptersWithSource(db, it, manga, source) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/SuspendHttpSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/SuspendHttpSource.kt index da7973a2c..bc398a906 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/SuspendHttpSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/SuspendHttpSource.kt @@ -8,7 +8,10 @@ import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga +import exh.util.asObservable import kotlin.jvm.Throws +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.runBlocking import okhttp3.Request import okhttp3.Response @@ -25,13 +28,17 @@ abstract class SuspendHttpSource : HttpSource() { * * @param page the page number to retrieve. */ - override fun fetchPopularManga(page: Int): Observable { - return Observable.just(runBlocking { fetchPopularMangaSuspended(page) }) + final override fun fetchPopularManga(page: Int): Observable { + return fetchPopularMangaFlow(page).asObservable() } - open suspend fun fetchPopularMangaSuspended(page: Int): MangasPage { - val response = client.newCall(popularMangaRequestSuspended(page)).await() - return popularMangaParseSuspended(response) + open fun fetchPopularMangaFlow(page: Int): Flow { + return flow { + val response = client.newCall(popularMangaRequestSuspended(page)).await() + emit( + popularMangaParseSuspended(response) + ) + } } /** @@ -39,7 +46,7 @@ abstract class SuspendHttpSource : HttpSource() { * * @param page the page number to retrieve. */ - override fun popularMangaRequest(page: Int): Request { + final override fun popularMangaRequest(page: Int): Request { return runBlocking { popularMangaRequestSuspended(page) } } @@ -50,7 +57,7 @@ abstract class SuspendHttpSource : HttpSource() { * * @param response the response from the site. */ - override fun popularMangaParse(response: Response): MangasPage { + final override fun popularMangaParse(response: Response): MangasPage { return runBlocking { popularMangaParseSuspended(response) } } @@ -64,13 +71,17 @@ abstract class SuspendHttpSource : HttpSource() { * @param query the search query. * @param filters the list of filters to apply. */ - override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { - return Observable.just(runBlocking { fetchSearchMangaSuspended(page, query, filters) }) + final override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { + return fetchSearchMangaSuspended(page, query, filters).asObservable() } - open suspend fun fetchSearchMangaSuspended(page: Int, query: String, filters: FilterList): MangasPage { - val response = client.newCall(searchMangaRequestSuspended(page, query, filters)).await() - return searchMangaParseSuspended(response) + open fun fetchSearchMangaSuspended(page: Int, query: String, filters: FilterList): Flow { + return flow { + val response = client.newCall(searchMangaRequestSuspended(page, query, filters)).await() + emit( + searchMangaParseSuspended(response) + ) + } } /** @@ -80,7 +91,7 @@ abstract class SuspendHttpSource : HttpSource() { * @param query the search query. * @param filters the list of filters to apply. */ - override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + final override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { return runBlocking { searchMangaRequestSuspended(page, query, filters) } } @@ -91,7 +102,7 @@ abstract class SuspendHttpSource : HttpSource() { * * @param response the response from the site. */ - override fun searchMangaParse(response: Response): MangasPage { + final override fun searchMangaParse(response: Response): MangasPage { return runBlocking { searchMangaParseSuspended(response) } } @@ -102,13 +113,17 @@ abstract class SuspendHttpSource : HttpSource() { * * @param page the page number to retrieve. */ - override fun fetchLatestUpdates(page: Int): Observable { - return Observable.just(runBlocking { fetchLatestUpdatesSuspended(page) }) + final override fun fetchLatestUpdates(page: Int): Observable { + return fetchLatestUpdatesFlow(page).asObservable() } - open suspend fun fetchLatestUpdatesSuspended(page: Int): MangasPage { - val response = client.newCall(latestUpdatesRequestSuspended(page)).await() - return latestUpdatesParseSuspended(response) + open fun fetchLatestUpdatesFlow(page: Int): Flow { + return flow { + val response = client.newCall(latestUpdatesRequestSuspended(page)).await() + emit( + latestUpdatesParseSuspended(response) + ) + } } /** @@ -116,7 +131,7 @@ abstract class SuspendHttpSource : HttpSource() { * * @param page the page number to retrieve. */ - override fun latestUpdatesRequest(page: Int): Request { + final override fun latestUpdatesRequest(page: Int): Request { return runBlocking { latestUpdatesRequestSuspended(page) } } @@ -127,7 +142,7 @@ abstract class SuspendHttpSource : HttpSource() { * * @param response the response from the site. */ - override fun latestUpdatesParse(response: Response): MangasPage { + final override fun latestUpdatesParse(response: Response): MangasPage { return runBlocking { latestUpdatesParseSuspended(response) } } @@ -139,13 +154,17 @@ abstract class SuspendHttpSource : HttpSource() { * * @param manga the manga to be updated. */ - override fun fetchMangaDetails(manga: SManga): Observable { - return Observable.just(runBlocking { fetchMangaDetailsSuspended(manga) }) + final override fun fetchMangaDetails(manga: SManga): Observable { + return fetchMangaDetailsFlow(manga).asObservable() } - open suspend fun fetchMangaDetailsSuspended(manga: SManga): SManga { - val response = client.newCall(mangaDetailsRequestSuspended(manga)).await() - return mangaDetailsParseSuspended(response).apply { initialized = true } + open fun fetchMangaDetailsFlow(manga: SManga): Flow { + return flow { + val response = client.newCall(mangaDetailsRequestSuspended(manga)).await() + emit( + mangaDetailsParseSuspended(response).apply { initialized = true } + ) + } } /** @@ -154,7 +173,7 @@ abstract class SuspendHttpSource : HttpSource() { * * @param manga the manga to be updated. */ - override fun mangaDetailsRequest(manga: SManga): Request { + final override fun mangaDetailsRequest(manga: SManga): Request { return runBlocking { mangaDetailsRequestSuspended(manga) } } @@ -167,7 +186,7 @@ abstract class SuspendHttpSource : HttpSource() { * * @param response the response from the site. */ - override fun mangaDetailsParse(response: Response): SManga { + final override fun mangaDetailsParse(response: Response): SManga { return runBlocking { mangaDetailsParseSuspended(response) } } @@ -179,21 +198,25 @@ abstract class SuspendHttpSource : HttpSource() { * * @param manga the manga to look for chapters. */ - override fun fetchChapterList(manga: SManga): Observable> { + final override fun fetchChapterList(manga: SManga): Observable> { return try { - Observable.just(runBlocking { fetchChapterListSuspended(manga) }) + fetchChapterListFlow(manga).asObservable() } catch (e: LicencedException) { Observable.error(Exception("Licensed - No chapters to show")) } } @Throws(LicencedException::class) - open suspend fun fetchChapterListSuspended(manga: SManga): List { - return if (manga.status != SManga.LICENSED) { - val response = client.newCall(chapterListRequestSuspended(manga)).await() - chapterListParseSuspended(response) - } else { - throw LicencedException("Licensed - No chapters to show") + open fun fetchChapterListFlow(manga: SManga): Flow> { + return flow { + if (manga.status != SManga.LICENSED) { + val response = client.newCall(chapterListRequestSuspended(manga)).await() + emit( + chapterListParseSuspended(response) + ) + } else { + throw LicencedException("Licensed - No chapters to show") + } } } @@ -203,7 +226,7 @@ abstract class SuspendHttpSource : HttpSource() { * * @param manga the manga to look for chapters. */ - override fun chapterListRequest(manga: SManga): Request { + final override fun chapterListRequest(manga: SManga): Request { return runBlocking { chapterListRequestSuspended(manga) } } @@ -216,7 +239,7 @@ abstract class SuspendHttpSource : HttpSource() { * * @param response the response from the site. */ - override fun chapterListParse(response: Response): List { + final override fun chapterListParse(response: Response): List { return runBlocking { chapterListParseSuspended(response) } } @@ -227,13 +250,17 @@ abstract class SuspendHttpSource : HttpSource() { * * @param chapter the chapter whose page list has to be fetched. */ - override fun fetchPageList(chapter: SChapter): Observable> { - return Observable.just(runBlocking { fetchPageListSuspended(chapter) }) + final override fun fetchPageList(chapter: SChapter): Observable> { + return fetchPageListFlow(chapter).asObservable() } - open suspend fun fetchPageListSuspended(chapter: SChapter): List { - val response = client.newCall(pageListRequestSuspended(chapter)).await() - return pageListParseSuspended(response) + open fun fetchPageListFlow(chapter: SChapter): Flow> { + return flow { + val response = client.newCall(pageListRequestSuspended(chapter)).await() + emit( + pageListParseSuspended(response) + ) + } } /** @@ -242,7 +269,7 @@ abstract class SuspendHttpSource : HttpSource() { * * @param chapter the chapter whose page list has to be fetched. */ - override fun pageListRequest(chapter: SChapter): Request { + final override fun pageListRequest(chapter: SChapter): Request { return runBlocking { pageListRequestSuspended(chapter) } } @@ -255,7 +282,7 @@ abstract class SuspendHttpSource : HttpSource() { * * @param response the response from the site. */ - override fun pageListParse(response: Response): List { + final override fun pageListParse(response: Response): List { return runBlocking { pageListParseSuspended(response) } } @@ -267,13 +294,17 @@ abstract class SuspendHttpSource : HttpSource() { * * @param page the page whose source image has to be fetched. */ - override fun fetchImageUrl(page: Page): Observable { - return Observable.just(runBlocking { fetchImageUrlSuspended(page) }) + final override fun fetchImageUrl(page: Page): Observable { + return fetchImageUrlFlow(page).asObservable() } - open suspend fun fetchImageUrlSuspended(page: Page): String { - val response = client.newCall(imageUrlRequestSuspended(page)).await() - return imageUrlParseSuspended(response) + open fun fetchImageUrlFlow(page: Page): Flow { + return flow { + val response = client.newCall(imageUrlRequestSuspended(page)).await() + emit( + imageUrlParseSuspended(response) + ) + } } /** @@ -282,7 +313,7 @@ abstract class SuspendHttpSource : HttpSource() { * * @param page the chapter whose page list has to be fetched */ - override fun imageUrlRequest(page: Page): Request { + final override fun imageUrlRequest(page: Page): Request { return runBlocking { imageUrlRequestSuspended(page) } } @@ -295,7 +326,7 @@ abstract class SuspendHttpSource : HttpSource() { * * @param response the response from the site. */ - override fun imageUrlParse(response: Response): String { + final override fun imageUrlParse(response: Response): String { return runBlocking { imageUrlParseSuspended(response) } } @@ -306,12 +337,16 @@ abstract class SuspendHttpSource : HttpSource() { * * @param page the page whose source image has to be downloaded. */ - override fun fetchImage(page: Page): Observable { - return Observable.just(runBlocking { fetchImageSuspended(page) }) + final override fun fetchImage(page: Page): Observable { + return fetchImageFlow(page).asObservable() } - open suspend fun fetchImageSuspended(page: Page): Response { - return client.newCallWithProgress(imageRequestSuspended(page), page).await() + open fun fetchImageFlow(page: Page): Flow { + return flow { + emit( + client.newCallWithProgress(imageRequestSuspended(page), page).await() + ) + } } /** @@ -320,7 +355,7 @@ abstract class SuspendHttpSource : HttpSource() { * * @param page the chapter whose page list has to be fetched */ - override fun imageRequest(page: Page): Request { + final override fun imageRequest(page: Page): Request { return runBlocking { imageRequestSuspended(page) } } @@ -335,7 +370,7 @@ abstract class SuspendHttpSource : HttpSource() { * @param chapter the chapter to be added. * @param manga the manga of the chapter. */ - override fun prepareNewChapter(chapter: SChapter, manga: SManga) { + final override fun prepareNewChapter(chapter: SChapter, manga: SManga) { runBlocking { prepareNewChapterSuspended(chapter, manga) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MergedSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MergedSource.kt index bd08aa245..804b37e2f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MergedSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MergedSource.kt @@ -1,43 +1,44 @@ package eu.kanade.tachiyomi.source.online.all import com.elvishew.xlog.XLog -import com.github.salomonbrys.kotson.fromJson -import com.google.gson.Gson -import com.google.gson.annotations.SerializedName import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.download.DownloadManager +import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.SuspendHttpSource +import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource +import eu.kanade.tachiyomi.util.shouldDownloadNewChapters import exh.MERGED_SOURCE_ID +import exh.merged.sql.models.MergedMangaReference import exh.util.asFlow import exh.util.await -import exh.util.awaitSingle import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.buffer -import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.flatMapMerge +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.take -import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.single +import kotlinx.coroutines.flow.singleOrNull import kotlinx.coroutines.withContext import okhttp3.Response -import rx.Observable import uy.kohesive.injekt.injectLazy -// TODO LocalSource compatibility -// TODO Disable clear database option class MergedSource : SuspendHttpSource() { private val db: DatabaseHelper by injectLazy() private val sourceManager: SourceManager by injectLazy() - private val gson: Gson by injectLazy() + private val downloadManager: DownloadManager by injectLazy() + private val preferences: PreferencesHelper by injectLazy() override val id: Long = MERGED_SOURCE_ID @@ -49,150 +50,140 @@ class MergedSource : SuspendHttpSource() { override suspend fun searchMangaParseSuspended(response: Response) = throw UnsupportedOperationException() override suspend fun latestUpdatesRequestSuspended(page: Int) = throw UnsupportedOperationException() override suspend fun latestUpdatesParseSuspended(response: Response) = throw UnsupportedOperationException() + override suspend fun mangaDetailsParseSuspended(response: Response) = throw UnsupportedOperationException() + override suspend fun chapterListParseSuspended(response: Response) = throw UnsupportedOperationException() + override suspend fun pageListParseSuspended(response: Response) = throw UnsupportedOperationException() + override suspend fun imageUrlParseSuspended(response: Response) = throw UnsupportedOperationException() + override fun fetchChapterListFlow(manga: SManga) = throw UnsupportedOperationException() + override fun fetchImageFlow(page: Page) = throw UnsupportedOperationException() + override fun fetchImageUrlFlow(page: Page) = throw UnsupportedOperationException() + override fun fetchPageListFlow(chapter: SChapter) = throw UnsupportedOperationException() + override fun fetchLatestUpdatesFlow(page: Int) = throw UnsupportedOperationException() + override fun fetchPopularMangaFlow(page: Int) = throw UnsupportedOperationException() - override suspend fun fetchMangaDetailsSuspended(manga: SManga): SManga { - return readMangaConfig(manga).load(db, sourceManager).take(1).map { loaded -> - SManga.create().apply { - this.copyFrom(loaded.manga) - url = manga.url - } - }.first() + override fun fetchMangaDetailsFlow(manga: SManga): Flow { + return flow { + val mergedManga = db.getManga(manga.url, id).await() ?: throw Exception("merged manga not in db") + val mangaReferences = mergedManga.id?.let { withContext(Dispatchers.IO) { db.getMergedMangaReferences(it).await() } } ?: throw Exception("merged manga id is null") + if (mangaReferences.isEmpty()) throw IllegalArgumentException("Manga references are empty, info unavailable, merge is likely corrupted") + if (mangaReferences.size == 1 || { + val mangaReference = mangaReferences.firstOrNull() + mangaReference == null || (mangaReference.mangaSourceId == MERGED_SOURCE_ID) + }() + ) throw IllegalArgumentException("Manga references contain only the merged reference, merge is likely corrupted") + + emit( + SManga.create().apply { + val mangaInfoReference = mangaReferences.firstOrNull { it.isInfoManga } ?: mangaReferences.firstOrNull { it.mangaId != it.mergeId } + val dbManga = mangaInfoReference?.let { withContext(Dispatchers.IO) { db.getManga(it.mangaUrl, it.mangaSourceId).await() } } + this.copyFrom(dbManga ?: mergedManga) + url = manga.url + } + ) + } } - override suspend fun fetchChapterListSuspended(manga: SManga): List { - val loadedMangas = readMangaConfig(manga).load(db, sourceManager).buffer() - return loadedMangas.flatMapMerge { loadedManga -> - withContext(Dispatchers.IO) { - loadedManga.source.fetchChapterList(loadedManga.manga).asFlow().map { chapterList -> - chapterList.map { chapter -> - chapter.apply { - url = writeUrlConfig( - UrlConfig( - loadedManga.source.id, - url, - loadedManga.manga.url - ) - ) + fun getChaptersFromDB(manga: Manga, editScanlators: Boolean = false, dedupe: Boolean = true): Flow> { + // TODO more chapter dedupe + return db.getChaptersByMergedMangaId(manga.id!!).asRxObservable() + .asFlow() + .map { chapterList -> + val mangaReferences = withContext(Dispatchers.IO) { db.getMergedMangaReferences(manga.id!!).await() } + val sources = mangaReferences.map { sourceManager.getOrStub(it.mangaSourceId) to it.mangaId } + if (editScanlators) { + chapterList.onEach { chapter -> + val source = sources.firstOrNull { chapter.manga_id == it.second }?.first + if (source != null) { + chapter.scanlator = if (chapter.scanlator.isNullOrBlank()) source.name + else "$source: ${chapter.scanlator}" } } } + if (dedupe) dedupeChapterList(mangaReferences, chapterList) else chapterList } - }.buffer().toList().flatten() } - override suspend fun mangaDetailsParseSuspended(response: Response) = throw UnsupportedOperationException() - override suspend fun chapterListParseSuspended(response: Response) = throw UnsupportedOperationException() - - override suspend fun fetchPageListSuspended(chapter: SChapter): List { - val config = readUrlConfig(chapter.url) - val source = sourceManager.getOrStub(config.source) - return source.fetchPageList( - SChapter.create().apply { - copyFrom(chapter) - url = config.url + private fun dedupeChapterList(mangaReferences: List, chapterList: List): List { + return when (mangaReferences.firstOrNull { it.mangaSourceId == MERGED_SOURCE_ID }?.chapterSortMode) { + MergedMangaReference.CHAPTER_SORT_NO_DEDUPE, MergedMangaReference.CHAPTER_SORT_NONE -> chapterList + MergedMangaReference.CHAPTER_SORT_PRIORITY -> chapterList + MergedMangaReference.CHAPTER_SORT_MOST_CHAPTERS -> { + findSourceWithMostChapters(chapterList)?.let { mangaId -> + chapterList.filter { it.manga_id == mangaId } + } ?: chapterList } - ).map { pages -> - pages.map { page -> - page.copyWithUrl(writeUrlConfig(UrlConfig(config.source, page.url, config.mangaUrl))) + MergedMangaReference.CHAPTER_SORT_HIGHEST_CHAPTER_NUMBER -> { + findSourceWithHighestChapterNumber(chapterList)?.let { mangaId -> + chapterList.filter { it.manga_id == mangaId } + } ?: chapterList } - }.awaitSingle() - } - - override suspend fun fetchImageUrlSuspended(page: Page): String { - val config = readUrlConfig(page.url) - val source = sourceManager.getOrStub(config.source) as? HttpSource ?: throw UnsupportedOperationException("This source does not support this operation!") - return source.fetchImageUrl(page.copyWithUrl(config.url)).awaitSingle() - } - - override suspend fun pageListParseSuspended(response: Response) = throw UnsupportedOperationException() - override suspend fun imageUrlParseSuspended(response: Response) = throw UnsupportedOperationException() - - override fun fetchImage(page: Page): Observable { - val config = readUrlConfig(page.url) - val source = sourceManager.getOrStub(config.source) as? HttpSource - ?: throw UnsupportedOperationException("This source does not support this operation!") - return source.fetchImage(page.copyWithUrl(config.url)) - } - - override suspend fun prepareNewChapterSuspended(chapter: SChapter, manga: SManga) { - val chapterConfig = readUrlConfig(chapter.url) - val source = sourceManager.getOrStub(chapterConfig.source) as? HttpSource ?: throw UnsupportedOperationException("This source does not support this operation!") - val copiedManga = SManga.create().apply { - this.copyFrom(manga) - url = chapterConfig.mangaUrl - } - chapter.url = chapterConfig.url - source.prepareNewChapter(chapter, copiedManga) - chapter.url = writeUrlConfig(UrlConfig(source.id, chapter.url, chapterConfig.mangaUrl)) - chapter.scanlator = if (chapter.scanlator.isNullOrBlank()) source.name - else "$source: ${chapter.scanlator}" - } - - fun readMangaConfig(manga: SManga): MangaConfig { - return MangaConfig.readFromUrl(gson, manga.url) - } - - fun readUrlConfig(url: String): UrlConfig { - return gson.fromJson(url) - } - - fun writeUrlConfig(urlConfig: UrlConfig): String { - return gson.toJson(urlConfig) - } - - data class LoadedMangaSource(val source: Source, val manga: Manga) - data class MangaSource( - @SerializedName("s") - val source: Long, - @SerializedName("u") - val url: String - ) { - suspend fun load(db: DatabaseHelper, sourceManager: SourceManager): LoadedMangaSource? { - val manga = db.getManga(url, source).await() ?: return null - val source = sourceManager.getOrStub(source) - return LoadedMangaSource(source, manga) + else -> chapterList } } - data class MangaConfig( - @SerializedName("c") - val children: List - ) { - fun load(db: DatabaseHelper, sourceManager: SourceManager): Flow { - return children.asFlow().map { mangaSource -> - mangaSource.load(db, sourceManager) ?: run { - XLog.w("> Missing source manga: $mangaSource") - throw IllegalStateException("Missing source manga: $mangaSource") + private fun findSourceWithMostChapters(chapterList: List): Long? { + return chapterList.groupBy { it.manga_id }.maxByOrNull { it.value.size }?.key + } + + private fun findSourceWithHighestChapterNumber(chapterList: List): Long? { + return chapterList.maxByOrNull { it.chapter_number }?.manga_id + } + + fun fetchChaptersForMergedManga(manga: Manga, downloadChapters: Boolean = true, editScanlators: Boolean = false, dedupe: Boolean = true): Flow> { + return flow { + withContext(Dispatchers.IO) { + fetchChaptersAndSync(manga, downloadChapters).collect() + } + emit( + getChaptersFromDB(manga, editScanlators, dedupe).singleOrNull() ?: emptyList() + ) + } + } + + suspend fun fetchChaptersAndSync(manga: Manga, downloadChapters: Boolean = true): Flow, List>> { + val mangaReferences = db.getMergedMangaReferences(manga.id!!).await() + if (mangaReferences.isEmpty()) throw IllegalArgumentException("Manga references are empty, chapters unavailable, merge is likely corrupted") + + val ifDownloadNewChapters = downloadChapters && manga.shouldDownloadNewChapters(db, preferences) + return mangaReferences.filter { it.mangaSourceId != MERGED_SOURCE_ID }.asFlow().map { + load(db, sourceManager, it) + }.buffer().flatMapMerge { loadedManga -> + withContext(Dispatchers.IO) { + if (loadedManga.manga != null && loadedManga.reference.getChapterUpdates) { + loadedManga.source.fetchChapterList(loadedManga.manga).asFlow() + .map { syncChaptersWithSource(db, it, loadedManga.manga, loadedManga.source) } + .onEach { + if (ifDownloadNewChapters && loadedManga.reference.downloadChapters) { + downloadManager.downloadChapters(loadedManga.manga, it.first) + } + } + } else { + emptyList, List>>().asFlow() } } - } - - fun writeAsUrl(gson: Gson): String { - return gson.toJson(this) - } - - companion object { - fun readFromUrl(gson: Gson, url: String): MangaConfig { - return gson.fromJson(url) - } - } + }.buffer() } - data class UrlConfig( - @SerializedName("s") - val source: Long, - @SerializedName("u") - val url: String, - @SerializedName("m") - val mangaUrl: String - ) + suspend fun load(db: DatabaseHelper, sourceManager: SourceManager, reference: MergedMangaReference): LoadedMangaSource { + var manga = db.getManga(reference.mangaUrl, reference.mangaSourceId).await() + val source = sourceManager.getOrStub(manga?.source ?: reference.mangaSourceId) + if (manga == null) { + manga = Manga.create(reference.mangaSourceId).apply { + url = reference.mangaUrl + } + manga.copyFrom(source.fetchMangaDetails(manga).asFlow().single()) + try { + manga.id = db.insertManga(manga).await().insertedId() + reference.mangaId = manga.id + db.insertNewMergedMangaId(reference).await() + } catch (e: Exception) { + XLog.st(e.stackTrace.contentToString(), 5) + } + } + return LoadedMangaSource(source, manga, reference) + } - fun Page.copyWithUrl(newUrl: String) = Page( - index, - newUrl, - imageUrl, - uri - ) + data class LoadedMangaSource(val source: Source, val manga: Manga?, val reference: MergedMangaReference) override val lang = "all" override val supportsLatest = false diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcessHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcessHolder.kt index 05fec10c9..d02b73615 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcessHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcessHolder.kt @@ -5,7 +5,6 @@ import android.widget.PopupMenu import androidx.core.view.isInvisible import androidx.core.view.isVisible import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.google.gson.Gson import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Manga @@ -13,7 +12,6 @@ import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.data.glide.toMangaThumbnail import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager -import eu.kanade.tachiyomi.source.online.all.MergedSource import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import eu.kanade.tachiyomi.ui.manga.MangaController @@ -21,6 +19,7 @@ import eu.kanade.tachiyomi.util.lang.launchUI import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.view.setVectorCompat import exh.MERGED_SOURCE_ID +import exh.util.await import java.text.DecimalFormat import kotlinx.android.synthetic.main.migration_manga_card.view.gradient import kotlinx.android.synthetic.main.migration_manga_card.view.loading_group @@ -51,7 +50,6 @@ class MigrationProcessHolder( private val db: DatabaseHelper by injectLazy() private val sourceManager: SourceManager by injectLazy() private var item: MigrationProcessItem? = null - private val gson: Gson by injectLazy() private val scope = CoroutineScope(Job() + Dispatchers.Main) init { @@ -154,7 +152,7 @@ class MigrationProcessHolder( migration_manga_card_to.clicks() } - private fun View.attachManga(manga: Manga, source: Source) { + private suspend fun View.attachManga(manga: Manga, source: Source) { loading_group.isVisible = false GlideApp.with(view.context.applicationContext) .load(manga.toMangaThumbnail()) @@ -171,8 +169,8 @@ class MigrationProcessHolder( gradient.isVisible = true manga_source_label.text = if (source.id == MERGED_SOURCE_ID) { - MergedSource.MangaConfig.readFromUrl(gson, manga.url).children.map { - sourceManager.getOrStub(it.source).toString() + db.getMergedMangaReferences(manga.id!!).await().map { + sourceManager.getOrStub(it.mangaSourceId).toString() }.distinct().joinToString() } else { source.toString() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt index 4b81b9808..64592b9d9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt @@ -441,6 +441,7 @@ class SourceController(bundle: Bundle? = null) : companion object { const val SMART_SEARCH_CONFIG = "SMART_SEARCH_CONFIG" + const val SMART_SEARCH_SOURCE_TAG = "smart_search_source_tag" } // SY <-- } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt index afc31cc76..facac6ec3 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt @@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.source.model.Filter.TriState.Companion.STATE_IGNORE import eu.kanade.tachiyomi.source.model.Filter.TriState.Companion.STATE_INCLUDE import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.source.online.all.MergedSource import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.util.isLocal import eu.kanade.tachiyomi.util.lang.combineLatest @@ -27,11 +28,15 @@ import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.removeCovers import exh.EH_SOURCE_ID import exh.EXH_SOURCE_ID +import exh.MERGED_SOURCE_ID import exh.favorites.FavoritesSyncHelper +import exh.util.await import exh.util.isLewd import exh.util.nullIfBlank import java.util.Collections import java.util.Comparator +import kotlinx.coroutines.flow.singleOrNull +import kotlinx.coroutines.runBlocking import rx.Observable import rx.Subscription import rx.android.schedulers.AndroidSchedulers @@ -241,7 +246,10 @@ class LibraryPresenter( for ((_, itemList) in map) { for (item in itemList) { item.downloadCount = if (showDownloadBadges) { - downloadManager.getDownloadCount(item.manga) + // SY --> + if (item.manga.source == MERGED_SOURCE_ID) { + item.manga.id?.let { mergeMangaId -> db.getMergedMangas(mergeMangaId).executeAsBlocking().map { downloadManager.getDownloadCount(it) }.sum() } ?: 0 + } else /* SY <-- */ downloadManager.getDownloadCount(item.manga) } else { // Unset download count if not enabled -1 @@ -455,8 +463,10 @@ class LibraryPresenter( mangas.forEach { manga -> launchIO { /* SY --> */ val chapters = if (manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID) { - val chapter = db.getChapters(manga).executeAsBlocking().minByOrNull { it.source_order } + val chapter = db.getChapters(manga).await().minByOrNull { it.source_order } if (chapter != null && !chapter.read) listOf(chapter) else emptyList() + } else if (manga.source == MERGED_SOURCE_ID) { + (sourceManager.getOrStub(MERGED_SOURCE_ID) as? MergedSource)?.getChaptersFromDB(manga)?.singleOrNull()?.filter { !it.read } ?: emptyList() } else /* SY <-- */ db.getChapters(manga).executeAsBlocking() .filter { !it.read } @@ -501,7 +511,7 @@ class LibraryPresenter( fun markReadStatus(mangas: List, read: Boolean) { mangas.forEach { manga -> launchIO { - val chapters = db.getChapters(manga).executeAsBlocking() + val chapters = if (manga.source == MERGED_SOURCE_ID) (sourceManager.get(MERGED_SOURCE_ID) as? MergedSource)?.getChaptersFromDB(manga)?.singleOrNull() ?: emptyList() else db.getChapters(manga).executeAsBlocking() chapters.forEach { it.read = read if (!read) { @@ -519,7 +529,16 @@ class LibraryPresenter( private fun deleteChapters(manga: Manga, chapters: List) { sourceManager.get(manga.source)?.let { source -> - downloadManager.deleteChapters(chapters, manga, source) + // SY --> + if (source is MergedSource) { + val mergedMangas = db.getMergedMangas(manga.id!!).executeAsBlocking() + val sources = mergedMangas.distinctBy { it.source }.map { sourceManager.getOrStub(it.source) } + chapters.groupBy { it.manga_id }.forEach { map -> + val mergedManga = mergedMangas.firstOrNull { it.id == map.key } ?: return@forEach + val mergedMangaSource = sources.firstOrNull { it.id == mergedManga.source } ?: return@forEach + downloadManager.deleteChapters(map.value, mergedManga, mergedMangaSource) + } + } else /* SY <-- */ downloadManager.deleteChapters(chapters, manga, source) } } @@ -543,7 +562,14 @@ class LibraryPresenter( mangaToDelete.forEach { manga -> val source = sourceManager.get(manga.source) as? HttpSource if (source != null) { - downloadManager.deleteManga(manga, source) + if (source is MergedSource) { + val mergedMangas = db.getMergedMangas(manga.id!!).await() + val sources = mergedMangas.distinctBy { it.source }.map { sourceManager.getOrStub(it.source) } + mergedMangas.forEach merge@{ mergedManga -> + val mergedSource = sources.firstOrNull { mergedManga.source == it.id } ?: return@merge + downloadManager.deleteManga(mergedManga, mergedSource) + } + } else downloadManager.deleteManga(manga, source) } } } @@ -571,7 +597,7 @@ class LibraryPresenter( // SY --> /** Returns first unread chapter of a manga */ fun getFirstUnread(manga: Manga): Chapter? { - val chapters = db.getChapters(manga).executeAsBlocking() + val chapters = (if (manga.source == MERGED_SOURCE_ID) (sourceManager.get(MERGED_SOURCE_ID) as? MergedSource).let { runBlocking { it?.getChaptersFromDB(manga)?.singleOrNull() } ?: emptyList() } else db.getChapters(manga).executeAsBlocking()) return if (manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID) { val chapter = chapters.sortedBy { it.source_order }.getOrNull(0) if (chapter?.read == false) chapter else null diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt index d94fb044f..775dc0739 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt @@ -54,9 +54,11 @@ import eu.kanade.tachiyomi.source.online.MetadataSource.Companion.getMetadataSou import eu.kanade.tachiyomi.ui.base.controller.FabController import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.ToolbarLiftOnScrollController +import eu.kanade.tachiyomi.ui.base.controller.popControllerWithTag import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController import eu.kanade.tachiyomi.ui.browse.source.SourceController +import eu.kanade.tachiyomi.ui.browse.source.SourceController.Companion.SMART_SEARCH_SOURCE_TAG import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog @@ -74,6 +76,7 @@ import eu.kanade.tachiyomi.ui.manga.chapter.MangaChaptersHeaderAdapter import eu.kanade.tachiyomi.ui.manga.info.MangaInfoButtonsAdapter import eu.kanade.tachiyomi.ui.manga.info.MangaInfoHeaderAdapter import eu.kanade.tachiyomi.ui.manga.info.MangaInfoItemAdapter +import eu.kanade.tachiyomi.ui.manga.merged.EditMergedSettingsDialog import eu.kanade.tachiyomi.ui.manga.track.TrackController import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.recent.history.HistoryController @@ -85,6 +88,7 @@ import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.view.getCoordinates import eu.kanade.tachiyomi.util.view.shrinkOnScroll import eu.kanade.tachiyomi.util.view.snack +import exh.MERGED_SOURCE_ID import exh.isEhBasedSource import exh.metadata.metadata.base.FlatMetadata import java.io.IOException @@ -195,6 +199,8 @@ class MangaController : private var editMangaDialog: EditMangaDialog? = null + private var editMergedSettingsDialog: EditMergedSettingsDialog? = null + private var currentAnimator: Animator? = null // EXH <-- @@ -423,6 +429,8 @@ class MangaController : // SY --> if (presenter.manga.favorite) menu.findItem(R.id.action_edit).isVisible = true if (preferences.recommendsInOverflow().get()) menu.findItem(R.id.action_recommend).isVisible = true + menu.findItem(R.id.action_merged).isVisible = presenter.manga.source == MERGED_SOURCE_ID + menu.findItem(R.id.action_toggle_dedupe).isVisible = false // presenter.manga.source == MERGED_SOURCE_ID // SY <-- } @@ -443,6 +451,16 @@ class MangaController : R.id.action_recommend -> { openRecommends() } + R.id.action_merged -> { + editMergedSettingsDialog = EditMergedSettingsDialog( + this, presenter.manga + ) + editMergedSettingsDialog?.showDialog(router) + } + R.id.action_toggle_dedupe -> { + presenter.dedupe = !presenter.dedupe + presenter.toggleDedupe() + } // SY <-- R.id.action_edit_categories -> onCategoriesClick() @@ -633,7 +651,7 @@ class MangaController : Bundle().apply { putParcelable(SourceController.SMART_SEARCH_CONFIG, smartSearchConfig) } - ).withFadeTransaction() + ).withFadeTransaction().tag(SMART_SEARCH_SOURCE_TAG) ) } @@ -643,7 +661,9 @@ class MangaController : presenter.smartSearchMerge(presenter.manga, smartSearchConfig?.origMangaId!!) } - router?.pushController( + router?.popControllerWithTag(SMART_SEARCH_SOURCE_TAG) + router?.popCurrentController() + router?.replaceTopController( MangaController( mergedManga, true, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt index 8f66fce47..2c88ca3b6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt @@ -4,7 +4,6 @@ import android.content.Context import android.net.Uri import android.os.Bundle import com.elvishew.xlog.XLog -import com.google.gson.Gson import com.jakewharton.rxrelay.BehaviorRelay import com.jakewharton.rxrelay.PublishRelay import eu.kanade.tachiyomi.data.cache.CoverCache @@ -20,6 +19,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.online.MetadataSource import eu.kanade.tachiyomi.source.online.MetadataSource.Companion.isMetadataSource import eu.kanade.tachiyomi.source.online.all.MergedSource @@ -37,10 +37,12 @@ import exh.MERGED_SOURCE_ID import exh.debug.DebugToggles import exh.eh.EHentaiUpdateHelper import exh.isEhBasedSource +import exh.merged.sql.models.MergedMangaReference import exh.metadata.metadata.base.FlatMetadata import exh.metadata.metadata.base.RaisedSearchMetadata import exh.metadata.metadata.base.getFlatMetadataForManga import exh.source.EnhancedHttpSource +import exh.util.asObservable import exh.util.await import exh.util.trimOrNull import java.util.Date @@ -63,9 +65,7 @@ class MangaPresenter( private val trackManager: TrackManager = Injekt.get(), private val downloadManager: DownloadManager = Injekt.get(), private val coverCache: CoverCache = Injekt.get(), - // SY --> - private val gson: Gson = Injekt.get() - // SY <-- + private val sourceManager: SourceManager = Injekt.get() ) : BasePresenter() { /** @@ -112,6 +112,10 @@ class MangaPresenter( data class EXHRedirect(val manga: Manga, val update: Boolean) var meta: RaisedSearchMetadata? = null + + private var mergedManga = emptyList() + + var dedupe: Boolean = true // EXH <-- override fun onCreate(savedState: Bundle?) { @@ -121,6 +125,10 @@ class MangaPresenter( if (manga.initialized && source.isMetadataSource()) { getMangaMetaObservable().subscribeLatestCache({ view, flatMetadata -> if (flatMetadata != null) view.onNextMetaInfo(flatMetadata) else XLog.d("Invalid metadata") }) } + + if (source is MergedSource) { + launchIO { mergedManga = db.getMergedMangas(manga.id!!).await() } + } // SY <-- // Manga info - start @@ -145,7 +153,7 @@ class MangaPresenter( // Add the subscription that retrieves the chapters from the database, keeps subscribed to // changes, and sends the list of chapters to the relay. add( - db.getChapters(manga).asRxObservable() + (/* SY --> */if (source is MergedSource) source.getChaptersFromDB(manga, true, dedupe).asObservable() else /* SY <-- */ db.getChapters(manga).asRxObservable()) .map { chapters -> // Convert every chapter to a model. chapters.map { it.toModel() } @@ -334,64 +342,138 @@ class MangaPresenter( } suspend fun smartSearchMerge(manga: Manga, originalMangaId: Long): Manga { - val originalManga = db.getManga(originalMangaId).await() - ?: throw IllegalArgumentException("Unknown manga ID: $originalMangaId") - val toInsert = if (originalManga.source == MERGED_SOURCE_ID) { - originalManga.apply { - val originalChildren = MergedSource.MangaConfig.readFromUrl(gson, url).children - if (originalChildren.any { it.source == manga.source && it.url == manga.url }) { - throw IllegalArgumentException("This manga is already merged with the current manga!") - } - - url = MergedSource.MangaConfig( - originalChildren + MergedSource.MangaSource( - manga.source, - manga.url - ) - ).writeAsUrl(gson) + val originalManga = db.getManga(originalMangaId).await() ?: throw IllegalArgumentException("Unknown manga ID: $originalMangaId") + if (originalManga.source == MERGED_SOURCE_ID) { + val children = db.getMergedMangaReferences(originalMangaId).await() + if (children.any { it.mangaSourceId == manga.source && it.mangaUrl == manga.url }) { + throw IllegalArgumentException("This manga is already merged with the current manga!") } - } else { - val newMangaConfig = MergedSource.MangaConfig( - listOf( - MergedSource.MangaSource( - originalManga.source, - originalManga.url - ), - MergedSource.MangaSource( - manga.source, - manga.url - ) + + val mangaReferences = mutableListOf( + MergedMangaReference( + id = null, + isInfoManga = false, + getChapterUpdates = true, + chapterSortMode = 0, + chapterPriority = 0, + downloadChapters = true, + mergeId = originalManga.id!!, + mergeUrl = originalManga.url, + mangaId = manga.id!!, + mangaUrl = manga.url, + mangaSourceId = manga.source ) ) - Manga.create(newMangaConfig.writeAsUrl(gson), originalManga.title, MERGED_SOURCE_ID).apply { + + if (children.isEmpty() || children.all { it.mangaSourceId != MERGED_SOURCE_ID }) { + mangaReferences += MergedMangaReference( + id = null, + isInfoManga = false, + getChapterUpdates = false, + chapterSortMode = 0, + chapterPriority = -1, + downloadChapters = false, + mergeId = originalManga.id!!, + mergeUrl = originalManga.url, + mangaId = originalManga.id!!, + mangaUrl = originalManga.url, + mangaSourceId = MERGED_SOURCE_ID + ) + } + + db.insertMergedMangas(mangaReferences).await() + + return originalManga + } else { + val mergedManga = Manga.create(originalManga.url, originalManga.title, MERGED_SOURCE_ID).apply { copyFrom(originalManga) favorite = true last_update = originalManga.last_update viewer = originalManga.viewer chapter_flags = originalManga.chapter_flags sorting = Manga.SORTING_NUMBER + date_added = Date().time } + var existingManga = db.getManga(mergedManga.url, mergedManga.source).await() + while (existingManga != null) { + if (existingManga.favorite) { + throw IllegalArgumentException("This merged manga is a duplicate!") + } else if (!existingManga.favorite) { + withContext(NonCancellable) { + db.deleteManga(existingManga!!).await() + db.deleteMangaForMergedManga(existingManga!!.id!!).await() + } + } + existingManga = db.getManga(mergedManga.url, mergedManga.source).await() + } + + // Reload chapters immediately + mergedManga.initialized = false + + val newId = db.insertManga(mergedManga).await().insertedId() + if (newId != null) mergedManga.id = newId + + val originalMangaReference = MergedMangaReference( + id = null, + isInfoManga = true, + getChapterUpdates = true, + chapterSortMode = 0, + chapterPriority = 0, + downloadChapters = true, + mergeId = mergedManga.id!!, + mergeUrl = mergedManga.url, + mangaId = originalManga.id!!, + mangaUrl = originalManga.url, + mangaSourceId = originalManga.source + ) + + val newMangaReference = MergedMangaReference( + id = null, + isInfoManga = false, + getChapterUpdates = true, + chapterSortMode = 0, + chapterPriority = 0, + downloadChapters = true, + mergeId = mergedManga.id!!, + mergeUrl = mergedManga.url, + mangaId = manga.id!!, + mangaUrl = manga.url, + mangaSourceId = manga.source + ) + + val mergedMangaReference = MergedMangaReference( + id = null, + isInfoManga = false, + getChapterUpdates = false, + chapterSortMode = 0, + chapterPriority = -1, + downloadChapters = false, + mergeId = mergedManga.id!!, + mergeUrl = mergedManga.url, + mangaId = mergedManga.id!!, + mangaUrl = mergedManga.url, + mangaSourceId = MERGED_SOURCE_ID + ) + + db.insertMergedMangas(listOf(originalMangaReference, newMangaReference, mergedMangaReference)).await() + + return mergedManga } // Note that if the manga are merged in a different order, this won't trigger, but I don't care lol - val existingManga = db.getManga(toInsert.url, toInsert.source).await() - if (existingManga != null) { - withContext(NonCancellable) { - if (toInsert.id != null) { - db.deleteManga(toInsert).await() - } + } + + fun updateMergeSettings(mergeReference: MergedMangaReference?, mergedMangaReferences: List) { + launchIO { + mergeReference?.let { + db.updateMergeMangaSettings(it).await() } - - return existingManga + if (mergedMangaReferences.isNotEmpty()) db.updateMergedMangaSettings(mergedMangaReferences).await() } + } - // Reload chapters immediately - toInsert.initialized = false - - val newId = db.insertManga(toInsert).await().insertedId() - if (newId != null) toInsert.id = newId - - return toInsert + fun toggleDedupe() { + // I cant find any way to call the chapter list subscription to get the chapters again } // SY <-- @@ -424,7 +506,13 @@ class MangaPresenter( * Deletes all the downloads for the manga. */ fun deleteDownloads() { - downloadManager.deleteManga(manga, source) + // SY --> + if (source is MergedSource) { + val mergedManga = mergedManga.map { it to sourceManager.getOrStub(it.source) } + mergedManga.forEach { (manga, source) -> + downloadManager.deleteManga(manga, source) + } + } else /* SY <-- */ downloadManager.deleteManga(manga, source) } /** @@ -515,10 +603,14 @@ class MangaPresenter( // Chapters list - start private fun observeDownloads() { + // SY --> + val isMergedSource = source is MergedSource + val mergedIds = if (isMergedSource) mergedManga.mapNotNull { it.id } else emptyList() + // SY <-- observeDownloadsSubscription?.let { remove(it) } observeDownloadsSubscription = downloadManager.queue.getStatusObservable() .observeOn(AndroidSchedulers.mainThread()) - .filter { download -> download.manga.id == manga.id } + .filter { download -> /* SY --> */ if (isMergedSource) download.manga.id in mergedIds else /* SY <-- */ download.manga.id == manga.id } .doOnNext { onDownloadStatusChange(it) } .subscribeLatestCache(MangaController::onChapterStatusChange) { _, error -> Timber.e(error) @@ -548,8 +640,11 @@ class MangaPresenter( * @param chapters the list of chapter from the database. */ private fun setDownloadedChapters(chapters: List) { + // SY --> + val isMergedSource = source is MergedSource + // SY <-- chapters - .filter { downloadManager.isChapterDownloaded(it, manga) } + .filter { downloadManager.isChapterDownloaded(it, /* SY --> */ if (isMergedSource) mergedManga.firstOrNull { manga -> it.manga_id == manga.id } ?: manga else /* SY <-- */ manga) } .forEach { it.status = Download.DOWNLOADED } } @@ -560,21 +655,38 @@ class MangaPresenter( hasRequested = true if (!fetchChaptersSubscription.isNullOrUnsubscribed()) return - fetchChaptersSubscription = Observable.defer { source.fetchChapterList(manga) } - .subscribeOn(Schedulers.io()) - .map { syncChaptersWithSource(db, it, manga, source) } - .doOnNext { - if (manualFetch) { - downloadNewChapters(it.first) + fetchChaptersSubscription = /* SY --> */ if (source !is MergedSource) { + // SY <-- + Observable.defer { source.fetchChapterList(manga) } + .subscribeOn(Schedulers.io()) + .map { syncChaptersWithSource(db, it, manga, source) } + .doOnNext { + if (manualFetch) { + downloadNewChapters(it.first) + } } - } - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst( - { view, _ -> - view.onFetchChaptersDone() - }, - MangaController::onFetchChaptersError - ) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeFirst( + { view, _ -> + view.onFetchChaptersDone() + }, + MangaController::onFetchChaptersError + ) + // SY --> + } else { + Observable.defer { source.fetchChaptersForMergedManga(manga, manualFetch, true, dedupe).asObservable() } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnNext { + } + .subscribeFirst( + { view, _ -> + view.onFetchChaptersDone() + }, + MangaController::onFetchChaptersError + ) + } + // SY <-- } /** @@ -680,7 +792,13 @@ class MangaPresenter( * @param chapters the list of chapters to download. */ fun downloadChapters(chapters: List) { - downloadManager.downloadChapters(manga, chapters) + // SY --> + if (source is MergedSource) { + chapters.groupBy { it.manga_id }.forEach { map -> + val manga = mergedManga.firstOrNull { it.id == map.key } ?: return@forEach + downloadManager.downloadChapters(manga, map.value) + } + } else /* SY <-- */ downloadManager.downloadChapters(manga, chapters) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt index acc5c79f5..d1be9fa53 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt @@ -8,6 +8,7 @@ import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.load.engine.DiskCacheStrategy import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.data.glide.MangaThumbnail @@ -18,7 +19,6 @@ import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.HttpSource -import eu.kanade.tachiyomi.source.online.all.MergedSource import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.view.setTooltip @@ -41,6 +41,10 @@ class MangaInfoHeaderAdapter( RecyclerView.Adapter() { private val trackManager: TrackManager by injectLazy() + // SY --> + private val db: DatabaseHelper by injectLazy() + private val sourceManager: SourceManager by injectLazy() + // SY <-- private var manga: Manga = controller.presenter.manga private var source: Source = controller.presenter.source @@ -254,9 +258,9 @@ class MangaInfoHeaderAdapter( val mangaSource = source?.toString() with(binding.mangaSource) { // SY --> - if (source != null && source.id == MERGED_SOURCE_ID) { - text = MergedSource.MangaConfig.readFromUrl(Injekt.get(), manga.url).children.map { - Injekt.get().getOrStub(it.source).toString() + if (source?.id == MERGED_SOURCE_ID) { + text = db.getMergedMangaReferences(manga.id!!).executeAsBlocking().map { + sourceManager.getOrStub(it.mangaSourceId).toString() }.distinct().joinToString() } else /* SY <-- */ if (mangaSource != null) { text = mangaSource diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/merged/EditMergedMangaAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/merged/EditMergedMangaAdapter.kt new file mode 100644 index 000000000..be5031832 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/merged/EditMergedMangaAdapter.kt @@ -0,0 +1,36 @@ +package eu.kanade.tachiyomi.ui.manga.merged + +import eu.davidea.flexibleadapter.FlexibleAdapter + +/** + * Adapter storing a list of merged manga. + * + * @param controller the context of the fragment containing this adapter. + * @param isPriorityOrder if deduplication mode is based on priority + */ +class EditMergedMangaAdapter(controller: EditMergedSettingsDialog, var isPriorityOrder: Boolean) : + FlexibleAdapter(null, controller, true), + EditMergedSettingsHeaderAdapter.SortingListener { + + /** + * Listener called when an item of the list is released. + */ + val editMergedMangaItemListener: EditMergedMangaItemListener = controller + + interface EditMergedMangaItemListener { + fun onItemReleased(position: Int) + fun onDeleteClick(position: Int) + fun onToggleChapterUpdatesClicked(position: Int) + fun onToggleChapterDownloadsClicked(position: Int) + } + + override fun onSetPrioritySort(isPriorityOrder: Boolean) { + isHandleDragEnabled = isPriorityOrder + this.isPriorityOrder = isPriorityOrder + allBoundViewHolders.onEach { editMergedMangaHolder -> + if (editMergedMangaHolder is EditMergedMangaHolder) { + editMergedMangaHolder.setHandelAlpha(isPriorityOrder) + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/merged/EditMergedMangaHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/merged/EditMergedMangaHolder.kt new file mode 100644 index 000000000..9d2ae9c19 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/merged/EditMergedMangaHolder.kt @@ -0,0 +1,83 @@ +package eu.kanade.tachiyomi.ui.manga.merged + +import android.view.View +import com.bumptech.glide.load.engine.DiskCacheStrategy +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.glide.GlideApp +import eu.kanade.tachiyomi.data.glide.toMangaThumbnail +import eu.kanade.tachiyomi.source.SourceManager +import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder +import eu.kanade.tachiyomi.util.system.getResourceColor +import exh.merged.sql.models.MergedMangaReference +import kotlinx.android.synthetic.main.edit_merged_settings_item.cover +import kotlinx.android.synthetic.main.edit_merged_settings_item.download +import kotlinx.android.synthetic.main.edit_merged_settings_item.get_chapter_updates +import kotlinx.android.synthetic.main.edit_merged_settings_item.remove +import kotlinx.android.synthetic.main.edit_merged_settings_item.reorder +import kotlinx.android.synthetic.main.edit_merged_settings_item.subtitle +import kotlinx.android.synthetic.main.edit_merged_settings_item.title +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class EditMergedMangaHolder(view: View, val adapter: EditMergedMangaAdapter) : BaseFlexibleViewHolder(view, adapter) { + + lateinit var reference: MergedMangaReference + + init { + setDragHandleView(reorder) + remove.setOnClickListener { + adapter.editMergedMangaItemListener.onDeleteClick(bindingAdapterPosition) + } + get_chapter_updates.setOnClickListener { + adapter.editMergedMangaItemListener.onToggleChapterUpdatesClicked(bindingAdapterPosition) + } + download.setOnClickListener { + adapter.editMergedMangaItemListener.onToggleChapterDownloadsClicked(bindingAdapterPosition) + } + setHandelAlpha(adapter.isPriorityOrder) + } + + override fun onItemReleased(position: Int) { + super.onItemReleased(position) + adapter.editMergedMangaItemListener.onItemReleased(position) + } + + fun bind(item: EditMergedMangaItem) { + reference = item.mergedMangaReference + item.mergedManga?.toMangaThumbnail()?.let { + GlideApp.with(itemView.context) + .load(it) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .centerCrop() + .into(cover) + } + + title.text = Injekt.get().getOrStub(item.mergedMangaReference.mangaSourceId).toString() + subtitle.text = item.mergedManga?.title + updateDownloadChaptersIcon(item.mergedMangaReference.downloadChapters) + updateChapterUpdatesIcon(item.mergedMangaReference.getChapterUpdates) + } + + fun setHandelAlpha(isPriorityOrder: Boolean) { + reorder.alpha = when (isPriorityOrder) { + true -> 1F + false -> 0.5F + } + } + + fun updateDownloadChaptersIcon(setTint: Boolean) { + val color = if (setTint) { + itemView.context.getResourceColor(R.attr.colorAccent) + } else itemView.context.getResourceColor(R.attr.colorOnSurface) + + download.drawable.setTint(color) + } + + fun updateChapterUpdatesIcon(setTint: Boolean) { + val color = if (setTint) { + itemView.context.getResourceColor(R.attr.colorAccent) + } else itemView.context.getResourceColor(R.attr.colorOnSurface) + + get_chapter_updates.drawable.setTint(color) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/merged/EditMergedMangaItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/merged/EditMergedMangaItem.kt new file mode 100644 index 000000000..890d7cc10 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/merged/EditMergedMangaItem.kt @@ -0,0 +1,50 @@ +package eu.kanade.tachiyomi.ui.manga.merged + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.databinding.EditMergedSettingsItemBinding +import exh.merged.sql.models.MergedMangaReference + +class EditMergedMangaItem(val mergedManga: Manga?, val mergedMangaReference: MergedMangaReference) : AbstractFlexibleItem() { + + override fun getLayoutRes(): Int { + return R.layout.edit_merged_settings_item + } + + override fun isDraggable(): Boolean { + return true + } + + lateinit var binding: EditMergedSettingsItemBinding + + override fun createViewHolder(view: View, adapter: FlexibleAdapter>): EditMergedMangaHolder { + binding = EditMergedSettingsItemBinding.bind(view) + return EditMergedMangaHolder(binding.root, adapter as EditMergedMangaAdapter) + } + + override fun bindViewHolder( + adapter: FlexibleAdapter>?, + holder: EditMergedMangaHolder, + position: Int, + payloads: MutableList? + ) { + holder.bind(this) + } + + override fun hashCode(): Int { + return mergedMangaReference.id!!.hashCode() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other is EditMergedMangaItem) { + return mergedMangaReference.id!! == other.mergedMangaReference.id!! + } + return false + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/merged/EditMergedSettingsDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/merged/EditMergedSettingsDialog.kt new file mode 100644 index 000000000..12bcad2ac --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/merged/EditMergedSettingsDialog.kt @@ -0,0 +1,184 @@ +package eu.kanade.tachiyomi.ui.manga.merged + +import android.app.Dialog +import android.os.Bundle +import android.view.View +import androidx.recyclerview.widget.ConcatAdapter +import androidx.recyclerview.widget.LinearLayoutManager +import com.afollestad.materialdialogs.MaterialDialog +import com.afollestad.materialdialogs.customview.customView +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.ui.base.controller.DialogController +import eu.kanade.tachiyomi.ui.manga.MangaController +import eu.kanade.tachiyomi.util.system.toast +import exh.MERGED_SOURCE_ID +import exh.merged.sql.models.MergedMangaReference +import kotlinx.android.synthetic.main.edit_merged_settings_dialog.view.recycler +import uy.kohesive.injekt.injectLazy + +class EditMergedSettingsDialog : DialogController, EditMergedMangaAdapter.EditMergedMangaItemListener { + + private var dialogView: View? = null + + private val manga: Manga + + val mergedMangas: MutableList> = mutableListOf() + + var mergeReference: MergedMangaReference? = null + + private val db: DatabaseHelper by injectLazy() + + private val mangaController + get() = targetController as MangaController + + constructor(target: MangaController, manga: Manga) : super( + Bundle() + .apply { + putLong(KEY_MANGA, manga.id!!) + } + ) { + targetController = target + this.manga = manga + } + + @Suppress("unused") + constructor(bundle: Bundle) : super(bundle) { + manga = db.getManga(bundle.getLong(KEY_MANGA)) + .executeAsBlocking()!! + } + + private var mergedHeaderAdapter: EditMergedSettingsHeaderAdapter? = null + private var mergedMangaAdapter: EditMergedMangaAdapter? = null + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + val dialog = MaterialDialog(activity!!).apply { + customView(viewRes = R.layout.edit_merged_settings_dialog, scrollable = true) + negativeButton(android.R.string.cancel) + positiveButton(R.string.action_save) { onPositiveButtonClick() } + } + dialogView = dialog.view + onViewCreated(dialog.view) + dialog.setOnShowListener { + val dView = (it as? MaterialDialog)?.view + dView?.contentLayout?.scrollView?.scrollTo(0, 0) + } + return dialog + } + + fun onViewCreated(view: View) { + val mergedManga = db.getMergedMangas(manga.id!!).executeAsBlocking() + val mergedReferences = db.getMergedMangaReferences(manga.id!!).executeAsBlocking() + if (mergedReferences.isEmpty() || mergedReferences.size == 1) { + activity?.toast(R.string.merged_references_invalid) + router.popCurrentController() + } + mergedMangas += mergedReferences.filter { it.mangaSourceId != MERGED_SOURCE_ID }.map { reference -> mergedManga.firstOrNull { it.id == reference.mangaId } to reference } + mergeReference = mergedReferences.firstOrNull { it.mangaSourceId == MERGED_SOURCE_ID } + + val isPriorityOrder = mergeReference?.let { it.chapterSortMode == MergedMangaReference.CHAPTER_SORT_PRIORITY } ?: false + + mergedMangaAdapter = EditMergedMangaAdapter(this, isPriorityOrder) + mergedHeaderAdapter = EditMergedSettingsHeaderAdapter(this, mergedMangaAdapter!!) + + view.recycler.adapter = ConcatAdapter(mergedHeaderAdapter, mergedMangaAdapter) + view.recycler.layoutManager = LinearLayoutManager(view.context) + + mergedMangaAdapter?.isHandleDragEnabled = isPriorityOrder + + mergedMangaAdapter?.updateDataSet(mergedMangas.map { it.toModel() }) + } + + override fun onDestroyView(view: View) { + super.onDestroyView(view) + dialogView = null + } + + private fun onPositiveButtonClick() { + mangaController.presenter.updateMergeSettings(mergeReference, mergedMangas.map { it.second }) + } + + + override fun onItemReleased(position: Int) { + val mergedMangaAdapter = mergedMangaAdapter ?: return + mergedMangas.onEach { mergedManga -> + mergedManga.second.chapterPriority = mergedMangaAdapter.currentItems.indexOfFirst { + mergedManga.second.id == it.mergedMangaReference.id + } + } + } + + override fun onDeleteClick(position: Int) { + val mergedMangaAdapter = mergedMangaAdapter ?: return + val mergeMangaReference = mergedMangaAdapter.currentItems.getOrNull(position)?.mergedMangaReference ?: return + + MaterialDialog(dialogView!!.context) + .title(R.string.delete_merged_manga) + .message(R.string.delete_merged_manga_desc) + .positiveButton(android.R.string.ok) { + db.deleteMergedManga(mergeMangaReference).executeAsBlocking() + dialog?.dismiss() + mangaController.router.popController(mangaController) + } + .negativeButton(android.R.string.cancel) + .show() + } + + override fun onToggleChapterUpdatesClicked(position: Int) { + MaterialDialog(dialogView!!.context) + .title(R.string.chapter_updates_merged_manga) + .message(R.string.chapter_updates_merged_manga_desc) + .positiveButton(android.R.string.ok) { + toggleChapterUpdates(position) + } + .negativeButton(android.R.string.cancel) + .show() + } + + private fun toggleChapterUpdates(position: Int) { + val adapterReference = mergedMangaAdapter?.currentItems?.getOrNull(position)?.mergedMangaReference + mergedMangas.firstOrNull { it.second.id != null && it.second.id == adapterReference?.id }?.apply { + second.getChapterUpdates = !second.getChapterUpdates + + mergedMangaAdapter?.allBoundViewHolders?.firstOrNull { it is EditMergedMangaHolder && it.reference.id == second.id }?.let { + if (it is EditMergedMangaHolder) { + it.updateChapterUpdatesIcon(second.getChapterUpdates) + } + } ?: activity!!.toast(R.string.merged_chapter_updates_error) + } ?: activity!!.toast(R.string.merged_toggle_chapter_updates_find_error) + } + + override fun onToggleChapterDownloadsClicked(position: Int) { + MaterialDialog(dialogView!!.context) + .title(R.string.download_merged_manga) + .message(R.string.download_merged_manga_desc) + .positiveButton(android.R.string.ok) { + toggleChapterDownloads(position) + } + .negativeButton(android.R.string.cancel) + .show() + } + + private fun toggleChapterDownloads(position: Int) { + val adapterReference = mergedMangaAdapter?.currentItems?.getOrNull(position)?.mergedMangaReference + mergedMangas.firstOrNull { it.second.id != null && it.second.id == adapterReference?.id }?.apply { + second.downloadChapters = !second.downloadChapters + + mergedMangaAdapter?.allBoundViewHolders?.firstOrNull { it is EditMergedMangaHolder && it.reference.id == second.id }?.let { + if (it is EditMergedMangaHolder) { + it.updateDownloadChaptersIcon(second.downloadChapters) + } + } ?: activity!!.toast(R.string.merged_toggle_download_chapters_error) + } ?: activity!!.toast(R.string.merged_toggle_download_chapters_find_error) + } + + private fun Pair.toModel(): EditMergedMangaItem { + return EditMergedMangaItem(first, second) + } + + + private companion object { + const val KEY_MANGA = "manga_id" + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/merged/EditMergedSettingsHeaderAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/merged/EditMergedSettingsHeaderAdapter.kt new file mode 100644 index 000000000..9d56906fe --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/merged/EditMergedSettingsHeaderAdapter.kt @@ -0,0 +1,150 @@ +package eu.kanade.tachiyomi.ui.manga.merged + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.ArrayAdapter +import androidx.recyclerview.widget.RecyclerView +import com.elvishew.xlog.XLog +import eu.kanade.tachiyomi.databinding.EditMergedSettingsHeaderBinding +import eu.kanade.tachiyomi.source.SourceManager +import exh.merged.sql.models.MergedMangaReference +import uy.kohesive.injekt.injectLazy + +class EditMergedSettingsHeaderAdapter(private val controller: EditMergedSettingsDialog, adapter: EditMergedMangaAdapter) : RecyclerView.Adapter() { + + private val sourceManager: SourceManager by injectLazy() + + private lateinit var binding: EditMergedSettingsHeaderBinding + + val editMergedMangaItemSortingListener: SortingListener = adapter + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderViewHolder { + binding = EditMergedSettingsHeaderBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return HeaderViewHolder(binding.root) + } + + override fun getItemCount(): Int = 1 + + override fun onBindViewHolder(holder: HeaderViewHolder, position: Int) { + holder.bind() + } + + inner class HeaderViewHolder(view: View) : RecyclerView.ViewHolder(view) { + fun bind() { + val dedupeAdapter: ArrayAdapter = ArrayAdapter( + itemView.context, android.R.layout.simple_spinner_item, + listOf( + "No dedupe", + "Dedupe by priority", + "Show source with most chapters", + "Show source with highest chapter number" + ) + ) + dedupeAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + binding.dedupeModeSpinner.adapter = dedupeAdapter + controller.mergeReference?.let { + binding.dedupeModeSpinner.setSelection( + when (it.chapterSortMode) { + MergedMangaReference.CHAPTER_SORT_NO_DEDUPE -> 0 + MergedMangaReference.CHAPTER_SORT_PRIORITY -> 1 + MergedMangaReference.CHAPTER_SORT_MOST_CHAPTERS -> 2 + MergedMangaReference.CHAPTER_SORT_HIGHEST_CHAPTER_NUMBER -> 3 + else -> 0 + } + ) + } + binding.dedupeModeSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected( + parent: AdapterView<*>?, + view: View?, + position: Int, + id: Long + ) { + controller.mergeReference?.chapterSortMode = when (position) { + 0 -> MergedMangaReference.CHAPTER_SORT_NO_DEDUPE + 1 -> MergedMangaReference.CHAPTER_SORT_PRIORITY + 2 -> MergedMangaReference.CHAPTER_SORT_MOST_CHAPTERS + 3 -> MergedMangaReference.CHAPTER_SORT_HIGHEST_CHAPTER_NUMBER + else -> MergedMangaReference.CHAPTER_SORT_NO_DEDUPE + } + XLog.nst().d(controller.mergeReference?.chapterSortMode) + editMergedMangaItemSortingListener.onSetPrioritySort(canMove()) + } + + override fun onNothingSelected(parent: AdapterView<*>?) { + controller.mergeReference?.chapterSortMode = MergedMangaReference.CHAPTER_SORT_NO_DEDUPE + } + } + + val mergedMangas = controller.mergedMangas + + val mangaInfoAdapter: ArrayAdapter = ArrayAdapter(itemView.context, android.R.layout.simple_spinner_item, mergedMangas.map { sourceManager.getOrStub(it.second.mangaSourceId).toString() + " " + it.first?.title }) + mangaInfoAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + binding.mangaInfoSpinner.adapter = mangaInfoAdapter + + mergedMangas.indexOfFirst { it.second.isInfoManga }.let { + if (it != -1) { + binding.mangaInfoSpinner.setSelection(it) + } else binding.mangaInfoSpinner.setSelection(0) + } + + binding.mangaInfoSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected( + parent: AdapterView<*>?, + view: View?, + position: Int, + id: Long + ) { + controller.mergedMangas.find { mergedManga -> mergedManga.second.id == mergedMangas.getOrNull(position)?.second?.id }?.second?.let { newInfoManga -> + controller.mergedMangas.onEach { + it.second.isInfoManga = false + } + newInfoManga.isInfoManga = true + } + } + + override fun onNothingSelected(parent: AdapterView<*>?) { + mergedMangas.find { it.second.isInfoManga }?.second?.let { newInfoManga -> + controller.mergedMangas.onEach { + it.second.isInfoManga = false + } + newInfoManga.isInfoManga = true + } + } + } + + binding.dedupeSwitch.isChecked = controller.mergeReference?.let { it.chapterSortMode != MergedMangaReference.CHAPTER_SORT_NONE } ?: false + binding.dedupeSwitch.setOnCheckedChangeListener { _, isChecked -> + binding.dedupeModeSpinner.isEnabled = isChecked + binding.dedupeModeSpinner.alpha = when (isChecked) { + true -> 1F + false -> 0.5F + } + controller.mergeReference?.chapterSortMode = when (isChecked) { + true -> MergedMangaReference.CHAPTER_SORT_NO_DEDUPE + false -> MergedMangaReference.CHAPTER_SORT_NONE + } + + if (isChecked) binding.dedupeModeSpinner.setSelection(0) + } + + binding.dedupeModeSpinner.isEnabled = binding.dedupeSwitch.isChecked + binding.dedupeModeSpinner.alpha = when (binding.dedupeSwitch.isChecked) { + true -> 1F + false -> 0.5F + } + } + } + + fun canMove() = controller.mergeReference?.let { it.chapterSortMode == MergedMangaReference.CHAPTER_SORT_PRIORITY } ?: false + + interface SortingListener { + fun onSetPrioritySort(isPriorityOrder: Boolean) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt index f7c05e7b8..dccc8bc9c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt @@ -17,6 +17,7 @@ import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.source.online.all.MergedSource import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.reader.chapter.ReaderChapterItem import eu.kanade.tachiyomi.ui.reader.loader.ChapterLoader @@ -32,6 +33,7 @@ import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.updateCoverLastModified import exh.EH_SOURCE_ID import exh.EXH_SOURCE_ID +import exh.MERGED_SOURCE_ID import exh.util.defaultReaderType import java.io.File import java.text.DecimalFormat @@ -39,6 +41,8 @@ import java.text.DecimalFormatSymbols import java.util.Date import java.util.concurrent.TimeUnit import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.singleOrNull +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import rx.Completable import rx.Observable @@ -97,7 +101,7 @@ class ReaderPresenter( */ private val chapterList by lazy { val manga = manga!! - val dbChapters = db.getChapters(manga).executeAsBlocking() + val dbChapters = if (manga.source == MERGED_SOURCE_ID) runBlocking { (sourceManager.get(MERGED_SOURCE_ID) as? MergedSource)?.getChaptersFromDB(manga)?.singleOrNull() ?: emptyList() } else db.getChapters(manga).executeAsBlocking() val selectedChapter = dbChapters.find { it.id == chapterId } ?: error("Requested chapter of id $chapterId not found in chapter list") @@ -236,7 +240,9 @@ class ReaderPresenter( val context = Injekt.get() val source = sourceManager.getOrStub(manga.source) - loader = ChapterLoader(context, downloadManager, manga, source) + val mergedReferences = if (source is MergedSource) db.getMergedMangaReferences(manga.id!!).executeAsBlocking() else emptyList() + val mergedManga = if (source is MergedSource) db.getMergedMangas(manga.id!!).executeAsBlocking() else emptyList() + loader = ChapterLoader(context, downloadManager, manga, source, sourceManager, mergedReferences, mergedManga) Observable.just(manga).subscribeLatestCache(ReaderActivity::setManga) viewerChaptersRelay.subscribeLatestCache(ReaderActivity::setChapters) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt index 68981efcf..3a17b88f5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt @@ -6,9 +6,12 @@ import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.source.online.all.MergedSource import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import exh.debug.DebugFunctions.prefs +import exh.merged.sql.models.MergedMangaReference import rx.Completable import rx.Observable import rx.android.schedulers.AndroidSchedulers @@ -22,7 +25,12 @@ class ChapterLoader( private val context: Context, private val downloadManager: DownloadManager, private val manga: Manga, - private val source: Source + private val source: Source, + // SY --> + private val sourceManager: SourceManager, + private val mergedReferences: List, + private val mergedManga: List +// SY <-- ) { /** @@ -81,6 +89,27 @@ class ChapterLoader( private fun getPageLoader(chapter: ReaderChapter): PageLoader { val isDownloaded = downloadManager.isChapterDownloaded(chapter.chapter, manga, true) return when { + // SY --> + source is MergedSource -> { + val mangaReference = mergedReferences.firstOrNull { it.mangaId == chapter.chapter.manga_id } ?: throw Exception("Merge reference null") + val source = sourceManager.get(mangaReference.mangaSourceId) ?: throw Exception("Source ${mangaReference.mangaSourceId} was null") + val manga = mergedManga.firstOrNull { it.id == chapter.chapter.manga_id } ?: throw Exception("Manga for merged chapter was null") + val isMergedMangaDownloaded = downloadManager.isChapterDownloaded(chapter.chapter, manga, true) + when { + isMergedMangaDownloaded -> DownloadPageLoader(chapter, manga, source, downloadManager) + source is HttpSource -> HttpPageLoader(chapter, source) + source is LocalSource -> source.getFormat(chapter.chapter).let { format -> + when (format) { + is LocalSource.Format.Directory -> DirectoryPageLoader(format.file) + is LocalSource.Format.Zip -> ZipPageLoader(format.file) + is LocalSource.Format.Rar -> RarPageLoader(format.file) + is LocalSource.Format.Epub -> EpubPageLoader(format.file) + } + } + else -> error(context.getString(R.string.loader_not_implemented_error)) + } + } + // SY <-- isDownloaded -> DownloadPageLoader(chapter, manga, source, downloadManager) source is HttpSource -> HttpPageLoader(chapter, source) source is LocalSource -> source.getFormat(chapter.chapter).let { format -> diff --git a/app/src/main/java/exh/EXHMigrations.kt b/app/src/main/java/exh/EXHMigrations.kt index 5295c0cfd..60ec990ba 100644 --- a/app/src/main/java/exh/EXHMigrations.kt +++ b/app/src/main/java/exh/EXHMigrations.kt @@ -2,6 +2,9 @@ package exh import android.content.Context import com.elvishew.xlog.XLog +import com.github.salomonbrys.kotson.fromJson +import com.google.gson.Gson +import com.google.gson.annotations.SerializedName import com.pushtorefresh.storio.sqlite.queries.Query import com.pushtorefresh.storio.sqlite.queries.RawQuery import eu.kanade.tachiyomi.BuildConfig @@ -12,13 +15,18 @@ import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.MangaImpl import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.resolvers.MangaUrlPutResolver +import eu.kanade.tachiyomi.data.database.tables.ChapterTable import eu.kanade.tachiyomi.data.database.tables.MangaTable import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.updater.UpdaterJob import eu.kanade.tachiyomi.extension.ExtensionUpdateJob +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.SourceManager +import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.all.Hitomi import eu.kanade.tachiyomi.source.online.all.NHentai +import exh.merged.sql.models.MergedMangaReference import exh.source.BlacklistedSources import java.io.File import java.net.URI @@ -27,6 +35,8 @@ import uy.kohesive.injekt.injectLazy object EXHMigrations { private val db: DatabaseHelper by injectLazy() + private val sourceManager: SourceManager by injectLazy() + private val gson: Gson by injectLazy() private val logger = XLog.tag("EXHMigrations") @@ -143,6 +153,106 @@ object EXHMigrations { ) } } + if (oldVersion < 7) { + db.inTransaction { + val mergedMangas = db.db.get() + .listOfObjects(Manga::class.java) + .withQuery( + Query.builder() + .table(MangaTable.TABLE) + .where("${MangaTable.COL_SOURCE} = $MERGED_SOURCE_ID") + .build() + ) + .prepare() + .executeAsBlocking() + + if (mergedMangas.isNotEmpty()) { + val mangaConfigs = mergedMangas.mapNotNull { mergedManga -> readMangaConfig(mergedManga, gson)?.let { mergedManga to it } } + if (mangaConfigs.isNotEmpty()) { + val mangaToUpdate = mutableListOf() + val mergedMangaReferences = mutableListOf() + mangaConfigs.onEach { mergedManga -> + mergedManga.second.children.firstOrNull()?.url?.let { + if (db.getManga(it, MERGED_SOURCE_ID).executeAsBlocking() != null) return@onEach + mergedManga.first.url = it + } + mangaToUpdate += mergedManga.first + mergedMangaReferences += MergedMangaReference( + id = null, + isInfoManga = false, + getChapterUpdates = false, + chapterSortMode = 0, + chapterPriority = 0, + downloadChapters = false, + mergeId = mergedManga.first.id!!, + mergeUrl = mergedManga.first.url, + mangaId = mergedManga.first.id!!, + mangaUrl = mergedManga.first.url, + mangaSourceId = MERGED_SOURCE_ID + ) + mergedManga.second.children.distinct().forEachIndexed { index, mangaSource -> + val load = mangaSource.load(db, sourceManager) ?: return@forEachIndexed + mergedMangaReferences += MergedMangaReference( + id = null, + isInfoManga = index == 0, + getChapterUpdates = true, + chapterSortMode = 0, + chapterPriority = 0, + downloadChapters = true, + mergeId = mergedManga.first.id!!, + mergeUrl = mergedManga.first.url, + mangaId = load.manga.id!!, + mangaUrl = load.manga.url, + mangaSourceId = load.source.id + ) + } + } + db.db.put() + .objects(mangaToUpdate) + // Extremely slow without the resolver :/ + .withPutResolver(MangaUrlPutResolver()) + .prepare() + .executeAsBlocking() + db.insertMergedMangas(mergedMangaReferences).executeAsBlocking() + + val loadedMangaList = mangaConfigs.map { it.second.children }.flatten().mapNotNull { it.load(db, sourceManager) }.distinct() + val chapters = db.db.get() + .listOfObjects(Chapter::class.java) + .withQuery( + Query.builder() + .table(ChapterTable.TABLE) + .where("${ChapterTable.COL_MANGA_ID} IN (${mergedMangas.filter { it.id != null }.joinToString { it.id.toString() }})") + .build() + ) + .prepare() + .executeAsBlocking() + val mergedMangaChapters = db.db.get() + .listOfObjects(Chapter::class.java) + .withQuery( + Query.builder() + .table(ChapterTable.TABLE) + .where("${ChapterTable.COL_MANGA_ID} IN (${loadedMangaList.filter { it.manga.id != null }.joinToString { it.manga.id.toString() }})") + .build() + ) + .prepare() + .executeAsBlocking() + val mergedMangaChaptersMatched = mergedMangaChapters.mapNotNull { chapter -> loadedMangaList.firstOrNull { it.manga.id == chapter.id }?.let { it to chapter } } + val parsedChapters = chapters.filter { it.read || it.last_page_read != 0 }.mapNotNull { chapter -> readUrlConfig(chapter.url, gson)?.let { chapter to it } } + val chaptersToUpdate = mutableListOf() + 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 += it.second.apply { + read = parsedChapter.first.read + last_page_read = parsedChapter.first.last_page_read + } + } + } + db.deleteChapters(mergedMangaChapters).executeAsBlocking() + db.updateChaptersProgress(chaptersToUpdate).executeAsBlocking() + } + } + } + } // if (oldVersion < 1) { } (1 is current release version) // do stuff here when releasing changed crap @@ -228,6 +338,57 @@ object EXHMigrations { orig } } + + private data class UrlConfig( + @SerializedName("s") + val source: Long, + @SerializedName("u") + val url: String, + @SerializedName("m") + val mangaUrl: String + ) + + private data class MangaConfig( + @SerializedName("c") + val children: List + ) { + companion object { + fun readFromUrl(gson: Gson, url: String): MangaConfig? { + return try { + gson.fromJson(url) + } catch (e: Exception) { + null + } + } + } + } + + private fun readMangaConfig(manga: SManga, gson: Gson): MangaConfig? { + return MangaConfig.readFromUrl(gson, manga.url) + } + + private data class MangaSource( + @SerializedName("s") + val source: Long, + @SerializedName("u") + val url: String + ) { + fun load(db: DatabaseHelper, sourceManager: SourceManager): LoadedMangaSource? { + val manga = db.getManga(url, source).executeAsBlocking() ?: return null + val source = sourceManager.getOrStub(source) + return LoadedMangaSource(source, manga) + } + } + + private fun readUrlConfig(url: String, gson: Gson): UrlConfig? { + return try { + gson.fromJson(url) + } catch (e: Exception) { + null + } + } + + private data class LoadedMangaSource(val source: Source, val manga: Manga) } data class BackupEntry( diff --git a/app/src/main/java/exh/merged/sql/resolvers/MergeMangaSettingsPutResolver.kt b/app/src/main/java/exh/merged/sql/resolvers/MergeMangaSettingsPutResolver.kt new file mode 100644 index 000000000..4a77fd0dd --- /dev/null +++ b/app/src/main/java/exh/merged/sql/resolvers/MergeMangaSettingsPutResolver.kt @@ -0,0 +1,31 @@ +package exh.merged.sql.resolvers + +import android.content.ContentValues +import com.pushtorefresh.storio.sqlite.StorIOSQLite +import com.pushtorefresh.storio.sqlite.operations.put.PutResolver +import com.pushtorefresh.storio.sqlite.operations.put.PutResult +import com.pushtorefresh.storio.sqlite.queries.UpdateQuery +import eu.kanade.tachiyomi.data.database.inTransactionReturn +import exh.merged.sql.models.MergedMangaReference +import exh.merged.sql.tables.MergedTable + +class MergeMangaSettingsPutResolver(val reset: Boolean = false) : PutResolver() { + + override fun performPut(db: StorIOSQLite, mergedMangaReference: MergedMangaReference) = db.inTransactionReturn { + val updateQuery = mapToUpdateQuery(mergedMangaReference) + val contentValues = mapToContentValues(mergedMangaReference) + + val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues) + PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table()) + } + + fun mapToUpdateQuery(mergedMangaReference: MergedMangaReference) = UpdateQuery.builder() + .table(MergedTable.TABLE) + .where("${MergedTable.COL_ID} = ?") + .whereArgs(mergedMangaReference.id) + .build() + + fun mapToContentValues(mergedMangaReference: MergedMangaReference) = ContentValues(1).apply { + put(MergedTable.COL_CHAPTER_SORT_MODE, mergedMangaReference.chapterSortMode) + } +} diff --git a/app/src/main/java/exh/merged/sql/resolvers/MergedMangaSettingsPutResolver.kt b/app/src/main/java/exh/merged/sql/resolvers/MergedMangaSettingsPutResolver.kt new file mode 100644 index 000000000..979794915 --- /dev/null +++ b/app/src/main/java/exh/merged/sql/resolvers/MergedMangaSettingsPutResolver.kt @@ -0,0 +1,34 @@ +package exh.merged.sql.resolvers + +import android.content.ContentValues +import com.pushtorefresh.storio.sqlite.StorIOSQLite +import com.pushtorefresh.storio.sqlite.operations.put.PutResolver +import com.pushtorefresh.storio.sqlite.operations.put.PutResult +import com.pushtorefresh.storio.sqlite.queries.UpdateQuery +import eu.kanade.tachiyomi.data.database.inTransactionReturn +import exh.merged.sql.models.MergedMangaReference +import exh.merged.sql.tables.MergedTable + +class MergedMangaSettingsPutResolver(val reset: Boolean = false) : PutResolver() { + + override fun performPut(db: StorIOSQLite, mergedMangaReference: MergedMangaReference) = db.inTransactionReturn { + val updateQuery = mapToUpdateQuery(mergedMangaReference) + val contentValues = mapToContentValues(mergedMangaReference) + + val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues) + PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table()) + } + + fun mapToUpdateQuery(mergedMangaReference: MergedMangaReference) = UpdateQuery.builder() + .table(MergedTable.TABLE) + .where("${MergedTable.COL_ID} = ?") + .whereArgs(mergedMangaReference.id) + .build() + + fun mapToContentValues(mergedMangaReference: MergedMangaReference) = ContentValues(4).apply { + put(MergedTable.COL_GET_CHAPTER_UPDATES, mergedMangaReference.getChapterUpdates) + put(MergedTable.COL_DOWNLOAD_CHAPTERS, mergedMangaReference.downloadChapters) + put(MergedTable.COL_IS_INFO_MANGA, mergedMangaReference.isInfoManga) + put(MergedTable.COL_CHAPTER_PRIORITY, mergedMangaReference.chapterPriority) + } +} diff --git a/app/src/main/java/exh/util/RxUtil.kt b/app/src/main/java/exh/util/RxUtil.kt index 5e17e5361..0b4cfe7a9 100644 --- a/app/src/main/java/exh/util/RxUtil.kt +++ b/app/src/main/java/exh/util/RxUtil.kt @@ -104,17 +104,30 @@ suspend fun Completable.awaitSuspending(subscribeOn: Scheduler? = null) { suspend fun Completable.awaitCompleted(): Unit = suspendCancellableCoroutine { cont -> subscribe(object : CompletableSubscriber { - override fun onSubscribe(s: Subscription) { cont.unsubscribeOnCancellation(s) } - override fun onCompleted() { cont.resume(Unit) } - override fun onError(e: Throwable) { cont.resumeWithException(e) } + override fun onSubscribe(s: Subscription) { + cont.unsubscribeOnCancellation(s) + } + + override fun onCompleted() { + cont.resume(Unit) + } + + override fun onError(e: Throwable) { + cont.resumeWithException(e) + } }) } suspend fun Single.await(): T = suspendCancellableCoroutine { cont -> cont.unsubscribeOnCancellation( subscribe(object : SingleSubscriber() { - override fun onSuccess(t: T) { cont.resume(t) } - override fun onError(error: Throwable) { cont.resumeWithException(error) } + override fun onSuccess(t: T) { + cont.resume(t) + } + + override fun onError(error: Throwable) { + cont.resumeWithException(error) + } }) ) } @@ -129,7 +142,11 @@ suspend fun Observable.awaitFirstOrDefault(default: T): T = firstOrDefaul suspend fun Observable.awaitFirstOrNull(): T? = firstOrDefault(null).awaitOne() @OptIn(InternalCoroutinesApi::class, ExperimentalCoroutinesApi::class) -suspend fun Observable.awaitFirstOrElse(defaultValue: () -> T): T = switchIfEmpty(Observable.fromCallable(defaultValue)).first().awaitOne() +suspend fun Observable.awaitFirstOrElse(defaultValue: () -> T): T = switchIfEmpty( + Observable.fromCallable( + defaultValue + ) +).first().awaitOne() @OptIn(InternalCoroutinesApi::class, ExperimentalCoroutinesApi::class) suspend fun Observable.awaitLast(): T = last().awaitOne() @@ -141,11 +158,24 @@ suspend fun Observable.awaitSingle(): T = single().awaitOne() private suspend fun Observable.awaitOne(): T = suspendCancellableCoroutine { cont -> cont.unsubscribeOnCancellation( subscribe(object : Subscriber() { - override fun onStart() { request(1) } - override fun onNext(t: T) { cont.resume(t) } - override fun onCompleted() { if (cont.isActive) cont.resumeWithException(IllegalStateException("Should have invoked onNext")) } + override fun onStart() { + request(1) + } + + override fun onNext(t: T) { + cont.resume(t) + } + + override fun onCompleted() { + if (cont.isActive) cont.resumeWithException( + IllegalStateException( + "Should have invoked onNext" + ) + ) + } + override fun onError(e: Throwable) { - /* + /* * Rx1 observable throws NoSuchElementException if cancellation happened before * element emission. To mitigate this we try to atomically resume continuation with exception: * if resume failed, then we know that continuation successfully cancelled itself @@ -185,7 +215,7 @@ fun Observable.asFlow(): Flow = callbackFlow { fun Flow.asObservable(backpressureMode: Emitter.BackpressureMode = Emitter.BackpressureMode.NONE): Observable { return Observable.create( { emitter -> - /* + /* * ATOMIC is used here to provide stable behaviour of subscribe+dispose pair even if * asObservable is already invoked from unconfined */ diff --git a/app/src/main/res/layout/edit_merged_settings_dialog.xml b/app/src/main/res/layout/edit_merged_settings_dialog.xml new file mode 100644 index 000000000..5204804c5 --- /dev/null +++ b/app/src/main/res/layout/edit_merged_settings_dialog.xml @@ -0,0 +1,17 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/edit_merged_settings_header.xml b/app/src/main/res/layout/edit_merged_settings_header.xml new file mode 100644 index 000000000..17f3a9d16 --- /dev/null +++ b/app/src/main/res/layout/edit_merged_settings_header.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/edit_merged_settings_item.xml b/app/src/main/res/layout/edit_merged_settings_item.xml new file mode 100644 index 000000000..b7d88d5e9 --- /dev/null +++ b/app/src/main/res/layout/edit_merged_settings_item.xml @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/eh_smart_search.xml b/app/src/main/res/layout/eh_smart_search.xml index ea7aa1f1f..6800a05ba 100644 --- a/app/src/main/res/layout/eh_smart_search.xml +++ b/app/src/main/res/layout/eh_smart_search.xml @@ -39,16 +39,14 @@ android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:text="Searching source..." - android:textAppearance="@style/TextAppearance.Medium.Title" - android:textColor="@android:color/white" /> + android:textAppearance="@style/TextAppearance.Medium.Title" /> + android:layout_gravity="center" /> diff --git a/app/src/main/res/menu/manga.xml b/app/src/main/res/menu/manga.xml index 7d0468e18..462925196 100644 --- a/app/src/main/res/menu/manga.xml +++ b/app/src/main/res/menu/manga.xml @@ -57,4 +57,18 @@ android:title="@string/az_recommends" android:visible="false" app:showAsAction="never" /> + + + + diff --git a/app/src/main/res/values/strings_sy.xml b/app/src/main/res/values/strings_sy.xml index 87a7cf167..ef705fa33 100644 --- a/app/src/main/res/values/strings_sy.xml +++ b/app/src/main/res/values/strings_sy.xml @@ -483,4 +483,23 @@ %2$s, %1$d pages + + Merge settings + Fetch chapter updates + Are you sure? + This will remove the manga from the merge, using this will also lose any unsaved changes applied to the merged manga + Toggle chapter updates + Toggling this will disable or enable chapter updates for this merged manga + Toggle new chapter downloads + Toggling this will disable or enable chapter downloads for this merged manga + Merged references invalid + Toggle chapter updates error + Could not find manga to toggle chapter updates + Toggle download chapters error + Could not find manga to toggle chapter downloads + Allow deduplication: + Dedupe mode: + Info manga: + Toggle dedupe + \ No newline at end of file