Merged manga implementation, man this took forever to make and bugfix, its not even done

This commit is contained in:
Jobobby04 2020-09-05 18:17:33 -04:00
parent d21a652944
commit a1d54880c3
32 changed files with 1753 additions and 339 deletions

View File

@ -43,7 +43,7 @@ android {
minSdkVersion AndroidConfig.minSdk minSdkVersion AndroidConfig.minSdk
targetSdkVersion AndroidConfig.targetSdk targetSdkVersion AndroidConfig.targetSdk
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
versionCode 6 versionCode 7
versionName "1.2.0" versionName "1.2.0"
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\"" buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""

View File

@ -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.EXTENSIONS
import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY 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.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.SAVEDSEARCHES
import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
import eu.kanade.tachiyomi.data.backup.models.DHistory 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.ChapterTypeAdapter
import eu.kanade.tachiyomi.data.backup.serializer.HistoryTypeAdapter import eu.kanade.tachiyomi.data.backup.serializer.HistoryTypeAdapter
import eu.kanade.tachiyomi.data.backup.serializer.MangaTypeAdapter 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.backup.serializer.TrackTypeAdapter
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.CategoryImpl 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.Source
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.all.EHentai import eu.kanade.tachiyomi.source.online.all.EHentai
import eu.kanade.tachiyomi.source.online.all.MergedSource
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import exh.EXHSavedSearch import exh.EXHSavedSearch
import exh.MERGED_SOURCE_ID
import exh.eh.EHentaiThrottleManager import exh.eh.EHentaiThrottleManager
import exh.merged.sql.models.MergedMangaReference
import exh.util.asObservable
import java.lang.RuntimeException import java.lang.RuntimeException
import kotlin.math.max import kotlin.math.max
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import rx.Observable import rx.Observable
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@ -106,6 +114,9 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build()) .registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build()) .registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build()) .registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
// SY -->
.registerTypeAdapter<MergedMangaReference>(MergedMangaReferenceTypeAdapter.build())
// SY <--
.create() .create()
else -> throw Exception("Json version unknown") else -> throw Exception("Json version unknown")
} }
@ -129,15 +140,21 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
// Create extension ID/name mapping // Create extension ID/name mapping
val extensionEntries = JsonArray() val extensionEntries = JsonArray()
// Merged Manga References
val mergedMangaReferenceEntries = JsonArray()
// Add value's to root // Add value's to root
root[Backup.VERSION] = CURRENT_VERSION root[Backup.VERSION] = CURRENT_VERSION
root[Backup.MANGAS] = mangaEntries root[Backup.MANGAS] = mangaEntries
root[CATEGORIES] = categoryEntries root[CATEGORIES] = categoryEntries
root[EXTENSIONS] = extensionEntries root[EXTENSIONS] = extensionEntries
// SY -->
root[MERGEDMANGAREFERENCES] = mergedMangaReferenceEntries
// SY <--
databaseHelper.inTransaction { databaseHelper.inTransaction {
// Get manga from database // Get manga from database
val mangas = getFavoriteManga() val mangas = getFavoriteManga() /* SY --> */ + getMergedManga() /* SY <-- */
val extensions: MutableSet<String> = mutableSetOf() val extensions: MutableSet<String> = mutableSetOf()
@ -163,6 +180,8 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
// SY --> // SY -->
root[SAVEDSEARCHES] = root[SAVEDSEARCHES] =
Injekt.get<PreferencesHelper>().eh_savedSearches().get().joinToString(separator = "***") Injekt.get<PreferencesHelper>().eh_savedSearches().get().joinToString(separator = "***")
backupMergedMangaReferences(mergedMangaReferenceEntries)
// SY <-- // 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 * 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<Chapter>, throttleManager: EHentaiThrottleManager): Observable<Pair<List<Chapter>, List<Chapter>>> { fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>, throttleManager: EHentaiThrottleManager): Observable<Pair<List<Chapter>, List<Chapter>>> {
// SY --> // SY -->
return ( if (source is MergedSource) {
if (source is EHentai) { val syncedChapters = runBlocking { source.fetchChaptersAndSync(manga, false) }
source.fetchChapterList(manga, throttleManager::throttle) return syncedChapters.onEach { pair ->
} 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()) { if (pair.first.isNotEmpty()) {
chapters.forEach { it.manga_id = manga.id } chapters.forEach { it.manga_id = manga.id }
insertChapters(chapters) 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()) 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<List<MergedMangaReference>>(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 <-- // SY <--
/** /**
@ -602,6 +682,9 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
internal fun getFavoriteManga(): List<Manga> = internal fun getFavoriteManga(): List<Manga> =
databaseHelper.getFavoriteMangas().executeAsBlocking() databaseHelper.getFavoriteMangas().executeAsBlocking()
internal fun getMergedManga(): List<Manga> =
databaseHelper.getMergedMangas().executeAsBlocking()
/** /**
* Inserts manga and returns id * Inserts manga and returns id
* *

View File

@ -238,7 +238,7 @@ class BackupRestoreService : Service() {
} }
totalAmount = mangasJson.size() 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() skippedAmount = mangasJson.size() - validManga.count()
// SY <-- // SY <--
restoreProgress = 0 restoreProgress = 0
@ -288,6 +288,15 @@ class BackupRestoreService : Service() {
restoreProgress += 1 restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.saved_searches)) 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 <-- // SY <--
private fun restoreManga(mangaJson: JsonObject) { private fun restoreManga(mangaJson: JsonObject) {

View File

@ -19,6 +19,7 @@ object Backup {
const val VERSION = "version" const val VERSION = "version"
// SY --> // SY -->
const val SAVEDSEARCHES = "savedsearches" const val SAVEDSEARCHES = "savedsearches"
const val MERGEDMANGAREFERENCES = "mergedmangareferences"
// SY <-- // SY <--
fun getDefaultFilename(): String { fun getDefaultFilename(): String {

View File

@ -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<MergedMangaReference> {
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
)
}
}
}
}

View File

@ -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.HistoryTable as History
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable as MangaCategory import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable as MangaCategory
import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga
import exh.MERGED_SOURCE_ID
import exh.merged.sql.tables.MergedTable as Merged import exh.merged.sql.tables.MergedTable as Merged
// SY --> // SY -->
@ -21,6 +22,9 @@ fun getMergedMangaQuery() =
ON ${Manga.TABLE}.${Manga.COL_ID} = M.${Merged.COL_MANGA_ID} 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() = fun getAllMergedMangaQuery() =
""" """
SELECT ${Manga.TABLE}.* SELECT ${Manga.TABLE}.*
@ -56,7 +60,6 @@ fun getMergedChaptersQuery() =
JOIN ${Chapter.TABLE} JOIN ${Chapter.TABLE}
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = M.${Merged.COL_MANGA_ID} 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. * 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} SELECT M.*, COALESCE(MC.${MangaCategory.COL_CATEGORY_ID}, 0) AS ${Manga.COL_CATEGORY}
FROM ( FROM (
SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD}, COALESCE(R.read, 0) AS ${Manga.COL_READ} SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD}, COALESCE(R.read, 0) AS ${Manga.COL_READ}
FROM ${Manga.TABLE} FROM ${Manga.TABLE}
LEFT JOIN ( LEFT JOIN (
SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS unread SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}, COUNT(*) AS unread
FROM ${Chapter.TABLE} FROM ${Chapter.TABLE}
WHERE ${Chapter.COL_READ} = 0 WHERE ${Chapter.COL_READ} = 0
GROUP BY ${Chapter.COL_MANGA_ID} GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
) AS C ) AS C
ON ${Manga.COL_ID} = C.${Chapter.COL_MANGA_ID} ON ${Manga.TABLE}.${Manga.COL_ID} = C.${Chapter.COL_MANGA_ID}
LEFT JOIN ( LEFT JOIN (
SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS read SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS read
FROM ${Chapter.TABLE} FROM ${Chapter.TABLE}
WHERE ${Chapter.COL_READ} = 1 WHERE ${Chapter.COL_READ} = 1
GROUP BY ${Chapter.COL_MANGA_ID} GROUP BY ${Chapter.COL_MANGA_ID}
) AS R ) AS R
ON ${Manga.COL_ID} = R.${Chapter.COL_MANGA_ID} ON ${Manga.TABLE}.${Manga.COL_ID} = R.${Chapter.COL_MANGA_ID}
WHERE ${Manga.COL_FAVORITE} = 1 WHERE ${Manga.COL_FAVORITE} = 1 AND ${Manga.COL_SOURCE} <> $MERGED_SOURCE_ID
GROUP BY ${Manga.COL_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} ORDER BY ${Manga.COL_TITLE}
) AS M ) AS M
LEFT JOIN ( LEFT JOIN (
SELECT * FROM ${MangaCategory.TABLE}) AS MC SELECT * FROM ${MangaCategory.TABLE}
ON MC.${MangaCategory.COL_MANGA_ID} = M.${Manga.COL_ID} ) 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. * Query to get the recent chapters of manga from the library up to a date.

View File

@ -22,6 +22,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.SManga 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.ui.library.LibraryGroup
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import eu.kanade.tachiyomi.util.prepUpdateCover 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.acquireWakeLock
import eu.kanade.tachiyomi.util.system.isServiceRunning import eu.kanade.tachiyomi.util.system.isServiceRunning
import exh.LIBRARY_UPDATE_EXCLUDED_SOURCES import exh.LIBRARY_UPDATE_EXCLUDED_SOURCES
import exh.MERGED_SOURCE_ID
import exh.util.asObservable
import exh.util.nullIfBlank import exh.util.nullIfBlank
import java.io.File import java.io.File
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.runBlocking
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
@ -385,7 +389,12 @@ class LibraryUpdateService(
private fun downloadChapters(manga: Manga, chapters: List<Chapter>) { private fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
// We don't want to start downloading while the library is updating, because websites // 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. // 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() .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) } .map { syncChaptersWithSource(db, it, manga, source) }
} }

View File

@ -8,7 +8,10 @@ import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import exh.util.asObservable
import kotlin.jvm.Throws import kotlin.jvm.Throws
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
@ -25,13 +28,17 @@ abstract class SuspendHttpSource : HttpSource() {
* *
* @param page the page number to retrieve. * @param page the page number to retrieve.
*/ */
override fun fetchPopularManga(page: Int): Observable<MangasPage> { final override fun fetchPopularManga(page: Int): Observable<MangasPage> {
return Observable.just(runBlocking { fetchPopularMangaSuspended(page) }) return fetchPopularMangaFlow(page).asObservable()
} }
open suspend fun fetchPopularMangaSuspended(page: Int): MangasPage { open fun fetchPopularMangaFlow(page: Int): Flow<MangasPage> {
val response = client.newCall(popularMangaRequestSuspended(page)).await() return flow {
return popularMangaParseSuspended(response) 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. * @param page the page number to retrieve.
*/ */
override fun popularMangaRequest(page: Int): Request { final override fun popularMangaRequest(page: Int): Request {
return runBlocking { popularMangaRequestSuspended(page) } return runBlocking { popularMangaRequestSuspended(page) }
} }
@ -50,7 +57,7 @@ abstract class SuspendHttpSource : HttpSource() {
* *
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun popularMangaParse(response: Response): MangasPage { final override fun popularMangaParse(response: Response): MangasPage {
return runBlocking { popularMangaParseSuspended(response) } return runBlocking { popularMangaParseSuspended(response) }
} }
@ -64,13 +71,17 @@ abstract class SuspendHttpSource : HttpSource() {
* @param query the search query. * @param query the search query.
* @param filters the list of filters to apply. * @param filters the list of filters to apply.
*/ */
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> { final override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return Observable.just(runBlocking { fetchSearchMangaSuspended(page, query, filters) }) return fetchSearchMangaSuspended(page, query, filters).asObservable()
} }
open suspend fun fetchSearchMangaSuspended(page: Int, query: String, filters: FilterList): MangasPage { open fun fetchSearchMangaSuspended(page: Int, query: String, filters: FilterList): Flow<MangasPage> {
val response = client.newCall(searchMangaRequestSuspended(page, query, filters)).await() return flow {
return searchMangaParseSuspended(response) 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 query the search query.
* @param filters the list of filters to apply. * @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) } return runBlocking { searchMangaRequestSuspended(page, query, filters) }
} }
@ -91,7 +102,7 @@ abstract class SuspendHttpSource : HttpSource() {
* *
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun searchMangaParse(response: Response): MangasPage { final override fun searchMangaParse(response: Response): MangasPage {
return runBlocking { searchMangaParseSuspended(response) } return runBlocking { searchMangaParseSuspended(response) }
} }
@ -102,13 +113,17 @@ abstract class SuspendHttpSource : HttpSource() {
* *
* @param page the page number to retrieve. * @param page the page number to retrieve.
*/ */
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> { final override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
return Observable.just(runBlocking { fetchLatestUpdatesSuspended(page) }) return fetchLatestUpdatesFlow(page).asObservable()
} }
open suspend fun fetchLatestUpdatesSuspended(page: Int): MangasPage { open fun fetchLatestUpdatesFlow(page: Int): Flow<MangasPage> {
val response = client.newCall(latestUpdatesRequestSuspended(page)).await() return flow {
return latestUpdatesParseSuspended(response) 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. * @param page the page number to retrieve.
*/ */
override fun latestUpdatesRequest(page: Int): Request { final override fun latestUpdatesRequest(page: Int): Request {
return runBlocking { latestUpdatesRequestSuspended(page) } return runBlocking { latestUpdatesRequestSuspended(page) }
} }
@ -127,7 +142,7 @@ abstract class SuspendHttpSource : HttpSource() {
* *
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun latestUpdatesParse(response: Response): MangasPage { final override fun latestUpdatesParse(response: Response): MangasPage {
return runBlocking { latestUpdatesParseSuspended(response) } return runBlocking { latestUpdatesParseSuspended(response) }
} }
@ -139,13 +154,17 @@ abstract class SuspendHttpSource : HttpSource() {
* *
* @param manga the manga to be updated. * @param manga the manga to be updated.
*/ */
override fun fetchMangaDetails(manga: SManga): Observable<SManga> { final override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return Observable.just(runBlocking { fetchMangaDetailsSuspended(manga) }) return fetchMangaDetailsFlow(manga).asObservable()
} }
open suspend fun fetchMangaDetailsSuspended(manga: SManga): SManga { open fun fetchMangaDetailsFlow(manga: SManga): Flow<SManga> {
val response = client.newCall(mangaDetailsRequestSuspended(manga)).await() return flow {
return mangaDetailsParseSuspended(response).apply { initialized = true } 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. * @param manga the manga to be updated.
*/ */
override fun mangaDetailsRequest(manga: SManga): Request { final override fun mangaDetailsRequest(manga: SManga): Request {
return runBlocking { mangaDetailsRequestSuspended(manga) } return runBlocking { mangaDetailsRequestSuspended(manga) }
} }
@ -167,7 +186,7 @@ abstract class SuspendHttpSource : HttpSource() {
* *
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun mangaDetailsParse(response: Response): SManga { final override fun mangaDetailsParse(response: Response): SManga {
return runBlocking { mangaDetailsParseSuspended(response) } return runBlocking { mangaDetailsParseSuspended(response) }
} }
@ -179,21 +198,25 @@ abstract class SuspendHttpSource : HttpSource() {
* *
* @param manga the manga to look for chapters. * @param manga the manga to look for chapters.
*/ */
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> { final override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return try { return try {
Observable.just(runBlocking { fetchChapterListSuspended(manga) }) fetchChapterListFlow(manga).asObservable()
} catch (e: LicencedException) { } catch (e: LicencedException) {
Observable.error(Exception("Licensed - No chapters to show")) Observable.error(Exception("Licensed - No chapters to show"))
} }
} }
@Throws(LicencedException::class) @Throws(LicencedException::class)
open suspend fun fetchChapterListSuspended(manga: SManga): List<SChapter> { open fun fetchChapterListFlow(manga: SManga): Flow<List<SChapter>> {
return if (manga.status != SManga.LICENSED) { return flow {
val response = client.newCall(chapterListRequestSuspended(manga)).await() if (manga.status != SManga.LICENSED) {
chapterListParseSuspended(response) val response = client.newCall(chapterListRequestSuspended(manga)).await()
} else { emit(
throw LicencedException("Licensed - No chapters to show") 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. * @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) } return runBlocking { chapterListRequestSuspended(manga) }
} }
@ -216,7 +239,7 @@ abstract class SuspendHttpSource : HttpSource() {
* *
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun chapterListParse(response: Response): List<SChapter> { final override fun chapterListParse(response: Response): List<SChapter> {
return runBlocking { chapterListParseSuspended(response) } return runBlocking { chapterListParseSuspended(response) }
} }
@ -227,13 +250,17 @@ abstract class SuspendHttpSource : HttpSource() {
* *
* @param chapter the chapter whose page list has to be fetched. * @param chapter the chapter whose page list has to be fetched.
*/ */
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> { final override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
return Observable.just(runBlocking { fetchPageListSuspended(chapter) }) return fetchPageListFlow(chapter).asObservable()
} }
open suspend fun fetchPageListSuspended(chapter: SChapter): List<Page> { open fun fetchPageListFlow(chapter: SChapter): Flow<List<Page>> {
val response = client.newCall(pageListRequestSuspended(chapter)).await() return flow {
return pageListParseSuspended(response) 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. * @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) } return runBlocking { pageListRequestSuspended(chapter) }
} }
@ -255,7 +282,7 @@ abstract class SuspendHttpSource : HttpSource() {
* *
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun pageListParse(response: Response): List<Page> { final override fun pageListParse(response: Response): List<Page> {
return runBlocking { pageListParseSuspended(response) } return runBlocking { pageListParseSuspended(response) }
} }
@ -267,13 +294,17 @@ abstract class SuspendHttpSource : HttpSource() {
* *
* @param page the page whose source image has to be fetched. * @param page the page whose source image has to be fetched.
*/ */
override fun fetchImageUrl(page: Page): Observable<String> { final override fun fetchImageUrl(page: Page): Observable<String> {
return Observable.just(runBlocking { fetchImageUrlSuspended(page) }) return fetchImageUrlFlow(page).asObservable()
} }
open suspend fun fetchImageUrlSuspended(page: Page): String { open fun fetchImageUrlFlow(page: Page): Flow<String> {
val response = client.newCall(imageUrlRequestSuspended(page)).await() return flow {
return imageUrlParseSuspended(response) 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 * @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) } return runBlocking { imageUrlRequestSuspended(page) }
} }
@ -295,7 +326,7 @@ abstract class SuspendHttpSource : HttpSource() {
* *
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun imageUrlParse(response: Response): String { final override fun imageUrlParse(response: Response): String {
return runBlocking { imageUrlParseSuspended(response) } return runBlocking { imageUrlParseSuspended(response) }
} }
@ -306,12 +337,16 @@ abstract class SuspendHttpSource : HttpSource() {
* *
* @param page the page whose source image has to be downloaded. * @param page the page whose source image has to be downloaded.
*/ */
override fun fetchImage(page: Page): Observable<Response> { final override fun fetchImage(page: Page): Observable<Response> {
return Observable.just(runBlocking { fetchImageSuspended(page) }) return fetchImageFlow(page).asObservable()
} }
open suspend fun fetchImageSuspended(page: Page): Response { open fun fetchImageFlow(page: Page): Flow<Response> {
return client.newCallWithProgress(imageRequestSuspended(page), page).await() 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 * @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) } return runBlocking { imageRequestSuspended(page) }
} }
@ -335,7 +370,7 @@ abstract class SuspendHttpSource : HttpSource() {
* @param chapter the chapter to be added. * @param chapter the chapter to be added.
* @param manga the manga of the chapter. * @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) } runBlocking { prepareNewChapterSuspended(chapter, manga) }
} }

View File

@ -1,43 +1,44 @@
package eu.kanade.tachiyomi.source.online.all package eu.kanade.tachiyomi.source.online.all
import com.elvishew.xlog.XLog 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.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga 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.Source
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga 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.source.online.SuspendHttpSource
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
import exh.MERGED_SOURCE_ID import exh.MERGED_SOURCE_ID
import exh.merged.sql.models.MergedMangaReference
import exh.util.asFlow import exh.util.asFlow
import exh.util.await import exh.util.await
import exh.util.awaitSingle
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flatMapMerge import kotlinx.coroutines.flow.flatMapMerge
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.single
import kotlinx.coroutines.flow.singleOrNull
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.Response import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
// TODO LocalSource compatibility
// TODO Disable clear database option
class MergedSource : SuspendHttpSource() { class MergedSource : SuspendHttpSource() {
private val db: DatabaseHelper by injectLazy() private val db: DatabaseHelper by injectLazy()
private val sourceManager: SourceManager 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 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 searchMangaParseSuspended(response: Response) = throw UnsupportedOperationException()
override suspend fun latestUpdatesRequestSuspended(page: Int) = throw UnsupportedOperationException() override suspend fun latestUpdatesRequestSuspended(page: Int) = throw UnsupportedOperationException()
override suspend fun latestUpdatesParseSuspended(response: Response) = 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 { override fun fetchMangaDetailsFlow(manga: SManga): Flow<SManga> {
return readMangaConfig(manga).load(db, sourceManager).take(1).map { loaded -> return flow {
SManga.create().apply { val mergedManga = db.getManga(manga.url, id).await() ?: throw Exception("merged manga not in db")
this.copyFrom(loaded.manga) val mangaReferences = mergedManga.id?.let { withContext(Dispatchers.IO) { db.getMergedMangaReferences(it).await() } } ?: throw Exception("merged manga id is null")
url = manga.url if (mangaReferences.isEmpty()) throw IllegalArgumentException("Manga references are empty, info unavailable, merge is likely corrupted")
} if (mangaReferences.size == 1 || {
}.first() 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<SChapter> { fun getChaptersFromDB(manga: Manga, editScanlators: Boolean = false, dedupe: Boolean = true): Flow<List<Chapter>> {
val loadedMangas = readMangaConfig(manga).load(db, sourceManager).buffer() // TODO more chapter dedupe
return loadedMangas.flatMapMerge { loadedManga -> return db.getChaptersByMergedMangaId(manga.id!!).asRxObservable()
withContext(Dispatchers.IO) { .asFlow()
loadedManga.source.fetchChapterList(loadedManga.manga).asFlow().map { chapterList -> .map { chapterList ->
chapterList.map { chapter -> val mangaReferences = withContext(Dispatchers.IO) { db.getMergedMangaReferences(manga.id!!).await() }
chapter.apply { val sources = mangaReferences.map { sourceManager.getOrStub(it.mangaSourceId) to it.mangaId }
url = writeUrlConfig( if (editScanlators) {
UrlConfig( chapterList.onEach { chapter ->
loadedManga.source.id, val source = sources.firstOrNull { chapter.manga_id == it.second }?.first
url, if (source != null) {
loadedManga.manga.url 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() private fun dedupeChapterList(mangaReferences: List<MergedMangaReference>, chapterList: List<Chapter>): List<Chapter> {
override suspend fun chapterListParseSuspended(response: Response) = throw UnsupportedOperationException() return when (mangaReferences.firstOrNull { it.mangaSourceId == MERGED_SOURCE_ID }?.chapterSortMode) {
MergedMangaReference.CHAPTER_SORT_NO_DEDUPE, MergedMangaReference.CHAPTER_SORT_NONE -> chapterList
override suspend fun fetchPageListSuspended(chapter: SChapter): List<Page> { MergedMangaReference.CHAPTER_SORT_PRIORITY -> chapterList
val config = readUrlConfig(chapter.url) MergedMangaReference.CHAPTER_SORT_MOST_CHAPTERS -> {
val source = sourceManager.getOrStub(config.source) findSourceWithMostChapters(chapterList)?.let { mangaId ->
return source.fetchPageList( chapterList.filter { it.manga_id == mangaId }
SChapter.create().apply { } ?: chapterList
copyFrom(chapter)
url = config.url
} }
).map { pages -> MergedMangaReference.CHAPTER_SORT_HIGHEST_CHAPTER_NUMBER -> {
pages.map { page -> findSourceWithHighestChapterNumber(chapterList)?.let { mangaId ->
page.copyWithUrl(writeUrlConfig(UrlConfig(config.source, page.url, config.mangaUrl))) chapterList.filter { it.manga_id == mangaId }
} ?: chapterList
} }
}.awaitSingle() else -> chapterList
}
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<Response> {
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)
} }
} }
data class MangaConfig( private fun findSourceWithMostChapters(chapterList: List<Chapter>): Long? {
@SerializedName("c") return chapterList.groupBy { it.manga_id }.maxByOrNull { it.value.size }?.key
val children: List<MangaSource> }
) {
fun load(db: DatabaseHelper, sourceManager: SourceManager): Flow<LoadedMangaSource> { private fun findSourceWithHighestChapterNumber(chapterList: List<Chapter>): Long? {
return children.asFlow().map { mangaSource -> return chapterList.maxByOrNull { it.chapter_number }?.manga_id
mangaSource.load(db, sourceManager) ?: run { }
XLog.w("> Missing source manga: $mangaSource")
throw IllegalStateException("Missing source manga: $mangaSource") fun fetchChaptersForMergedManga(manga: Manga, downloadChapters: Boolean = true, editScanlators: Boolean = false, dedupe: Boolean = true): Flow<List<Chapter>> {
return flow {
withContext(Dispatchers.IO) {
fetchChaptersAndSync(manga, downloadChapters).collect()
}
emit(
getChaptersFromDB(manga, editScanlators, dedupe).singleOrNull() ?: emptyList<Chapter>()
)
}
}
suspend fun fetchChaptersAndSync(manga: Manga, downloadChapters: Boolean = true): Flow<Pair<List<Chapter>, List<Chapter>>> {
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<Pair<List<Chapter>, List<Chapter>>>().asFlow()
} }
} }
} }.buffer()
fun writeAsUrl(gson: Gson): String {
return gson.toJson(this)
}
companion object {
fun readFromUrl(gson: Gson, url: String): MangaConfig {
return gson.fromJson(url)
}
}
} }
data class UrlConfig( suspend fun load(db: DatabaseHelper, sourceManager: SourceManager, reference: MergedMangaReference): LoadedMangaSource {
@SerializedName("s") var manga = db.getManga(reference.mangaUrl, reference.mangaSourceId).await()
val source: Long, val source = sourceManager.getOrStub(manga?.source ?: reference.mangaSourceId)
@SerializedName("u") if (manga == null) {
val url: String, manga = Manga.create(reference.mangaSourceId).apply {
@SerializedName("m") url = reference.mangaUrl
val mangaUrl: String }
) 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( data class LoadedMangaSource(val source: Source, val manga: Manga?, val reference: MergedMangaReference)
index,
newUrl,
imageUrl,
uri
)
override val lang = "all" override val lang = "all"
override val supportsLatest = false override val supportsLatest = false

View File

@ -5,7 +5,6 @@ import android.widget.PopupMenu
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.google.gson.Gson
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga 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.data.glide.toMangaThumbnail
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager 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.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.ui.manga.MangaController 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.system.getResourceColor
import eu.kanade.tachiyomi.util.view.setVectorCompat import eu.kanade.tachiyomi.util.view.setVectorCompat
import exh.MERGED_SOURCE_ID import exh.MERGED_SOURCE_ID
import exh.util.await
import java.text.DecimalFormat import java.text.DecimalFormat
import kotlinx.android.synthetic.main.migration_manga_card.view.gradient import kotlinx.android.synthetic.main.migration_manga_card.view.gradient
import kotlinx.android.synthetic.main.migration_manga_card.view.loading_group 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 db: DatabaseHelper by injectLazy()
private val sourceManager: SourceManager by injectLazy() private val sourceManager: SourceManager by injectLazy()
private var item: MigrationProcessItem? = null private var item: MigrationProcessItem? = null
private val gson: Gson by injectLazy()
private val scope = CoroutineScope(Job() + Dispatchers.Main) private val scope = CoroutineScope(Job() + Dispatchers.Main)
init { init {
@ -154,7 +152,7 @@ class MigrationProcessHolder(
migration_manga_card_to.clicks() 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 loading_group.isVisible = false
GlideApp.with(view.context.applicationContext) GlideApp.with(view.context.applicationContext)
.load(manga.toMangaThumbnail()) .load(manga.toMangaThumbnail())
@ -171,8 +169,8 @@ class MigrationProcessHolder(
gradient.isVisible = true gradient.isVisible = true
manga_source_label.text = if (source.id == MERGED_SOURCE_ID) { manga_source_label.text = if (source.id == MERGED_SOURCE_ID) {
MergedSource.MangaConfig.readFromUrl(gson, manga.url).children.map { db.getMergedMangaReferences(manga.id!!).await().map {
sourceManager.getOrStub(it.source).toString() sourceManager.getOrStub(it.mangaSourceId).toString()
}.distinct().joinToString() }.distinct().joinToString()
} else { } else {
source.toString() source.toString()

View File

@ -441,6 +441,7 @@ class SourceController(bundle: Bundle? = null) :
companion object { companion object {
const val SMART_SEARCH_CONFIG = "SMART_SEARCH_CONFIG" const val SMART_SEARCH_CONFIG = "SMART_SEARCH_CONFIG"
const val SMART_SEARCH_SOURCE_TAG = "smart_search_source_tag"
} }
// SY <-- // SY <--
} }

View File

@ -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.Filter.TriState.Companion.STATE_INCLUDE
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource 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.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.isLocal import eu.kanade.tachiyomi.util.isLocal
import eu.kanade.tachiyomi.util.lang.combineLatest 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 eu.kanade.tachiyomi.util.removeCovers
import exh.EH_SOURCE_ID import exh.EH_SOURCE_ID
import exh.EXH_SOURCE_ID import exh.EXH_SOURCE_ID
import exh.MERGED_SOURCE_ID
import exh.favorites.FavoritesSyncHelper import exh.favorites.FavoritesSyncHelper
import exh.util.await
import exh.util.isLewd import exh.util.isLewd
import exh.util.nullIfBlank import exh.util.nullIfBlank
import java.util.Collections import java.util.Collections
import java.util.Comparator import java.util.Comparator
import kotlinx.coroutines.flow.singleOrNull
import kotlinx.coroutines.runBlocking
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
@ -241,7 +246,10 @@ class LibraryPresenter(
for ((_, itemList) in map) { for ((_, itemList) in map) {
for (item in itemList) { for (item in itemList) {
item.downloadCount = if (showDownloadBadges) { 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 { } else {
// Unset download count if not enabled // Unset download count if not enabled
-1 -1
@ -455,8 +463,10 @@ class LibraryPresenter(
mangas.forEach { manga -> mangas.forEach { manga ->
launchIO { launchIO {
/* SY --> */ val chapters = if (manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID) { /* 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() 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() } else /* SY <-- */ db.getChapters(manga).executeAsBlocking()
.filter { !it.read } .filter { !it.read }
@ -501,7 +511,7 @@ class LibraryPresenter(
fun markReadStatus(mangas: List<Manga>, read: Boolean) { fun markReadStatus(mangas: List<Manga>, read: Boolean) {
mangas.forEach { manga -> mangas.forEach { manga ->
launchIO { 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 { chapters.forEach {
it.read = read it.read = read
if (!read) { if (!read) {
@ -519,7 +529,16 @@ class LibraryPresenter(
private fun deleteChapters(manga: Manga, chapters: List<Chapter>) { private fun deleteChapters(manga: Manga, chapters: List<Chapter>) {
sourceManager.get(manga.source)?.let { source -> 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 -> mangaToDelete.forEach { manga ->
val source = sourceManager.get(manga.source) as? HttpSource val source = sourceManager.get(manga.source) as? HttpSource
if (source != null) { 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 --> // SY -->
/** Returns first unread chapter of a manga */ /** Returns first unread chapter of a manga */
fun getFirstUnread(manga: Manga): Chapter? { 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) { return if (manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID) {
val chapter = chapters.sortedBy { it.source_order }.getOrNull(0) val chapter = chapters.sortedBy { it.source_order }.getOrNull(0)
if (chapter?.read == false) chapter else null if (chapter?.read == false) chapter else null

View File

@ -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.FabController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.ToolbarLiftOnScrollController 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.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController 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
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.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog 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.MangaInfoButtonsAdapter
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoHeaderAdapter import eu.kanade.tachiyomi.ui.manga.info.MangaInfoHeaderAdapter
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoItemAdapter 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.manga.track.TrackController
import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.ui.recent.history.HistoryController 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.getCoordinates
import eu.kanade.tachiyomi.util.view.shrinkOnScroll import eu.kanade.tachiyomi.util.view.shrinkOnScroll
import eu.kanade.tachiyomi.util.view.snack import eu.kanade.tachiyomi.util.view.snack
import exh.MERGED_SOURCE_ID
import exh.isEhBasedSource import exh.isEhBasedSource
import exh.metadata.metadata.base.FlatMetadata import exh.metadata.metadata.base.FlatMetadata
import java.io.IOException import java.io.IOException
@ -195,6 +199,8 @@ class MangaController :
private var editMangaDialog: EditMangaDialog? = null private var editMangaDialog: EditMangaDialog? = null
private var editMergedSettingsDialog: EditMergedSettingsDialog? = null
private var currentAnimator: Animator? = null private var currentAnimator: Animator? = null
// EXH <-- // EXH <--
@ -423,6 +429,8 @@ class MangaController :
// SY --> // SY -->
if (presenter.manga.favorite) menu.findItem(R.id.action_edit).isVisible = true if (presenter.manga.favorite) menu.findItem(R.id.action_edit).isVisible = true
if (preferences.recommendsInOverflow().get()) menu.findItem(R.id.action_recommend).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 <-- // SY <--
} }
@ -443,6 +451,16 @@ class MangaController :
R.id.action_recommend -> { R.id.action_recommend -> {
openRecommends() 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 <-- // SY <--
R.id.action_edit_categories -> onCategoriesClick() R.id.action_edit_categories -> onCategoriesClick()
@ -633,7 +651,7 @@ class MangaController :
Bundle().apply { Bundle().apply {
putParcelable(SourceController.SMART_SEARCH_CONFIG, smartSearchConfig) 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!!) presenter.smartSearchMerge(presenter.manga, smartSearchConfig?.origMangaId!!)
} }
router?.pushController( router?.popControllerWithTag(SMART_SEARCH_SOURCE_TAG)
router?.popCurrentController()
router?.replaceTopController(
MangaController( MangaController(
mergedManga, mergedManga,
true, true,

View File

@ -4,7 +4,6 @@ import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import com.elvishew.xlog.XLog import com.elvishew.xlog.XLog
import com.google.gson.Gson
import com.jakewharton.rxrelay.BehaviorRelay import com.jakewharton.rxrelay.BehaviorRelay
import com.jakewharton.rxrelay.PublishRelay import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.tachiyomi.data.cache.CoverCache 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.data.track.TrackManager
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source 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
import eu.kanade.tachiyomi.source.online.MetadataSource.Companion.isMetadataSource import eu.kanade.tachiyomi.source.online.MetadataSource.Companion.isMetadataSource
import eu.kanade.tachiyomi.source.online.all.MergedSource import eu.kanade.tachiyomi.source.online.all.MergedSource
@ -37,10 +37,12 @@ import exh.MERGED_SOURCE_ID
import exh.debug.DebugToggles import exh.debug.DebugToggles
import exh.eh.EHentaiUpdateHelper import exh.eh.EHentaiUpdateHelper
import exh.isEhBasedSource import exh.isEhBasedSource
import exh.merged.sql.models.MergedMangaReference
import exh.metadata.metadata.base.FlatMetadata import exh.metadata.metadata.base.FlatMetadata
import exh.metadata.metadata.base.RaisedSearchMetadata import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.metadata.metadata.base.getFlatMetadataForManga import exh.metadata.metadata.base.getFlatMetadataForManga
import exh.source.EnhancedHttpSource import exh.source.EnhancedHttpSource
import exh.util.asObservable
import exh.util.await import exh.util.await
import exh.util.trimOrNull import exh.util.trimOrNull
import java.util.Date import java.util.Date
@ -63,9 +65,7 @@ class MangaPresenter(
private val trackManager: TrackManager = Injekt.get(), private val trackManager: TrackManager = Injekt.get(),
private val downloadManager: DownloadManager = Injekt.get(), private val downloadManager: DownloadManager = Injekt.get(),
private val coverCache: CoverCache = Injekt.get(), private val coverCache: CoverCache = Injekt.get(),
// SY --> private val sourceManager: SourceManager = Injekt.get()
private val gson: Gson = Injekt.get()
// SY <--
) : BasePresenter<MangaController>() { ) : BasePresenter<MangaController>() {
/** /**
@ -112,6 +112,10 @@ class MangaPresenter(
data class EXHRedirect(val manga: Manga, val update: Boolean) data class EXHRedirect(val manga: Manga, val update: Boolean)
var meta: RaisedSearchMetadata? = null var meta: RaisedSearchMetadata? = null
private var mergedManga = emptyList<Manga>()
var dedupe: Boolean = true
// EXH <-- // EXH <--
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
@ -121,6 +125,10 @@ class MangaPresenter(
if (manga.initialized && source.isMetadataSource()) { if (manga.initialized && source.isMetadataSource()) {
getMangaMetaObservable().subscribeLatestCache({ view, flatMetadata -> if (flatMetadata != null) view.onNextMetaInfo(flatMetadata) else XLog.d("Invalid metadata") }) 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 <-- // SY <--
// Manga info - start // Manga info - start
@ -145,7 +153,7 @@ class MangaPresenter(
// Add the subscription that retrieves the chapters from the database, keeps subscribed to // Add the subscription that retrieves the chapters from the database, keeps subscribed to
// changes, and sends the list of chapters to the relay. // changes, and sends the list of chapters to the relay.
add( add(
db.getChapters(manga).asRxObservable() (/* SY --> */if (source is MergedSource) source.getChaptersFromDB(manga, true, dedupe).asObservable() else /* SY <-- */ db.getChapters(manga).asRxObservable())
.map { chapters -> .map { chapters ->
// Convert every chapter to a model. // Convert every chapter to a model.
chapters.map { it.toModel() } chapters.map { it.toModel() }
@ -334,64 +342,138 @@ class MangaPresenter(
} }
suspend fun smartSearchMerge(manga: Manga, originalMangaId: Long): Manga { suspend fun smartSearchMerge(manga: Manga, originalMangaId: Long): Manga {
val originalManga = db.getManga(originalMangaId).await() val originalManga = db.getManga(originalMangaId).await() ?: throw IllegalArgumentException("Unknown manga ID: $originalMangaId")
?: throw IllegalArgumentException("Unknown manga ID: $originalMangaId") if (originalManga.source == MERGED_SOURCE_ID) {
val toInsert = if (originalManga.source == MERGED_SOURCE_ID) { val children = db.getMergedMangaReferences(originalMangaId).await()
originalManga.apply { if (children.any { it.mangaSourceId == manga.source && it.mangaUrl == manga.url }) {
val originalChildren = MergedSource.MangaConfig.readFromUrl(gson, url).children throw IllegalArgumentException("This manga is already merged with the current manga!")
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)
} }
} else {
val newMangaConfig = MergedSource.MangaConfig( val mangaReferences = mutableListOf(
listOf( MergedMangaReference(
MergedSource.MangaSource( id = null,
originalManga.source, isInfoManga = false,
originalManga.url getChapterUpdates = true,
), chapterSortMode = 0,
MergedSource.MangaSource( chapterPriority = 0,
manga.source, downloadChapters = true,
manga.url 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) copyFrom(originalManga)
favorite = true favorite = true
last_update = originalManga.last_update last_update = originalManga.last_update
viewer = originalManga.viewer viewer = originalManga.viewer
chapter_flags = originalManga.chapter_flags chapter_flags = originalManga.chapter_flags
sorting = Manga.SORTING_NUMBER 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 // 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) { fun updateMergeSettings(mergeReference: MergedMangaReference?, mergedMangaReferences: List<MergedMangaReference>) {
if (toInsert.id != null) { launchIO {
db.deleteManga(toInsert).await() mergeReference?.let {
} db.updateMergeMangaSettings(it).await()
} }
if (mergedMangaReferences.isNotEmpty()) db.updateMergedMangaSettings(mergedMangaReferences).await()
return existingManga
} }
}
// Reload chapters immediately fun toggleDedupe() {
toInsert.initialized = false // I cant find any way to call the chapter list subscription to get the chapters again
val newId = db.insertManga(toInsert).await().insertedId()
if (newId != null) toInsert.id = newId
return toInsert
} }
// SY <-- // SY <--
@ -424,7 +506,13 @@ class MangaPresenter(
* Deletes all the downloads for the manga. * Deletes all the downloads for the manga.
*/ */
fun deleteDownloads() { 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 // Chapters list - start
private fun observeDownloads() { 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?.let { remove(it) }
observeDownloadsSubscription = downloadManager.queue.getStatusObservable() observeDownloadsSubscription = downloadManager.queue.getStatusObservable()
.observeOn(AndroidSchedulers.mainThread()) .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) } .doOnNext { onDownloadStatusChange(it) }
.subscribeLatestCache(MangaController::onChapterStatusChange) { _, error -> .subscribeLatestCache(MangaController::onChapterStatusChange) { _, error ->
Timber.e(error) Timber.e(error)
@ -548,8 +640,11 @@ class MangaPresenter(
* @param chapters the list of chapter from the database. * @param chapters the list of chapter from the database.
*/ */
private fun setDownloadedChapters(chapters: List<ChapterItem>) { private fun setDownloadedChapters(chapters: List<ChapterItem>) {
// SY -->
val isMergedSource = source is MergedSource
// SY <--
chapters 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 } .forEach { it.status = Download.DOWNLOADED }
} }
@ -560,21 +655,38 @@ class MangaPresenter(
hasRequested = true hasRequested = true
if (!fetchChaptersSubscription.isNullOrUnsubscribed()) return if (!fetchChaptersSubscription.isNullOrUnsubscribed()) return
fetchChaptersSubscription = Observable.defer { source.fetchChapterList(manga) } fetchChaptersSubscription = /* SY --> */ if (source !is MergedSource) {
.subscribeOn(Schedulers.io()) // SY <--
.map { syncChaptersWithSource(db, it, manga, source) } Observable.defer { source.fetchChapterList(manga) }
.doOnNext { .subscribeOn(Schedulers.io())
if (manualFetch) { .map { syncChaptersWithSource(db, it, manga, source) }
downloadNewChapters(it.first) .doOnNext {
if (manualFetch) {
downloadNewChapters(it.first)
}
} }
} .observeOn(AndroidSchedulers.mainThread())
.observeOn(AndroidSchedulers.mainThread()) .subscribeFirst(
.subscribeFirst( { view, _ ->
{ view, _ -> view.onFetchChaptersDone()
view.onFetchChaptersDone() },
}, MangaController::onFetchChaptersError
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. * @param chapters the list of chapters to download.
*/ */
fun downloadChapters(chapters: List<Chapter>) { fun downloadChapters(chapters: List<Chapter>) {
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)
} }
/** /**

View File

@ -8,6 +8,7 @@ import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.kanade.tachiyomi.R 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.database.models.Manga
import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.data.glide.MangaThumbnail 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.SourceManager
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource 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.ui.manga.MangaController
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.view.setTooltip import eu.kanade.tachiyomi.util.view.setTooltip
@ -41,6 +41,10 @@ class MangaInfoHeaderAdapter(
RecyclerView.Adapter<MangaInfoHeaderAdapter.HeaderViewHolder>() { RecyclerView.Adapter<MangaInfoHeaderAdapter.HeaderViewHolder>() {
private val trackManager: TrackManager by injectLazy() 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 manga: Manga = controller.presenter.manga
private var source: Source = controller.presenter.source private var source: Source = controller.presenter.source
@ -254,9 +258,9 @@ class MangaInfoHeaderAdapter(
val mangaSource = source?.toString() val mangaSource = source?.toString()
with(binding.mangaSource) { with(binding.mangaSource) {
// SY --> // SY -->
if (source != null && source.id == MERGED_SOURCE_ID) { if (source?.id == MERGED_SOURCE_ID) {
text = MergedSource.MangaConfig.readFromUrl(Injekt.get(), manga.url).children.map { text = db.getMergedMangaReferences(manga.id!!).executeAsBlocking().map {
Injekt.get<SourceManager>().getOrStub(it.source).toString() sourceManager.getOrStub(it.mangaSourceId).toString()
}.distinct().joinToString() }.distinct().joinToString()
} else /* SY <-- */ if (mangaSource != null) { } else /* SY <-- */ if (mangaSource != null) {
text = mangaSource text = mangaSource

View File

@ -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<EditMergedMangaItem>(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)
}
}
}
}

View File

@ -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<SourceManager>().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)
}
}

View File

@ -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<EditMergedMangaHolder>() {
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<IFlexible<RecyclerView.ViewHolder>>): EditMergedMangaHolder {
binding = EditMergedSettingsItemBinding.bind(view)
return EditMergedMangaHolder(binding.root, adapter as EditMergedMangaAdapter)
}
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>?,
holder: EditMergedMangaHolder,
position: Int,
payloads: MutableList<Any>?
) {
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
}
}

View File

@ -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<Pair<Manga?, MergedMangaReference>> = 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<Manga?, MergedMangaReference>.toModel(): EditMergedMangaItem {
return EditMergedMangaItem(first, second)
}
private companion object {
const val KEY_MANGA = "manga_id"
}
}

View File

@ -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<EditMergedSettingsHeaderAdapter.HeaderViewHolder>() {
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<String> = 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<String> = 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)
}
}

View File

@ -17,6 +17,7 @@ import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Page 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.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.reader.chapter.ReaderChapterItem import eu.kanade.tachiyomi.ui.reader.chapter.ReaderChapterItem
import eu.kanade.tachiyomi.ui.reader.loader.ChapterLoader 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 eu.kanade.tachiyomi.util.updateCoverLastModified
import exh.EH_SOURCE_ID import exh.EH_SOURCE_ID
import exh.EXH_SOURCE_ID import exh.EXH_SOURCE_ID
import exh.MERGED_SOURCE_ID
import exh.util.defaultReaderType import exh.util.defaultReaderType
import java.io.File import java.io.File
import java.text.DecimalFormat import java.text.DecimalFormat
@ -39,6 +41,8 @@ import java.text.DecimalFormatSymbols
import java.util.Date import java.util.Date
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.singleOrNull
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import rx.Completable import rx.Completable
import rx.Observable import rx.Observable
@ -97,7 +101,7 @@ class ReaderPresenter(
*/ */
private val chapterList by lazy { private val chapterList by lazy {
val manga = manga!! 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 } val selectedChapter = dbChapters.find { it.id == chapterId }
?: error("Requested chapter of id $chapterId not found in chapter list") ?: error("Requested chapter of id $chapterId not found in chapter list")
@ -236,7 +240,9 @@ class ReaderPresenter(
val context = Injekt.get<Application>() val context = Injekt.get<Application>()
val source = sourceManager.getOrStub(manga.source) 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) Observable.just(manga).subscribeLatestCache(ReaderActivity::setManga)
viewerChaptersRelay.subscribeLatestCache(ReaderActivity::setChapters) viewerChaptersRelay.subscribeLatestCache(ReaderActivity::setChapters)

View File

@ -6,9 +6,12 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source 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.HttpSource
import eu.kanade.tachiyomi.source.online.all.MergedSource
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import exh.debug.DebugFunctions.prefs import exh.debug.DebugFunctions.prefs
import exh.merged.sql.models.MergedMangaReference
import rx.Completable import rx.Completable
import rx.Observable import rx.Observable
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
@ -22,7 +25,12 @@ class ChapterLoader(
private val context: Context, private val context: Context,
private val downloadManager: DownloadManager, private val downloadManager: DownloadManager,
private val manga: Manga, private val manga: Manga,
private val source: Source private val source: Source,
// SY -->
private val sourceManager: SourceManager,
private val mergedReferences: List<MergedMangaReference>,
private val mergedManga: List<Manga>
// SY <--
) { ) {
/** /**
@ -81,6 +89,27 @@ class ChapterLoader(
private fun getPageLoader(chapter: ReaderChapter): PageLoader { private fun getPageLoader(chapter: ReaderChapter): PageLoader {
val isDownloaded = downloadManager.isChapterDownloaded(chapter.chapter, manga, true) val isDownloaded = downloadManager.isChapterDownloaded(chapter.chapter, manga, true)
return when { 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) isDownloaded -> DownloadPageLoader(chapter, manga, source, downloadManager)
source is HttpSource -> HttpPageLoader(chapter, source) source is HttpSource -> HttpPageLoader(chapter, source)
source is LocalSource -> source.getFormat(chapter.chapter).let { format -> source is LocalSource -> source.getFormat(chapter.chapter).let { format ->

View File

@ -2,6 +2,9 @@ package exh
import android.content.Context import android.content.Context
import com.elvishew.xlog.XLog 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.Query
import com.pushtorefresh.storio.sqlite.queries.RawQuery import com.pushtorefresh.storio.sqlite.queries.RawQuery
import eu.kanade.tachiyomi.BuildConfig 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.MangaImpl
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.resolvers.MangaUrlPutResolver 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.database.tables.MangaTable
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.updater.UpdaterJob import eu.kanade.tachiyomi.data.updater.UpdaterJob
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob 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.Hitomi
import eu.kanade.tachiyomi.source.online.all.NHentai import eu.kanade.tachiyomi.source.online.all.NHentai
import exh.merged.sql.models.MergedMangaReference
import exh.source.BlacklistedSources import exh.source.BlacklistedSources
import java.io.File import java.io.File
import java.net.URI import java.net.URI
@ -27,6 +35,8 @@ import uy.kohesive.injekt.injectLazy
object EXHMigrations { object EXHMigrations {
private val db: DatabaseHelper by injectLazy() private val db: DatabaseHelper by injectLazy()
private val sourceManager: SourceManager by injectLazy()
private val gson: Gson by injectLazy()
private val logger = XLog.tag("EXHMigrations") 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<Manga>()
val mergedMangaReferences = mutableListOf<MergedMangaReference>()
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<Chapter>()
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) // if (oldVersion < 1) { } (1 is current release version)
// do stuff here when releasing changed crap // do stuff here when releasing changed crap
@ -228,6 +338,57 @@ object EXHMigrations {
orig 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<MangaSource>
) {
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( data class BackupEntry(

View File

@ -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<MergedMangaReference>() {
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)
}
}

View File

@ -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<MergedMangaReference>() {
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)
}
}

View File

@ -104,17 +104,30 @@ suspend fun Completable.awaitSuspending(subscribeOn: Scheduler? = null) {
suspend fun Completable.awaitCompleted(): Unit = suspendCancellableCoroutine { cont -> suspend fun Completable.awaitCompleted(): Unit = suspendCancellableCoroutine { cont ->
subscribe(object : CompletableSubscriber { subscribe(object : CompletableSubscriber {
override fun onSubscribe(s: Subscription) { cont.unsubscribeOnCancellation(s) } override fun onSubscribe(s: Subscription) {
override fun onCompleted() { cont.resume(Unit) } cont.unsubscribeOnCancellation(s)
override fun onError(e: Throwable) { cont.resumeWithException(e) } }
override fun onCompleted() {
cont.resume(Unit)
}
override fun onError(e: Throwable) {
cont.resumeWithException(e)
}
}) })
} }
suspend fun <T> Single<T>.await(): T = suspendCancellableCoroutine { cont -> suspend fun <T> Single<T>.await(): T = suspendCancellableCoroutine { cont ->
cont.unsubscribeOnCancellation( cont.unsubscribeOnCancellation(
subscribe(object : SingleSubscriber<T>() { subscribe(object : SingleSubscriber<T>() {
override fun onSuccess(t: T) { cont.resume(t) } override fun onSuccess(t: T) {
override fun onError(error: Throwable) { cont.resumeWithException(error) } cont.resume(t)
}
override fun onError(error: Throwable) {
cont.resumeWithException(error)
}
}) })
) )
} }
@ -129,7 +142,11 @@ suspend fun <T> Observable<T>.awaitFirstOrDefault(default: T): T = firstOrDefaul
suspend fun <T> Observable<T>.awaitFirstOrNull(): T? = firstOrDefault(null).awaitOne() suspend fun <T> Observable<T>.awaitFirstOrNull(): T? = firstOrDefault(null).awaitOne()
@OptIn(InternalCoroutinesApi::class, ExperimentalCoroutinesApi::class) @OptIn(InternalCoroutinesApi::class, ExperimentalCoroutinesApi::class)
suspend fun <T> Observable<T>.awaitFirstOrElse(defaultValue: () -> T): T = switchIfEmpty(Observable.fromCallable(defaultValue)).first().awaitOne() suspend fun <T> Observable<T>.awaitFirstOrElse(defaultValue: () -> T): T = switchIfEmpty(
Observable.fromCallable(
defaultValue
)
).first().awaitOne()
@OptIn(InternalCoroutinesApi::class, ExperimentalCoroutinesApi::class) @OptIn(InternalCoroutinesApi::class, ExperimentalCoroutinesApi::class)
suspend fun <T> Observable<T>.awaitLast(): T = last().awaitOne() suspend fun <T> Observable<T>.awaitLast(): T = last().awaitOne()
@ -141,11 +158,24 @@ suspend fun <T> Observable<T>.awaitSingle(): T = single().awaitOne()
private suspend fun <T> Observable<T>.awaitOne(): T = suspendCancellableCoroutine { cont -> private suspend fun <T> Observable<T>.awaitOne(): T = suspendCancellableCoroutine { cont ->
cont.unsubscribeOnCancellation( cont.unsubscribeOnCancellation(
subscribe(object : Subscriber<T>() { subscribe(object : Subscriber<T>() {
override fun onStart() { request(1) } override fun onStart() {
override fun onNext(t: T) { cont.resume(t) } request(1)
override fun onCompleted() { if (cont.isActive) cont.resumeWithException(IllegalStateException("Should have invoked onNext")) } }
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) { override fun onError(e: Throwable) {
/* /*
* Rx1 observable throws NoSuchElementException if cancellation happened before * Rx1 observable throws NoSuchElementException if cancellation happened before
* element emission. To mitigate this we try to atomically resume continuation with exception: * element emission. To mitigate this we try to atomically resume continuation with exception:
* if resume failed, then we know that continuation successfully cancelled itself * if resume failed, then we know that continuation successfully cancelled itself
@ -185,7 +215,7 @@ fun <T : Any> Observable<T>.asFlow(): Flow<T> = callbackFlow {
fun <T : Any> Flow<T>.asObservable(backpressureMode: Emitter.BackpressureMode = Emitter.BackpressureMode.NONE): Observable<T> { fun <T : Any> Flow<T>.asObservable(backpressureMode: Emitter.BackpressureMode = Emitter.BackpressureMode.NONE): Observable<T> {
return Observable.create( return Observable.create(
{ emitter -> { emitter ->
/* /*
* ATOMIC is used here to provide stable behaviour of subscribe+dispose pair even if * ATOMIC is used here to provide stable behaviour of subscribe+dispose pair even if
* asObservable is already invoked from unconfined * asObservable is already invoked from unconfined
*/ */

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/divider"/>
</LinearLayout>

View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/holder"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="40dp"
android:padding="8dp"
android:layout_gravity="center_horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="@string/allow_deduplication"
android:layout_marginEnd="8dp"
android:gravity="center" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/dedupe_switch"
android:layout_width="wrap_content"
android:layout_height="match_parent" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="@string/deduplication_mode"
android:padding="8dp"
android:layout_gravity="center_horizontal"
android:gravity="center"/>
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/dedupe_mode_spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:padding="8dp"
style="@style/Widget.AppCompat.Spinner.DropDown"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="@string/manga_info_manga"
android:padding="8dp"
android:gravity="center"
android:layout_gravity="center_horizontal"/>
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/manga_info_spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:padding="8dp"
style="@style/Widget.AppCompat.Spinner.DropDown"/>
</LinearLayout>

View File

@ -0,0 +1,138 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/holder"
style="@style/Theme.Widget.CardView.Item"
android:padding="0dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="@+id/reorder"
android:layout_width="@dimen/material_component_lists_single_line_with_avatar_height"
android:layout_height="0dp"
android:layout_alignParentStart="true"
android:layout_gravity="start"
android:scaleType="center"
android:alpha="1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_drag_handle_24dp"
app:tint="?android:attr/textColorHint" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/infoLayout"
android:layout_width="0dp"
android:layout_height="80dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/reorder"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/cover"
android:layout_width="0dp"
android:layout_height="match_parent"
android:contentDescription="@string/description_cover"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="h,3:2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@mipmap/ic_launcher" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="8dp"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/cover"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="2"
android:textAppearance="@style/TextAppearance.Medium"
tools:text="Title" />
<TextView
android:id="@+id/subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:maxLines="2"
tools:text="Subtitle" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="0dp"
android:layout_height="40dp"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/reorder"
app:layout_constraintTop_toBottomOf="@+id/infoLayout">
<ImageButton
android:id="@+id/remove"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/action_delete"
android:padding="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/download"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_delete_24dp"
app:tint="?android:attr/textColorPrimary" />
<ImageButton
android:id="@+id/download"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/pref_download_new"
android:padding="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/get_chapter_updates"
app:layout_constraintStart_toEndOf="@+id/remove"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_get_app_24dp"
app:tint="?android:attr/textColorPrimary" />
<ImageButton
android:id="@+id/get_chapter_updates"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/fetch_chapter_updates"
android:padding="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/download"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_sync_24dp"
app:tint="?android:attr/textColorPrimary" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>

View File

@ -39,16 +39,14 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:text="Searching source..." android:text="Searching source..."
android:textAppearance="@style/TextAppearance.Medium.Title" android:textAppearance="@style/TextAppearance.Medium.Title" />
android:textColor="@android:color/white" />
<ProgressBar <ProgressBar
android:id="@+id/intercept_progress" android:id="@+id/intercept_progress"
style="?android:attr/progressBarStyleLarge" style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center" />
android:indeterminateTint="@android:color/white" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@ -57,4 +57,18 @@
android:title="@string/az_recommends" android:title="@string/az_recommends"
android:visible="false" android:visible="false"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:id="@+id/action_merged"
android:icon="@drawable/ic_edit_24dp"
android:title="@string/merge_settings"
android:visible="false"
app:showAsAction="never" />
<item
android:id="@+id/action_toggle_dedupe"
android:icon="@drawable/ic_edit_24dp"
android:title="@string/toggle_dedupe"
android:visible="false"
app:showAsAction="never" />
</menu> </menu>

View File

@ -483,4 +483,23 @@
<item quantity="other">%2$s, %1$d pages</item> <item quantity="other">%2$s, %1$d pages</item>
</plurals> </plurals>
<!-- Merged manga -->
<string name="merge_settings">Merge settings</string>
<string name="fetch_chapter_updates">Fetch chapter updates</string>
<string name="delete_merged_manga">Are you sure?</string>
<string name="delete_merged_manga_desc">This will remove the manga from the merge, using this will also lose any unsaved changes applied to the merged manga</string>
<string name="chapter_updates_merged_manga">Toggle chapter updates</string>
<string name="chapter_updates_merged_manga_desc">Toggling this will disable or enable chapter updates for this merged manga</string>
<string name="download_merged_manga">Toggle new chapter downloads</string>
<string name="download_merged_manga_desc">Toggling this will disable or enable chapter downloads for this merged manga</string>
<string name="merged_references_invalid">Merged references invalid</string>
<string name="merged_chapter_updates_error">Toggle chapter updates error</string>
<string name="merged_toggle_chapter_updates_find_error">Could not find manga to toggle chapter updates</string>
<string name="merged_toggle_download_chapters_error">Toggle download chapters error</string>
<string name="merged_toggle_download_chapters_find_error">Could not find manga to toggle chapter downloads</string>
<string name="allow_deduplication">Allow deduplication:</string>
<string name="deduplication_mode">Dedupe mode:</string>
<string name="manga_info_manga">Info manga:</string>
<string name="toggle_dedupe">Toggle dedupe</string>
</resources> </resources>