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
targetSdkVersion AndroidConfig.targetSdk
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
versionCode 6
versionCode 7
versionName "1.2.0"
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.HISTORY
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA
import eu.kanade.tachiyomi.data.backup.models.Backup.MERGEDMANGAREFERENCES
import eu.kanade.tachiyomi.data.backup.models.Backup.SAVEDSEARCHES
import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
import eu.kanade.tachiyomi.data.backup.models.DHistory
@ -39,6 +40,7 @@ import eu.kanade.tachiyomi.data.backup.serializer.CategoryTypeAdapter
import eu.kanade.tachiyomi.data.backup.serializer.ChapterTypeAdapter
import eu.kanade.tachiyomi.data.backup.serializer.HistoryTypeAdapter
import eu.kanade.tachiyomi.data.backup.serializer.MangaTypeAdapter
import eu.kanade.tachiyomi.data.backup.serializer.MergedMangaReferenceTypeAdapter
import eu.kanade.tachiyomi.data.backup.serializer.TrackTypeAdapter
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
@ -57,11 +59,17 @@ import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.all.EHentai
import eu.kanade.tachiyomi.source.online.all.MergedSource
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import exh.EXHSavedSearch
import exh.MERGED_SOURCE_ID
import exh.eh.EHentaiThrottleManager
import exh.merged.sql.models.MergedMangaReference
import exh.util.asObservable
import java.lang.RuntimeException
import kotlin.math.max
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import rx.Observable
import timber.log.Timber
import uy.kohesive.injekt.Injekt
@ -106,6 +114,9 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
// SY -->
.registerTypeAdapter<MergedMangaReference>(MergedMangaReferenceTypeAdapter.build())
// SY <--
.create()
else -> throw Exception("Json version unknown")
}
@ -129,15 +140,21 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
// Create extension ID/name mapping
val extensionEntries = JsonArray()
// Merged Manga References
val mergedMangaReferenceEntries = JsonArray()
// Add value's to root
root[Backup.VERSION] = CURRENT_VERSION
root[Backup.MANGAS] = mangaEntries
root[CATEGORIES] = categoryEntries
root[EXTENSIONS] = extensionEntries
// SY -->
root[MERGEDMANGAREFERENCES] = mergedMangaReferenceEntries
// SY <--
databaseHelper.inTransaction {
// Get manga from database
val mangas = getFavoriteManga()
val mangas = getFavoriteManga() /* SY --> */ + getMergedManga() /* SY <-- */
val extensions: MutableSet<String> = mutableSetOf()
@ -163,6 +180,8 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
// SY -->
root[SAVEDSEARCHES] =
Injekt.get<PreferencesHelper>().eh_savedSearches().get().joinToString(separator = "***")
backupMergedMangaReferences(mergedMangaReferenceEntries)
// SY <--
}
@ -212,6 +231,13 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
}
}
// SY -->
private fun backupMergedMangaReferences(root: JsonArray) {
val mergedMangaReferences = databaseHelper.getMergedMangaReferences().executeAsBlocking()
mergedMangaReferences.forEach { root.add(parser.toJsonTree(it)) }
}
// SY <--
/**
* Backup the categories of library
*
@ -317,29 +343,40 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
*/
fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>, throttleManager: EHentaiThrottleManager): Observable<Pair<List<Chapter>, List<Chapter>>> {
// SY -->
return (
if (source is EHentai) {
source.fetchChapterList(manga, throttleManager::throttle)
} else {
source.fetchChapterList(manga)
}
).map {
if (it.last().chapter_number == -99F) {
chapters.forEach { chapter ->
chapter.name = "Chapter ${chapter.chapter_number} restored by dummy source"
}
syncChaptersWithSource(databaseHelper, chapters, manga, source)
} else {
syncChaptersWithSource(databaseHelper, it, manga, source)
}
}
// SY <--
.doOnNext { pair ->
if (source is MergedSource) {
val syncedChapters = runBlocking { source.fetchChaptersAndSync(manga, false) }
return syncedChapters.onEach { pair ->
if (pair.first.isNotEmpty()) {
chapters.forEach { it.manga_id = manga.id }
insertChapters(chapters)
}
}.asObservable()
} else {
return (
if (source is EHentai) {
source.fetchChapterList(manga, throttleManager::throttle)
} else {
source.fetchChapterList(manga)
}
).map {
if (it.last().chapter_number == -99F) {
chapters.forEach { chapter ->
chapter.name =
"Chapter ${chapter.chapter_number} restored by dummy source"
}
syncChaptersWithSource(databaseHelper, chapters, manga, source)
} else {
syncChaptersWithSource(databaseHelper, it, manga, source)
}
}
// SY <--
.doOnNext { pair ->
if (pair.first.isNotEmpty()) {
chapters.forEach { it.manga_id = manga.id }
insertChapters(chapters)
}
}
}
}
/**
@ -584,6 +621,49 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
}
preferences.eh_savedSearches().set((otherSerialized + newSerialized).toSet())
}
/**
* Restore the categories from Json
*
* @param jsonMergedMangaReferences array containing md manga references
*/
internal fun restoreMergedMangaReferences(jsonMergedMangaReferences: JsonArray) {
// Get merged manga references from file and from db
val dbMergedMangaReferences = databaseHelper.getMergedMangaReferences().executeAsBlocking()
val backupMergedMangaReferences = parser.fromJson<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 <--
/**
@ -602,6 +682,9 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
internal fun getFavoriteManga(): List<Manga> =
databaseHelper.getFavoriteMangas().executeAsBlocking()
internal fun getMergedManga(): List<Manga> =
databaseHelper.getMergedMangas().executeAsBlocking()
/**
* Inserts manga and returns id
*

View File

@ -238,7 +238,7 @@ class BackupRestoreService : Service() {
}
totalAmount = mangasJson.size()
restoreAmount = validManga.count() + 1 // +1 for categories
restoreAmount = validManga.count() + 3 // +1 for categories, +1 for saved searches, +1 for merged manga references
skippedAmount = mangasJson.size() - validManga.count()
// SY <--
restoreProgress = 0
@ -288,6 +288,15 @@ class BackupRestoreService : Service() {
restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.saved_searches))
}
private fun restoreMergedMangaReferences(mergedMangaReferencesJson: JsonElement) {
db.inTransaction {
backupManager.restoreMergedMangaReferences(mergedMangaReferencesJson.asJsonArray)
}
restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.categories))
}
// SY <--
private fun restoreManga(mangaJson: JsonObject) {

View File

@ -19,6 +19,7 @@ object Backup {
const val VERSION = "version"
// SY -->
const val SAVEDSEARCHES = "savedsearches"
const val MERGEDMANGAREFERENCES = "mergedmangareferences"
// SY <--
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.MangaCategoryTable as MangaCategory
import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga
import exh.MERGED_SOURCE_ID
import exh.merged.sql.tables.MergedTable as Merged
// SY -->
@ -21,6 +22,9 @@ fun getMergedMangaQuery() =
ON ${Manga.TABLE}.${Manga.COL_ID} = M.${Merged.COL_MANGA_ID}
"""
/**
* Query to get all the manga that are merged into other manga
*/
fun getAllMergedMangaQuery() =
"""
SELECT ${Manga.TABLE}.*
@ -56,7 +60,6 @@ fun getMergedChaptersQuery() =
JOIN ${Chapter.TABLE}
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = M.${Merged.COL_MANGA_ID}
"""
// SY <--
/**
* Query to get the manga from the library, with their categories and unread count.
@ -66,29 +69,54 @@ val libraryQuery =
SELECT M.*, COALESCE(MC.${MangaCategory.COL_CATEGORY_ID}, 0) AS ${Manga.COL_CATEGORY}
FROM (
SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD}, COALESCE(R.read, 0) AS ${Manga.COL_READ}
FROM ${Manga.TABLE}
LEFT JOIN (
SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS unread
FROM ${Chapter.TABLE}
WHERE ${Chapter.COL_READ} = 0
GROUP BY ${Chapter.COL_MANGA_ID}
) AS C
ON ${Manga.COL_ID} = C.${Chapter.COL_MANGA_ID}
LEFT JOIN (
SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS read
FROM ${Chapter.TABLE}
WHERE ${Chapter.COL_READ} = 1
GROUP BY ${Chapter.COL_MANGA_ID}
) AS R
ON ${Manga.COL_ID} = R.${Chapter.COL_MANGA_ID}
WHERE ${Manga.COL_FAVORITE} = 1
GROUP BY ${Manga.COL_ID}
FROM ${Manga.TABLE}
LEFT JOIN (
SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}, COUNT(*) AS unread
FROM ${Chapter.TABLE}
WHERE ${Chapter.COL_READ} = 0
GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
) AS C
ON ${Manga.TABLE}.${Manga.COL_ID} = C.${Chapter.COL_MANGA_ID}
LEFT JOIN (
SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS read
FROM ${Chapter.TABLE}
WHERE ${Chapter.COL_READ} = 1
GROUP BY ${Chapter.COL_MANGA_ID}
) AS R
ON ${Manga.TABLE}.${Manga.COL_ID} = R.${Chapter.COL_MANGA_ID}
WHERE ${Manga.COL_FAVORITE} = 1 AND ${Manga.COL_SOURCE} <> $MERGED_SOURCE_ID
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
UNION
SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD}, COALESCE(R.read, 0) AS ${Manga.COL_READ}
FROM ${Manga.TABLE}
LEFT JOIN (
SELECT ${Merged.TABLE}.${Merged.COL_MERGE_ID}, COUNT(*) as unread
FROM ${Merged.TABLE}
JOIN ${Chapter.TABLE}
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ${Merged.TABLE}.${Merged.COL_MANGA_ID}
WHERE ${Chapter.TABLE}.${Chapter.COL_READ} = 0
GROUP BY ${Merged.TABLE}.${Merged.COL_MERGE_ID}
) AS C
ON ${Manga.TABLE}.${Manga.COL_ID} = C.${Merged.COL_MERGE_ID}
LEFT JOIN (
SELECT ${Merged.TABLE}.${Merged.COL_MERGE_ID}, COUNT(*) as read
FROM ${Merged.TABLE}
JOIN ${Chapter.TABLE}
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ${Merged.TABLE}.${Merged.COL_MANGA_ID}
WHERE ${Chapter.TABLE}.${Chapter.COL_READ} = 1
GROUP BY ${Merged.TABLE}.${Merged.COL_MERGE_ID}
) AS R
ON ${Manga.TABLE}.${Manga.COL_ID} = R.${Merged.COL_MERGE_ID}
WHERE ${Manga.COL_FAVORITE} = 1 AND ${Manga.COL_SOURCE} = $MERGED_SOURCE_ID
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
ORDER BY ${Manga.COL_TITLE}
) AS M
LEFT JOIN (
SELECT * FROM ${MangaCategory.TABLE}) AS MC
ON MC.${MangaCategory.COL_MANGA_ID} = M.${Manga.COL_ID}
SELECT * FROM ${MangaCategory.TABLE}
) AS MC
ON MC.${MangaCategory.COL_MANGA_ID} = M.${Manga.COL_ID};
"""
// SY <--
/**
* Query to get the recent chapters of manga from the library up to a date.

View File

@ -22,6 +22,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.all.MergedSource
import eu.kanade.tachiyomi.ui.library.LibraryGroup
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import eu.kanade.tachiyomi.util.prepUpdateCover
@ -30,9 +31,12 @@ import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.isServiceRunning
import exh.LIBRARY_UPDATE_EXCLUDED_SOURCES
import exh.MERGED_SOURCE_ID
import exh.util.asObservable
import exh.util.nullIfBlank
import java.io.File
import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.runBlocking
import rx.Observable
import rx.Subscription
import rx.schedulers.Schedulers
@ -385,7 +389,12 @@ class LibraryUpdateService(
private fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
// We don't want to start downloading while the library is updating, because websites
// may don't like it and they could ban the user.
downloadManager.downloadChapters(manga, chapters, false)
// SY -->
val chapterFilter = if (manga.source == MERGED_SOURCE_ID) {
db.getMergedMangaReferences(manga.id!!).executeAsBlocking().filterNot { it.downloadChapters }.mapNotNull { it.mangaId }
} else emptyList()
// SY <--
downloadManager.downloadChapters(manga, /* SY --> */ chapters.filter { it.manga_id !in chapterFilter } /* SY <-- */, false)
}
/**
@ -417,7 +426,8 @@ class LibraryUpdateService(
.subscribe()
}
return source.fetchChapterList(manga)
return /* SY --> */ if (source is MergedSource) runBlocking { source.fetchChaptersAndSync(manga, false).asObservable() }
else /* SY <-- */ source.fetchChapterList(manga)
.map { syncChaptersWithSource(db, it, manga, source) }
}

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

View File

@ -1,43 +1,44 @@
package eu.kanade.tachiyomi.source.online.all
import com.elvishew.xlog.XLog
import com.github.salomonbrys.kotson.fromJson
import com.google.gson.Gson
import com.google.gson.annotations.SerializedName
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.SuspendHttpSource
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
import exh.MERGED_SOURCE_ID
import exh.merged.sql.models.MergedMangaReference
import exh.util.asFlow
import exh.util.await
import exh.util.awaitSingle
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flatMapMerge
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.single
import kotlinx.coroutines.flow.singleOrNull
import kotlinx.coroutines.withContext
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.injectLazy
// TODO LocalSource compatibility
// TODO Disable clear database option
class MergedSource : SuspendHttpSource() {
private val db: DatabaseHelper by injectLazy()
private val sourceManager: SourceManager by injectLazy()
private val gson: Gson by injectLazy()
private val downloadManager: DownloadManager by injectLazy()
private val preferences: PreferencesHelper by injectLazy()
override val id: Long = MERGED_SOURCE_ID
@ -49,150 +50,140 @@ class MergedSource : SuspendHttpSource() {
override suspend fun searchMangaParseSuspended(response: Response) = throw UnsupportedOperationException()
override suspend fun latestUpdatesRequestSuspended(page: Int) = throw UnsupportedOperationException()
override suspend fun latestUpdatesParseSuspended(response: Response) = throw UnsupportedOperationException()
override suspend fun mangaDetailsParseSuspended(response: Response) = throw UnsupportedOperationException()
override suspend fun chapterListParseSuspended(response: Response) = throw UnsupportedOperationException()
override suspend fun pageListParseSuspended(response: Response) = throw UnsupportedOperationException()
override suspend fun imageUrlParseSuspended(response: Response) = throw UnsupportedOperationException()
override fun fetchChapterListFlow(manga: SManga) = throw UnsupportedOperationException()
override fun fetchImageFlow(page: Page) = throw UnsupportedOperationException()
override fun fetchImageUrlFlow(page: Page) = throw UnsupportedOperationException()
override fun fetchPageListFlow(chapter: SChapter) = throw UnsupportedOperationException()
override fun fetchLatestUpdatesFlow(page: Int) = throw UnsupportedOperationException()
override fun fetchPopularMangaFlow(page: Int) = throw UnsupportedOperationException()
override suspend fun fetchMangaDetailsSuspended(manga: SManga): SManga {
return readMangaConfig(manga).load(db, sourceManager).take(1).map { loaded ->
SManga.create().apply {
this.copyFrom(loaded.manga)
url = manga.url
}
}.first()
override fun fetchMangaDetailsFlow(manga: SManga): Flow<SManga> {
return flow {
val mergedManga = db.getManga(manga.url, id).await() ?: throw Exception("merged manga not in db")
val mangaReferences = mergedManga.id?.let { withContext(Dispatchers.IO) { db.getMergedMangaReferences(it).await() } } ?: throw Exception("merged manga id is null")
if (mangaReferences.isEmpty()) throw IllegalArgumentException("Manga references are empty, info unavailable, merge is likely corrupted")
if (mangaReferences.size == 1 || {
val mangaReference = mangaReferences.firstOrNull()
mangaReference == null || (mangaReference.mangaSourceId == MERGED_SOURCE_ID)
}()
) throw IllegalArgumentException("Manga references contain only the merged reference, merge is likely corrupted")
emit(
SManga.create().apply {
val mangaInfoReference = mangaReferences.firstOrNull { it.isInfoManga } ?: mangaReferences.firstOrNull { it.mangaId != it.mergeId }
val dbManga = mangaInfoReference?.let { withContext(Dispatchers.IO) { db.getManga(it.mangaUrl, it.mangaSourceId).await() } }
this.copyFrom(dbManga ?: mergedManga)
url = manga.url
}
)
}
}
override suspend fun fetchChapterListSuspended(manga: SManga): List<SChapter> {
val loadedMangas = readMangaConfig(manga).load(db, sourceManager).buffer()
return loadedMangas.flatMapMerge { loadedManga ->
withContext(Dispatchers.IO) {
loadedManga.source.fetchChapterList(loadedManga.manga).asFlow().map { chapterList ->
chapterList.map { chapter ->
chapter.apply {
url = writeUrlConfig(
UrlConfig(
loadedManga.source.id,
url,
loadedManga.manga.url
)
)
fun getChaptersFromDB(manga: Manga, editScanlators: Boolean = false, dedupe: Boolean = true): Flow<List<Chapter>> {
// TODO more chapter dedupe
return db.getChaptersByMergedMangaId(manga.id!!).asRxObservable()
.asFlow()
.map { chapterList ->
val mangaReferences = withContext(Dispatchers.IO) { db.getMergedMangaReferences(manga.id!!).await() }
val sources = mangaReferences.map { sourceManager.getOrStub(it.mangaSourceId) to it.mangaId }
if (editScanlators) {
chapterList.onEach { chapter ->
val source = sources.firstOrNull { chapter.manga_id == it.second }?.first
if (source != null) {
chapter.scanlator = if (chapter.scanlator.isNullOrBlank()) source.name
else "$source: ${chapter.scanlator}"
}
}
}
if (dedupe) dedupeChapterList(mangaReferences, chapterList) else chapterList
}
}.buffer().toList().flatten()
}
override suspend fun mangaDetailsParseSuspended(response: Response) = throw UnsupportedOperationException()
override suspend fun chapterListParseSuspended(response: Response) = throw UnsupportedOperationException()
override suspend fun fetchPageListSuspended(chapter: SChapter): List<Page> {
val config = readUrlConfig(chapter.url)
val source = sourceManager.getOrStub(config.source)
return source.fetchPageList(
SChapter.create().apply {
copyFrom(chapter)
url = config.url
private fun dedupeChapterList(mangaReferences: List<MergedMangaReference>, chapterList: List<Chapter>): List<Chapter> {
return when (mangaReferences.firstOrNull { it.mangaSourceId == MERGED_SOURCE_ID }?.chapterSortMode) {
MergedMangaReference.CHAPTER_SORT_NO_DEDUPE, MergedMangaReference.CHAPTER_SORT_NONE -> chapterList
MergedMangaReference.CHAPTER_SORT_PRIORITY -> chapterList
MergedMangaReference.CHAPTER_SORT_MOST_CHAPTERS -> {
findSourceWithMostChapters(chapterList)?.let { mangaId ->
chapterList.filter { it.manga_id == mangaId }
} ?: chapterList
}
).map { pages ->
pages.map { page ->
page.copyWithUrl(writeUrlConfig(UrlConfig(config.source, page.url, config.mangaUrl)))
MergedMangaReference.CHAPTER_SORT_HIGHEST_CHAPTER_NUMBER -> {
findSourceWithHighestChapterNumber(chapterList)?.let { mangaId ->
chapterList.filter { it.manga_id == mangaId }
} ?: chapterList
}
}.awaitSingle()
}
override suspend fun fetchImageUrlSuspended(page: Page): String {
val config = readUrlConfig(page.url)
val source = sourceManager.getOrStub(config.source) as? HttpSource ?: throw UnsupportedOperationException("This source does not support this operation!")
return source.fetchImageUrl(page.copyWithUrl(config.url)).awaitSingle()
}
override suspend fun pageListParseSuspended(response: Response) = throw UnsupportedOperationException()
override suspend fun imageUrlParseSuspended(response: Response) = throw UnsupportedOperationException()
override fun fetchImage(page: Page): Observable<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)
else -> chapterList
}
}
data class MangaConfig(
@SerializedName("c")
val children: List<MangaSource>
) {
fun load(db: DatabaseHelper, sourceManager: SourceManager): Flow<LoadedMangaSource> {
return children.asFlow().map { mangaSource ->
mangaSource.load(db, sourceManager) ?: run {
XLog.w("> Missing source manga: $mangaSource")
throw IllegalStateException("Missing source manga: $mangaSource")
private fun findSourceWithMostChapters(chapterList: List<Chapter>): Long? {
return chapterList.groupBy { it.manga_id }.maxByOrNull { it.value.size }?.key
}
private fun findSourceWithHighestChapterNumber(chapterList: List<Chapter>): Long? {
return chapterList.maxByOrNull { it.chapter_number }?.manga_id
}
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()
}
}
}
fun writeAsUrl(gson: Gson): String {
return gson.toJson(this)
}
companion object {
fun readFromUrl(gson: Gson, url: String): MangaConfig {
return gson.fromJson(url)
}
}
}.buffer()
}
data class UrlConfig(
@SerializedName("s")
val source: Long,
@SerializedName("u")
val url: String,
@SerializedName("m")
val mangaUrl: String
)
suspend fun load(db: DatabaseHelper, sourceManager: SourceManager, reference: MergedMangaReference): LoadedMangaSource {
var manga = db.getManga(reference.mangaUrl, reference.mangaSourceId).await()
val source = sourceManager.getOrStub(manga?.source ?: reference.mangaSourceId)
if (manga == null) {
manga = Manga.create(reference.mangaSourceId).apply {
url = reference.mangaUrl
}
manga.copyFrom(source.fetchMangaDetails(manga).asFlow().single())
try {
manga.id = db.insertManga(manga).await().insertedId()
reference.mangaId = manga.id
db.insertNewMergedMangaId(reference).await()
} catch (e: Exception) {
XLog.st(e.stackTrace.contentToString(), 5)
}
}
return LoadedMangaSource(source, manga, reference)
}
fun Page.copyWithUrl(newUrl: String) = Page(
index,
newUrl,
imageUrl,
uri
)
data class LoadedMangaSource(val source: Source, val manga: Manga?, val reference: MergedMangaReference)
override val lang = "all"
override val supportsLatest = false

View File

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

View File

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

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.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.all.MergedSource
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.isLocal
import eu.kanade.tachiyomi.util.lang.combineLatest
@ -27,11 +28,15 @@ import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.removeCovers
import exh.EH_SOURCE_ID
import exh.EXH_SOURCE_ID
import exh.MERGED_SOURCE_ID
import exh.favorites.FavoritesSyncHelper
import exh.util.await
import exh.util.isLewd
import exh.util.nullIfBlank
import java.util.Collections
import java.util.Comparator
import kotlinx.coroutines.flow.singleOrNull
import kotlinx.coroutines.runBlocking
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
@ -241,7 +246,10 @@ class LibraryPresenter(
for ((_, itemList) in map) {
for (item in itemList) {
item.downloadCount = if (showDownloadBadges) {
downloadManager.getDownloadCount(item.manga)
// SY -->
if (item.manga.source == MERGED_SOURCE_ID) {
item.manga.id?.let { mergeMangaId -> db.getMergedMangas(mergeMangaId).executeAsBlocking().map { downloadManager.getDownloadCount(it) }.sum() } ?: 0
} else /* SY <-- */ downloadManager.getDownloadCount(item.manga)
} else {
// Unset download count if not enabled
-1
@ -455,8 +463,10 @@ class LibraryPresenter(
mangas.forEach { manga ->
launchIO {
/* SY --> */ val chapters = if (manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID) {
val chapter = db.getChapters(manga).executeAsBlocking().minByOrNull { it.source_order }
val chapter = db.getChapters(manga).await().minByOrNull { it.source_order }
if (chapter != null && !chapter.read) listOf(chapter) else emptyList()
} else if (manga.source == MERGED_SOURCE_ID) {
(sourceManager.getOrStub(MERGED_SOURCE_ID) as? MergedSource)?.getChaptersFromDB(manga)?.singleOrNull()?.filter { !it.read } ?: emptyList()
} else /* SY <-- */ db.getChapters(manga).executeAsBlocking()
.filter { !it.read }
@ -501,7 +511,7 @@ class LibraryPresenter(
fun markReadStatus(mangas: List<Manga>, read: Boolean) {
mangas.forEach { manga ->
launchIO {
val chapters = db.getChapters(manga).executeAsBlocking()
val chapters = if (manga.source == MERGED_SOURCE_ID) (sourceManager.get(MERGED_SOURCE_ID) as? MergedSource)?.getChaptersFromDB(manga)?.singleOrNull() ?: emptyList() else db.getChapters(manga).executeAsBlocking()
chapters.forEach {
it.read = read
if (!read) {
@ -519,7 +529,16 @@ class LibraryPresenter(
private fun deleteChapters(manga: Manga, chapters: List<Chapter>) {
sourceManager.get(manga.source)?.let { source ->
downloadManager.deleteChapters(chapters, manga, source)
// SY -->
if (source is MergedSource) {
val mergedMangas = db.getMergedMangas(manga.id!!).executeAsBlocking()
val sources = mergedMangas.distinctBy { it.source }.map { sourceManager.getOrStub(it.source) }
chapters.groupBy { it.manga_id }.forEach { map ->
val mergedManga = mergedMangas.firstOrNull { it.id == map.key } ?: return@forEach
val mergedMangaSource = sources.firstOrNull { it.id == mergedManga.source } ?: return@forEach
downloadManager.deleteChapters(map.value, mergedManga, mergedMangaSource)
}
} else /* SY <-- */ downloadManager.deleteChapters(chapters, manga, source)
}
}
@ -543,7 +562,14 @@ class LibraryPresenter(
mangaToDelete.forEach { manga ->
val source = sourceManager.get(manga.source) as? HttpSource
if (source != null) {
downloadManager.deleteManga(manga, source)
if (source is MergedSource) {
val mergedMangas = db.getMergedMangas(manga.id!!).await()
val sources = mergedMangas.distinctBy { it.source }.map { sourceManager.getOrStub(it.source) }
mergedMangas.forEach merge@{ mergedManga ->
val mergedSource = sources.firstOrNull { mergedManga.source == it.id } ?: return@merge
downloadManager.deleteManga(mergedManga, mergedSource)
}
} else downloadManager.deleteManga(manga, source)
}
}
}
@ -571,7 +597,7 @@ class LibraryPresenter(
// SY -->
/** Returns first unread chapter of a manga */
fun getFirstUnread(manga: Manga): Chapter? {
val chapters = db.getChapters(manga).executeAsBlocking()
val chapters = (if (manga.source == MERGED_SOURCE_ID) (sourceManager.get(MERGED_SOURCE_ID) as? MergedSource).let { runBlocking { it?.getChaptersFromDB(manga)?.singleOrNull() } ?: emptyList() } else db.getChapters(manga).executeAsBlocking())
return if (manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID) {
val chapter = chapters.sortedBy { it.source_order }.getOrNull(0)
if (chapter?.read == false) chapter else null

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

View File

@ -4,7 +4,6 @@ import android.content.Context
import android.net.Uri
import android.os.Bundle
import com.elvishew.xlog.XLog
import com.google.gson.Gson
import com.jakewharton.rxrelay.BehaviorRelay
import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.tachiyomi.data.cache.CoverCache
@ -20,6 +19,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.MetadataSource
import eu.kanade.tachiyomi.source.online.MetadataSource.Companion.isMetadataSource
import eu.kanade.tachiyomi.source.online.all.MergedSource
@ -37,10 +37,12 @@ import exh.MERGED_SOURCE_ID
import exh.debug.DebugToggles
import exh.eh.EHentaiUpdateHelper
import exh.isEhBasedSource
import exh.merged.sql.models.MergedMangaReference
import exh.metadata.metadata.base.FlatMetadata
import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.metadata.metadata.base.getFlatMetadataForManga
import exh.source.EnhancedHttpSource
import exh.util.asObservable
import exh.util.await
import exh.util.trimOrNull
import java.util.Date
@ -63,9 +65,7 @@ class MangaPresenter(
private val trackManager: TrackManager = Injekt.get(),
private val downloadManager: DownloadManager = Injekt.get(),
private val coverCache: CoverCache = Injekt.get(),
// SY -->
private val gson: Gson = Injekt.get()
// SY <--
private val sourceManager: SourceManager = Injekt.get()
) : BasePresenter<MangaController>() {
/**
@ -112,6 +112,10 @@ class MangaPresenter(
data class EXHRedirect(val manga: Manga, val update: Boolean)
var meta: RaisedSearchMetadata? = null
private var mergedManga = emptyList<Manga>()
var dedupe: Boolean = true
// EXH <--
override fun onCreate(savedState: Bundle?) {
@ -121,6 +125,10 @@ class MangaPresenter(
if (manga.initialized && source.isMetadataSource()) {
getMangaMetaObservable().subscribeLatestCache({ view, flatMetadata -> if (flatMetadata != null) view.onNextMetaInfo(flatMetadata) else XLog.d("Invalid metadata") })
}
if (source is MergedSource) {
launchIO { mergedManga = db.getMergedMangas(manga.id!!).await() }
}
// SY <--
// Manga info - start
@ -145,7 +153,7 @@ class MangaPresenter(
// Add the subscription that retrieves the chapters from the database, keeps subscribed to
// changes, and sends the list of chapters to the relay.
add(
db.getChapters(manga).asRxObservable()
(/* SY --> */if (source is MergedSource) source.getChaptersFromDB(manga, true, dedupe).asObservable() else /* SY <-- */ db.getChapters(manga).asRxObservable())
.map { chapters ->
// Convert every chapter to a model.
chapters.map { it.toModel() }
@ -334,64 +342,138 @@ class MangaPresenter(
}
suspend fun smartSearchMerge(manga: Manga, originalMangaId: Long): Manga {
val originalManga = db.getManga(originalMangaId).await()
?: throw IllegalArgumentException("Unknown manga ID: $originalMangaId")
val toInsert = if (originalManga.source == MERGED_SOURCE_ID) {
originalManga.apply {
val originalChildren = MergedSource.MangaConfig.readFromUrl(gson, url).children
if (originalChildren.any { it.source == manga.source && it.url == manga.url }) {
throw IllegalArgumentException("This manga is already merged with the current manga!")
}
url = MergedSource.MangaConfig(
originalChildren + MergedSource.MangaSource(
manga.source,
manga.url
)
).writeAsUrl(gson)
val originalManga = db.getManga(originalMangaId).await() ?: throw IllegalArgumentException("Unknown manga ID: $originalMangaId")
if (originalManga.source == MERGED_SOURCE_ID) {
val children = db.getMergedMangaReferences(originalMangaId).await()
if (children.any { it.mangaSourceId == manga.source && it.mangaUrl == manga.url }) {
throw IllegalArgumentException("This manga is already merged with the current manga!")
}
} else {
val newMangaConfig = MergedSource.MangaConfig(
listOf(
MergedSource.MangaSource(
originalManga.source,
originalManga.url
),
MergedSource.MangaSource(
manga.source,
manga.url
)
val mangaReferences = mutableListOf(
MergedMangaReference(
id = null,
isInfoManga = false,
getChapterUpdates = true,
chapterSortMode = 0,
chapterPriority = 0,
downloadChapters = true,
mergeId = originalManga.id!!,
mergeUrl = originalManga.url,
mangaId = manga.id!!,
mangaUrl = manga.url,
mangaSourceId = manga.source
)
)
Manga.create(newMangaConfig.writeAsUrl(gson), originalManga.title, MERGED_SOURCE_ID).apply {
if (children.isEmpty() || children.all { it.mangaSourceId != MERGED_SOURCE_ID }) {
mangaReferences += MergedMangaReference(
id = null,
isInfoManga = false,
getChapterUpdates = false,
chapterSortMode = 0,
chapterPriority = -1,
downloadChapters = false,
mergeId = originalManga.id!!,
mergeUrl = originalManga.url,
mangaId = originalManga.id!!,
mangaUrl = originalManga.url,
mangaSourceId = MERGED_SOURCE_ID
)
}
db.insertMergedMangas(mangaReferences).await()
return originalManga
} else {
val mergedManga = Manga.create(originalManga.url, originalManga.title, MERGED_SOURCE_ID).apply {
copyFrom(originalManga)
favorite = true
last_update = originalManga.last_update
viewer = originalManga.viewer
chapter_flags = originalManga.chapter_flags
sorting = Manga.SORTING_NUMBER
date_added = Date().time
}
var existingManga = db.getManga(mergedManga.url, mergedManga.source).await()
while (existingManga != null) {
if (existingManga.favorite) {
throw IllegalArgumentException("This merged manga is a duplicate!")
} else if (!existingManga.favorite) {
withContext(NonCancellable) {
db.deleteManga(existingManga!!).await()
db.deleteMangaForMergedManga(existingManga!!.id!!).await()
}
}
existingManga = db.getManga(mergedManga.url, mergedManga.source).await()
}
// Reload chapters immediately
mergedManga.initialized = false
val newId = db.insertManga(mergedManga).await().insertedId()
if (newId != null) mergedManga.id = newId
val originalMangaReference = MergedMangaReference(
id = null,
isInfoManga = true,
getChapterUpdates = true,
chapterSortMode = 0,
chapterPriority = 0,
downloadChapters = true,
mergeId = mergedManga.id!!,
mergeUrl = mergedManga.url,
mangaId = originalManga.id!!,
mangaUrl = originalManga.url,
mangaSourceId = originalManga.source
)
val newMangaReference = MergedMangaReference(
id = null,
isInfoManga = false,
getChapterUpdates = true,
chapterSortMode = 0,
chapterPriority = 0,
downloadChapters = true,
mergeId = mergedManga.id!!,
mergeUrl = mergedManga.url,
mangaId = manga.id!!,
mangaUrl = manga.url,
mangaSourceId = manga.source
)
val mergedMangaReference = MergedMangaReference(
id = null,
isInfoManga = false,
getChapterUpdates = false,
chapterSortMode = 0,
chapterPriority = -1,
downloadChapters = false,
mergeId = mergedManga.id!!,
mergeUrl = mergedManga.url,
mangaId = mergedManga.id!!,
mangaUrl = mergedManga.url,
mangaSourceId = MERGED_SOURCE_ID
)
db.insertMergedMangas(listOf(originalMangaReference, newMangaReference, mergedMangaReference)).await()
return mergedManga
}
// Note that if the manga are merged in a different order, this won't trigger, but I don't care lol
val existingManga = db.getManga(toInsert.url, toInsert.source).await()
if (existingManga != null) {
withContext(NonCancellable) {
if (toInsert.id != null) {
db.deleteManga(toInsert).await()
}
}
fun updateMergeSettings(mergeReference: MergedMangaReference?, mergedMangaReferences: List<MergedMangaReference>) {
launchIO {
mergeReference?.let {
db.updateMergeMangaSettings(it).await()
}
return existingManga
if (mergedMangaReferences.isNotEmpty()) db.updateMergedMangaSettings(mergedMangaReferences).await()
}
}
// Reload chapters immediately
toInsert.initialized = false
val newId = db.insertManga(toInsert).await().insertedId()
if (newId != null) toInsert.id = newId
return toInsert
fun toggleDedupe() {
// I cant find any way to call the chapter list subscription to get the chapters again
}
// SY <--
@ -424,7 +506,13 @@ class MangaPresenter(
* Deletes all the downloads for the manga.
*/
fun deleteDownloads() {
downloadManager.deleteManga(manga, source)
// SY -->
if (source is MergedSource) {
val mergedManga = mergedManga.map { it to sourceManager.getOrStub(it.source) }
mergedManga.forEach { (manga, source) ->
downloadManager.deleteManga(manga, source)
}
} else /* SY <-- */ downloadManager.deleteManga(manga, source)
}
/**
@ -515,10 +603,14 @@ class MangaPresenter(
// Chapters list - start
private fun observeDownloads() {
// SY -->
val isMergedSource = source is MergedSource
val mergedIds = if (isMergedSource) mergedManga.mapNotNull { it.id } else emptyList()
// SY <--
observeDownloadsSubscription?.let { remove(it) }
observeDownloadsSubscription = downloadManager.queue.getStatusObservable()
.observeOn(AndroidSchedulers.mainThread())
.filter { download -> download.manga.id == manga.id }
.filter { download -> /* SY --> */ if (isMergedSource) download.manga.id in mergedIds else /* SY <-- */ download.manga.id == manga.id }
.doOnNext { onDownloadStatusChange(it) }
.subscribeLatestCache(MangaController::onChapterStatusChange) { _, error ->
Timber.e(error)
@ -548,8 +640,11 @@ class MangaPresenter(
* @param chapters the list of chapter from the database.
*/
private fun setDownloadedChapters(chapters: List<ChapterItem>) {
// SY -->
val isMergedSource = source is MergedSource
// SY <--
chapters
.filter { downloadManager.isChapterDownloaded(it, manga) }
.filter { downloadManager.isChapterDownloaded(it, /* SY --> */ if (isMergedSource) mergedManga.firstOrNull { manga -> it.manga_id == manga.id } ?: manga else /* SY <-- */ manga) }
.forEach { it.status = Download.DOWNLOADED }
}
@ -560,21 +655,38 @@ class MangaPresenter(
hasRequested = true
if (!fetchChaptersSubscription.isNullOrUnsubscribed()) return
fetchChaptersSubscription = Observable.defer { source.fetchChapterList(manga) }
.subscribeOn(Schedulers.io())
.map { syncChaptersWithSource(db, it, manga, source) }
.doOnNext {
if (manualFetch) {
downloadNewChapters(it.first)
fetchChaptersSubscription = /* SY --> */ if (source !is MergedSource) {
// SY <--
Observable.defer { source.fetchChapterList(manga) }
.subscribeOn(Schedulers.io())
.map { syncChaptersWithSource(db, it, manga, source) }
.doOnNext {
if (manualFetch) {
downloadNewChapters(it.first)
}
}
}
.observeOn(AndroidSchedulers.mainThread())
.subscribeFirst(
{ view, _ ->
view.onFetchChaptersDone()
},
MangaController::onFetchChaptersError
)
.observeOn(AndroidSchedulers.mainThread())
.subscribeFirst(
{ view, _ ->
view.onFetchChaptersDone()
},
MangaController::onFetchChaptersError
)
// SY -->
} else {
Observable.defer { source.fetchChaptersForMergedManga(manga, manualFetch, true, dedupe).asObservable() }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnNext {
}
.subscribeFirst(
{ view, _ ->
view.onFetchChaptersDone()
},
MangaController::onFetchChaptersError
)
}
// SY <--
}
/**
@ -680,7 +792,13 @@ class MangaPresenter(
* @param chapters the list of chapters to download.
*/
fun downloadChapters(chapters: List<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 com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.data.glide.MangaThumbnail
@ -18,7 +19,6 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.all.MergedSource
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.view.setTooltip
@ -41,6 +41,10 @@ class MangaInfoHeaderAdapter(
RecyclerView.Adapter<MangaInfoHeaderAdapter.HeaderViewHolder>() {
private val trackManager: TrackManager by injectLazy()
// SY -->
private val db: DatabaseHelper by injectLazy()
private val sourceManager: SourceManager by injectLazy()
// SY <--
private var manga: Manga = controller.presenter.manga
private var source: Source = controller.presenter.source
@ -254,9 +258,9 @@ class MangaInfoHeaderAdapter(
val mangaSource = source?.toString()
with(binding.mangaSource) {
// SY -->
if (source != null && source.id == MERGED_SOURCE_ID) {
text = MergedSource.MangaConfig.readFromUrl(Injekt.get(), manga.url).children.map {
Injekt.get<SourceManager>().getOrStub(it.source).toString()
if (source?.id == MERGED_SOURCE_ID) {
text = db.getMergedMangaReferences(manga.id!!).executeAsBlocking().map {
sourceManager.getOrStub(it.mangaSourceId).toString()
}.distinct().joinToString()
} else /* SY <-- */ if (mangaSource != null) {
text = mangaSource

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

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.source.LocalSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.all.MergedSource
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import exh.debug.DebugFunctions.prefs
import exh.merged.sql.models.MergedMangaReference
import rx.Completable
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
@ -22,7 +25,12 @@ class ChapterLoader(
private val context: Context,
private val downloadManager: DownloadManager,
private val manga: Manga,
private val source: Source
private val source: Source,
// SY -->
private val sourceManager: SourceManager,
private val mergedReferences: List<MergedMangaReference>,
private val mergedManga: List<Manga>
// SY <--
) {
/**
@ -81,6 +89,27 @@ class ChapterLoader(
private fun getPageLoader(chapter: ReaderChapter): PageLoader {
val isDownloaded = downloadManager.isChapterDownloaded(chapter.chapter, manga, true)
return when {
// SY -->
source is MergedSource -> {
val mangaReference = mergedReferences.firstOrNull { it.mangaId == chapter.chapter.manga_id } ?: throw Exception("Merge reference null")
val source = sourceManager.get(mangaReference.mangaSourceId) ?: throw Exception("Source ${mangaReference.mangaSourceId} was null")
val manga = mergedManga.firstOrNull { it.id == chapter.chapter.manga_id } ?: throw Exception("Manga for merged chapter was null")
val isMergedMangaDownloaded = downloadManager.isChapterDownloaded(chapter.chapter, manga, true)
when {
isMergedMangaDownloaded -> DownloadPageLoader(chapter, manga, source, downloadManager)
source is HttpSource -> HttpPageLoader(chapter, source)
source is LocalSource -> source.getFormat(chapter.chapter).let { format ->
when (format) {
is LocalSource.Format.Directory -> DirectoryPageLoader(format.file)
is LocalSource.Format.Zip -> ZipPageLoader(format.file)
is LocalSource.Format.Rar -> RarPageLoader(format.file)
is LocalSource.Format.Epub -> EpubPageLoader(format.file)
}
}
else -> error(context.getString(R.string.loader_not_implemented_error))
}
}
// SY <--
isDownloaded -> DownloadPageLoader(chapter, manga, source, downloadManager)
source is HttpSource -> HttpPageLoader(chapter, source)
source is LocalSource -> source.getFormat(chapter.chapter).let { format ->

View File

@ -2,6 +2,9 @@ package exh
import android.content.Context
import com.elvishew.xlog.XLog
import com.github.salomonbrys.kotson.fromJson
import com.google.gson.Gson
import com.google.gson.annotations.SerializedName
import com.pushtorefresh.storio.sqlite.queries.Query
import com.pushtorefresh.storio.sqlite.queries.RawQuery
import eu.kanade.tachiyomi.BuildConfig
@ -12,13 +15,18 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.resolvers.MangaUrlPutResolver
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
import eu.kanade.tachiyomi.data.database.tables.MangaTable
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.updater.UpdaterJob
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.all.Hitomi
import eu.kanade.tachiyomi.source.online.all.NHentai
import exh.merged.sql.models.MergedMangaReference
import exh.source.BlacklistedSources
import java.io.File
import java.net.URI
@ -27,6 +35,8 @@ import uy.kohesive.injekt.injectLazy
object EXHMigrations {
private val db: DatabaseHelper by injectLazy()
private val sourceManager: SourceManager by injectLazy()
private val gson: Gson by injectLazy()
private val logger = XLog.tag("EXHMigrations")
@ -143,6 +153,106 @@ object EXHMigrations {
)
}
}
if (oldVersion < 7) {
db.inTransaction {
val mergedMangas = db.db.get()
.listOfObjects(Manga::class.java)
.withQuery(
Query.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_SOURCE} = $MERGED_SOURCE_ID")
.build()
)
.prepare()
.executeAsBlocking()
if (mergedMangas.isNotEmpty()) {
val mangaConfigs = mergedMangas.mapNotNull { mergedManga -> readMangaConfig(mergedManga, gson)?.let { mergedManga to it } }
if (mangaConfigs.isNotEmpty()) {
val mangaToUpdate = mutableListOf<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)
// do stuff here when releasing changed crap
@ -228,6 +338,57 @@ object EXHMigrations {
orig
}
}
private data class UrlConfig(
@SerializedName("s")
val source: Long,
@SerializedName("u")
val url: String,
@SerializedName("m")
val mangaUrl: String
)
private data class MangaConfig(
@SerializedName("c")
val children: List<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(

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 ->
subscribe(object : CompletableSubscriber {
override fun onSubscribe(s: Subscription) { cont.unsubscribeOnCancellation(s) }
override fun onCompleted() { cont.resume(Unit) }
override fun onError(e: Throwable) { cont.resumeWithException(e) }
override fun onSubscribe(s: Subscription) {
cont.unsubscribeOnCancellation(s)
}
override fun onCompleted() {
cont.resume(Unit)
}
override fun onError(e: Throwable) {
cont.resumeWithException(e)
}
})
}
suspend fun <T> Single<T>.await(): T = suspendCancellableCoroutine { cont ->
cont.unsubscribeOnCancellation(
subscribe(object : SingleSubscriber<T>() {
override fun onSuccess(t: T) { cont.resume(t) }
override fun onError(error: Throwable) { cont.resumeWithException(error) }
override fun onSuccess(t: T) {
cont.resume(t)
}
override fun onError(error: Throwable) {
cont.resumeWithException(error)
}
})
)
}
@ -129,7 +142,11 @@ suspend fun <T> Observable<T>.awaitFirstOrDefault(default: T): T = firstOrDefaul
suspend fun <T> Observable<T>.awaitFirstOrNull(): T? = firstOrDefault(null).awaitOne()
@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)
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 ->
cont.unsubscribeOnCancellation(
subscribe(object : Subscriber<T>() {
override fun onStart() { request(1) }
override fun onNext(t: T) { cont.resume(t) }
override fun onCompleted() { if (cont.isActive) cont.resumeWithException(IllegalStateException("Should have invoked onNext")) }
override fun onStart() {
request(1)
}
override fun onNext(t: T) {
cont.resume(t)
}
override fun onCompleted() {
if (cont.isActive) cont.resumeWithException(
IllegalStateException(
"Should have invoked onNext"
)
)
}
override fun onError(e: Throwable) {
/*
/*
* Rx1 observable throws NoSuchElementException if cancellation happened before
* element emission. To mitigate this we try to atomically resume continuation with exception:
* if resume failed, then we know that continuation successfully cancelled itself
@ -185,7 +215,7 @@ fun <T : Any> Observable<T>.asFlow(): Flow<T> = callbackFlow {
fun <T : Any> Flow<T>.asObservable(backpressureMode: Emitter.BackpressureMode = Emitter.BackpressureMode.NONE): Observable<T> {
return Observable.create(
{ emitter ->
/*
/*
* ATOMIC is used here to provide stable behaviour of subscribe+dispose pair even if
* 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_marginBottom="8dp"
android:text="Searching source..."
android:textAppearance="@style/TextAppearance.Medium.Title"
android:textColor="@android:color/white" />
android:textAppearance="@style/TextAppearance.Medium.Title" />
<ProgressBar
android:id="@+id/intercept_progress"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminateTint="@android:color/white" />
android:layout_gravity="center" />
</LinearLayout>
</LinearLayout>

View File

@ -57,4 +57,18 @@
android:title="@string/az_recommends"
android:visible="false"
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>

View File

@ -483,4 +483,23 @@
<item quantity="other">%2$s, %1$d pages</item>
</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>