Merged manga implementation, man this took forever to make and bugfix, its not even done
This commit is contained in:
parent
d21a652944
commit
a1d54880c3
@ -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()}\""
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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) {
|
||||
|
@ -19,6 +19,7 @@ object Backup {
|
||||
const val VERSION = "version"
|
||||
// SY -->
|
||||
const val SAVEDSEARCHES = "savedsearches"
|
||||
const val MERGEDMANGAREFERENCES = "mergedmangareferences"
|
||||
// SY <--
|
||||
|
||||
fun getDefaultFilename(): String {
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
@ -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) }
|
||||
}
|
||||
|
||||
|
@ -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) }
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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 <--
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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 ->
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
*/
|
||||
|
17
app/src/main/res/layout/edit_merged_settings_dialog.xml
Normal file
17
app/src/main/res/layout/edit_merged_settings_dialog.xml
Normal 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>
|
65
app/src/main/res/layout/edit_merged_settings_header.xml
Normal file
65
app/src/main/res/layout/edit_merged_settings_header.xml
Normal 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>
|
138
app/src/main/res/layout/edit_merged_settings_item.xml
Normal file
138
app/src/main/res/layout/edit_merged_settings_item.xml
Normal 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>
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
Loading…
x
Reference in New Issue
Block a user