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
|
minSdkVersion AndroidConfig.minSdk
|
||||||
targetSdkVersion AndroidConfig.targetSdk
|
targetSdkVersion AndroidConfig.targetSdk
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
versionCode 6
|
versionCode 7
|
||||||
versionName "1.2.0"
|
versionName "1.2.0"
|
||||||
|
|
||||||
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
||||||
|
@ -32,6 +32,7 @@ import eu.kanade.tachiyomi.data.backup.models.Backup.CURRENT_VERSION
|
|||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.EXTENSIONS
|
import eu.kanade.tachiyomi.data.backup.models.Backup.EXTENSIONS
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY
|
import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA
|
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.MERGEDMANGAREFERENCES
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.SAVEDSEARCHES
|
import eu.kanade.tachiyomi.data.backup.models.Backup.SAVEDSEARCHES
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
|
import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
|
||||||
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
||||||
@ -39,6 +40,7 @@ import eu.kanade.tachiyomi.data.backup.serializer.CategoryTypeAdapter
|
|||||||
import eu.kanade.tachiyomi.data.backup.serializer.ChapterTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.serializer.ChapterTypeAdapter
|
||||||
import eu.kanade.tachiyomi.data.backup.serializer.HistoryTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.serializer.HistoryTypeAdapter
|
||||||
import eu.kanade.tachiyomi.data.backup.serializer.MangaTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.serializer.MangaTypeAdapter
|
||||||
|
import eu.kanade.tachiyomi.data.backup.serializer.MergedMangaReferenceTypeAdapter
|
||||||
import eu.kanade.tachiyomi.data.backup.serializer.TrackTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.serializer.TrackTypeAdapter
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
||||||
@ -57,11 +59,17 @@ import eu.kanade.tachiyomi.source.LocalSource
|
|||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.online.all.EHentai
|
import eu.kanade.tachiyomi.source.online.all.EHentai
|
||||||
|
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||||
import exh.EXHSavedSearch
|
import exh.EXHSavedSearch
|
||||||
|
import exh.MERGED_SOURCE_ID
|
||||||
import exh.eh.EHentaiThrottleManager
|
import exh.eh.EHentaiThrottleManager
|
||||||
|
import exh.merged.sql.models.MergedMangaReference
|
||||||
|
import exh.util.asObservable
|
||||||
import java.lang.RuntimeException
|
import java.lang.RuntimeException
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
@ -106,6 +114,9 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
|
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
|
||||||
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
|
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
|
||||||
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
|
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
|
||||||
|
// SY -->
|
||||||
|
.registerTypeAdapter<MergedMangaReference>(MergedMangaReferenceTypeAdapter.build())
|
||||||
|
// SY <--
|
||||||
.create()
|
.create()
|
||||||
else -> throw Exception("Json version unknown")
|
else -> throw Exception("Json version unknown")
|
||||||
}
|
}
|
||||||
@ -129,15 +140,21 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
// Create extension ID/name mapping
|
// Create extension ID/name mapping
|
||||||
val extensionEntries = JsonArray()
|
val extensionEntries = JsonArray()
|
||||||
|
|
||||||
|
// Merged Manga References
|
||||||
|
val mergedMangaReferenceEntries = JsonArray()
|
||||||
|
|
||||||
// Add value's to root
|
// Add value's to root
|
||||||
root[Backup.VERSION] = CURRENT_VERSION
|
root[Backup.VERSION] = CURRENT_VERSION
|
||||||
root[Backup.MANGAS] = mangaEntries
|
root[Backup.MANGAS] = mangaEntries
|
||||||
root[CATEGORIES] = categoryEntries
|
root[CATEGORIES] = categoryEntries
|
||||||
root[EXTENSIONS] = extensionEntries
|
root[EXTENSIONS] = extensionEntries
|
||||||
|
// SY -->
|
||||||
|
root[MERGEDMANGAREFERENCES] = mergedMangaReferenceEntries
|
||||||
|
// SY <--
|
||||||
|
|
||||||
databaseHelper.inTransaction {
|
databaseHelper.inTransaction {
|
||||||
// Get manga from database
|
// Get manga from database
|
||||||
val mangas = getFavoriteManga()
|
val mangas = getFavoriteManga() /* SY --> */ + getMergedManga() /* SY <-- */
|
||||||
|
|
||||||
val extensions: MutableSet<String> = mutableSetOf()
|
val extensions: MutableSet<String> = mutableSetOf()
|
||||||
|
|
||||||
@ -163,6 +180,8 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
// SY -->
|
// SY -->
|
||||||
root[SAVEDSEARCHES] =
|
root[SAVEDSEARCHES] =
|
||||||
Injekt.get<PreferencesHelper>().eh_savedSearches().get().joinToString(separator = "***")
|
Injekt.get<PreferencesHelper>().eh_savedSearches().get().joinToString(separator = "***")
|
||||||
|
|
||||||
|
backupMergedMangaReferences(mergedMangaReferenceEntries)
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,6 +231,13 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
private fun backupMergedMangaReferences(root: JsonArray) {
|
||||||
|
val mergedMangaReferences = databaseHelper.getMergedMangaReferences().executeAsBlocking()
|
||||||
|
mergedMangaReferences.forEach { root.add(parser.toJsonTree(it)) }
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Backup the categories of library
|
* Backup the categories of library
|
||||||
*
|
*
|
||||||
@ -317,29 +343,40 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
*/
|
*/
|
||||||
fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>, throttleManager: EHentaiThrottleManager): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>, throttleManager: EHentaiThrottleManager): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
||||||
// SY -->
|
// SY -->
|
||||||
return (
|
if (source is MergedSource) {
|
||||||
if (source is EHentai) {
|
val syncedChapters = runBlocking { source.fetchChaptersAndSync(manga, false) }
|
||||||
source.fetchChapterList(manga, throttleManager::throttle)
|
return syncedChapters.onEach { pair ->
|
||||||
} else {
|
|
||||||
source.fetchChapterList(manga)
|
|
||||||
}
|
|
||||||
).map {
|
|
||||||
if (it.last().chapter_number == -99F) {
|
|
||||||
chapters.forEach { chapter ->
|
|
||||||
chapter.name = "Chapter ${chapter.chapter_number} restored by dummy source"
|
|
||||||
}
|
|
||||||
syncChaptersWithSource(databaseHelper, chapters, manga, source)
|
|
||||||
} else {
|
|
||||||
syncChaptersWithSource(databaseHelper, it, manga, source)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// SY <--
|
|
||||||
.doOnNext { pair ->
|
|
||||||
if (pair.first.isNotEmpty()) {
|
if (pair.first.isNotEmpty()) {
|
||||||
chapters.forEach { it.manga_id = manga.id }
|
chapters.forEach { it.manga_id = manga.id }
|
||||||
insertChapters(chapters)
|
insertChapters(chapters)
|
||||||
}
|
}
|
||||||
|
}.asObservable()
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
if (source is EHentai) {
|
||||||
|
source.fetchChapterList(manga, throttleManager::throttle)
|
||||||
|
} else {
|
||||||
|
source.fetchChapterList(manga)
|
||||||
|
}
|
||||||
|
).map {
|
||||||
|
if (it.last().chapter_number == -99F) {
|
||||||
|
chapters.forEach { chapter ->
|
||||||
|
chapter.name =
|
||||||
|
"Chapter ${chapter.chapter_number} restored by dummy source"
|
||||||
|
}
|
||||||
|
syncChaptersWithSource(databaseHelper, chapters, manga, source)
|
||||||
|
} else {
|
||||||
|
syncChaptersWithSource(databaseHelper, it, manga, source)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// SY <--
|
||||||
|
.doOnNext { pair ->
|
||||||
|
if (pair.first.isNotEmpty()) {
|
||||||
|
chapters.forEach { it.manga_id = manga.id }
|
||||||
|
insertChapters(chapters)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -584,6 +621,49 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
}
|
}
|
||||||
preferences.eh_savedSearches().set((otherSerialized + newSerialized).toSet())
|
preferences.eh_savedSearches().set((otherSerialized + newSerialized).toSet())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore the categories from Json
|
||||||
|
*
|
||||||
|
* @param jsonMergedMangaReferences array containing md manga references
|
||||||
|
*/
|
||||||
|
internal fun restoreMergedMangaReferences(jsonMergedMangaReferences: JsonArray) {
|
||||||
|
// Get merged manga references from file and from db
|
||||||
|
val dbMergedMangaReferences = databaseHelper.getMergedMangaReferences().executeAsBlocking()
|
||||||
|
val backupMergedMangaReferences = parser.fromJson<List<MergedMangaReference>>(jsonMergedMangaReferences)
|
||||||
|
var lastMergeManga: Manga? = null
|
||||||
|
|
||||||
|
// Iterate over them
|
||||||
|
backupMergedMangaReferences.forEach { mergedMangaReference ->
|
||||||
|
// Used to know if the merged manga reference is already in the db
|
||||||
|
var found = false
|
||||||
|
for (dbMergedMangaReference in dbMergedMangaReferences) {
|
||||||
|
// If the mergedMangaReference is already in the db, assign the id to the file's mergedMangaReference
|
||||||
|
// and do nothing
|
||||||
|
if (mergedMangaReference.mergeUrl == dbMergedMangaReference.mergeUrl && mergedMangaReference.mangaUrl == dbMergedMangaReference.mangaUrl) {
|
||||||
|
mergedMangaReference.id = dbMergedMangaReference.id
|
||||||
|
mergedMangaReference.mergeId = dbMergedMangaReference.mergeId
|
||||||
|
mergedMangaReference.mangaId = dbMergedMangaReference.mangaId
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If the mergedMangaReference isn't in the db, remove the id and insert a new mergedMangaReference
|
||||||
|
// Store the inserted id in the mergedMangaReference
|
||||||
|
if (!found) {
|
||||||
|
// Let the db assign the id
|
||||||
|
val mergedManga = (if (mergedMangaReference.mergeUrl != lastMergeManga?.url) databaseHelper.getManga(mergedMangaReference.mergeUrl, MERGED_SOURCE_ID).executeAsBlocking() else lastMergeManga) ?: return@forEach
|
||||||
|
val manga = databaseHelper.getManga(mergedMangaReference.mangaUrl, mergedMangaReference.mangaSourceId).executeAsBlocking() ?: return@forEach
|
||||||
|
lastMergeManga = mergedManga
|
||||||
|
|
||||||
|
mergedMangaReference.mergeId = mergedManga.id
|
||||||
|
mergedMangaReference.mangaId = manga.id
|
||||||
|
mergedMangaReference.id = null
|
||||||
|
val result = databaseHelper.insertMergedManga(mergedMangaReference).executeAsBlocking()
|
||||||
|
mergedMangaReference.id = result.insertedId()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -602,6 +682,9 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
|||||||
internal fun getFavoriteManga(): List<Manga> =
|
internal fun getFavoriteManga(): List<Manga> =
|
||||||
databaseHelper.getFavoriteMangas().executeAsBlocking()
|
databaseHelper.getFavoriteMangas().executeAsBlocking()
|
||||||
|
|
||||||
|
internal fun getMergedManga(): List<Manga> =
|
||||||
|
databaseHelper.getMergedMangas().executeAsBlocking()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inserts manga and returns id
|
* Inserts manga and returns id
|
||||||
*
|
*
|
||||||
|
@ -238,7 +238,7 @@ class BackupRestoreService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
totalAmount = mangasJson.size()
|
totalAmount = mangasJson.size()
|
||||||
restoreAmount = validManga.count() + 1 // +1 for categories
|
restoreAmount = validManga.count() + 3 // +1 for categories, +1 for saved searches, +1 for merged manga references
|
||||||
skippedAmount = mangasJson.size() - validManga.count()
|
skippedAmount = mangasJson.size() - validManga.count()
|
||||||
// SY <--
|
// SY <--
|
||||||
restoreProgress = 0
|
restoreProgress = 0
|
||||||
@ -288,6 +288,15 @@ class BackupRestoreService : Service() {
|
|||||||
restoreProgress += 1
|
restoreProgress += 1
|
||||||
showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.saved_searches))
|
showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.saved_searches))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun restoreMergedMangaReferences(mergedMangaReferencesJson: JsonElement) {
|
||||||
|
db.inTransaction {
|
||||||
|
backupManager.restoreMergedMangaReferences(mergedMangaReferencesJson.asJsonArray)
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreProgress += 1
|
||||||
|
showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.categories))
|
||||||
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
private fun restoreManga(mangaJson: JsonObject) {
|
private fun restoreManga(mangaJson: JsonObject) {
|
||||||
|
@ -19,6 +19,7 @@ object Backup {
|
|||||||
const val VERSION = "version"
|
const val VERSION = "version"
|
||||||
// SY -->
|
// SY -->
|
||||||
const val SAVEDSEARCHES = "savedsearches"
|
const val SAVEDSEARCHES = "savedsearches"
|
||||||
|
const val MERGEDMANGAREFERENCES = "mergedmangareferences"
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
fun getDefaultFilename(): String {
|
fun getDefaultFilename(): String {
|
||||||
|
@ -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.HistoryTable as History
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable as MangaCategory
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable as MangaCategory
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga
|
||||||
|
import exh.MERGED_SOURCE_ID
|
||||||
import exh.merged.sql.tables.MergedTable as Merged
|
import exh.merged.sql.tables.MergedTable as Merged
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
@ -21,6 +22,9 @@ fun getMergedMangaQuery() =
|
|||||||
ON ${Manga.TABLE}.${Manga.COL_ID} = M.${Merged.COL_MANGA_ID}
|
ON ${Manga.TABLE}.${Manga.COL_ID} = M.${Merged.COL_MANGA_ID}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query to get all the manga that are merged into other manga
|
||||||
|
*/
|
||||||
fun getAllMergedMangaQuery() =
|
fun getAllMergedMangaQuery() =
|
||||||
"""
|
"""
|
||||||
SELECT ${Manga.TABLE}.*
|
SELECT ${Manga.TABLE}.*
|
||||||
@ -56,7 +60,6 @@ fun getMergedChaptersQuery() =
|
|||||||
JOIN ${Chapter.TABLE}
|
JOIN ${Chapter.TABLE}
|
||||||
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = M.${Merged.COL_MANGA_ID}
|
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = M.${Merged.COL_MANGA_ID}
|
||||||
"""
|
"""
|
||||||
// SY <--
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query to get the manga from the library, with their categories and unread count.
|
* Query to get the manga from the library, with their categories and unread count.
|
||||||
@ -66,29 +69,54 @@ val libraryQuery =
|
|||||||
SELECT M.*, COALESCE(MC.${MangaCategory.COL_CATEGORY_ID}, 0) AS ${Manga.COL_CATEGORY}
|
SELECT M.*, COALESCE(MC.${MangaCategory.COL_CATEGORY_ID}, 0) AS ${Manga.COL_CATEGORY}
|
||||||
FROM (
|
FROM (
|
||||||
SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD}, COALESCE(R.read, 0) AS ${Manga.COL_READ}
|
SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD}, COALESCE(R.read, 0) AS ${Manga.COL_READ}
|
||||||
FROM ${Manga.TABLE}
|
FROM ${Manga.TABLE}
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS unread
|
SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}, COUNT(*) AS unread
|
||||||
FROM ${Chapter.TABLE}
|
FROM ${Chapter.TABLE}
|
||||||
WHERE ${Chapter.COL_READ} = 0
|
WHERE ${Chapter.COL_READ} = 0
|
||||||
GROUP BY ${Chapter.COL_MANGA_ID}
|
GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||||
) AS C
|
) AS C
|
||||||
ON ${Manga.COL_ID} = C.${Chapter.COL_MANGA_ID}
|
ON ${Manga.TABLE}.${Manga.COL_ID} = C.${Chapter.COL_MANGA_ID}
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS read
|
SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS read
|
||||||
FROM ${Chapter.TABLE}
|
FROM ${Chapter.TABLE}
|
||||||
WHERE ${Chapter.COL_READ} = 1
|
WHERE ${Chapter.COL_READ} = 1
|
||||||
GROUP BY ${Chapter.COL_MANGA_ID}
|
GROUP BY ${Chapter.COL_MANGA_ID}
|
||||||
) AS R
|
) AS R
|
||||||
ON ${Manga.COL_ID} = R.${Chapter.COL_MANGA_ID}
|
ON ${Manga.TABLE}.${Manga.COL_ID} = R.${Chapter.COL_MANGA_ID}
|
||||||
WHERE ${Manga.COL_FAVORITE} = 1
|
WHERE ${Manga.COL_FAVORITE} = 1 AND ${Manga.COL_SOURCE} <> $MERGED_SOURCE_ID
|
||||||
GROUP BY ${Manga.COL_ID}
|
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
|
||||||
|
UNION
|
||||||
|
SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD}, COALESCE(R.read, 0) AS ${Manga.COL_READ}
|
||||||
|
FROM ${Manga.TABLE}
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT ${Merged.TABLE}.${Merged.COL_MERGE_ID}, COUNT(*) as unread
|
||||||
|
FROM ${Merged.TABLE}
|
||||||
|
JOIN ${Chapter.TABLE}
|
||||||
|
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ${Merged.TABLE}.${Merged.COL_MANGA_ID}
|
||||||
|
WHERE ${Chapter.TABLE}.${Chapter.COL_READ} = 0
|
||||||
|
GROUP BY ${Merged.TABLE}.${Merged.COL_MERGE_ID}
|
||||||
|
) AS C
|
||||||
|
ON ${Manga.TABLE}.${Manga.COL_ID} = C.${Merged.COL_MERGE_ID}
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT ${Merged.TABLE}.${Merged.COL_MERGE_ID}, COUNT(*) as read
|
||||||
|
FROM ${Merged.TABLE}
|
||||||
|
JOIN ${Chapter.TABLE}
|
||||||
|
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ${Merged.TABLE}.${Merged.COL_MANGA_ID}
|
||||||
|
WHERE ${Chapter.TABLE}.${Chapter.COL_READ} = 1
|
||||||
|
GROUP BY ${Merged.TABLE}.${Merged.COL_MERGE_ID}
|
||||||
|
) AS R
|
||||||
|
ON ${Manga.TABLE}.${Manga.COL_ID} = R.${Merged.COL_MERGE_ID}
|
||||||
|
WHERE ${Manga.COL_FAVORITE} = 1 AND ${Manga.COL_SOURCE} = $MERGED_SOURCE_ID
|
||||||
|
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
|
||||||
ORDER BY ${Manga.COL_TITLE}
|
ORDER BY ${Manga.COL_TITLE}
|
||||||
) AS M
|
) AS M
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT * FROM ${MangaCategory.TABLE}) AS MC
|
SELECT * FROM ${MangaCategory.TABLE}
|
||||||
ON MC.${MangaCategory.COL_MANGA_ID} = M.${Manga.COL_ID}
|
) AS MC
|
||||||
|
ON MC.${MangaCategory.COL_MANGA_ID} = M.${Manga.COL_ID};
|
||||||
"""
|
"""
|
||||||
|
// SY <--
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query to get the recent chapters of manga from the library up to a date.
|
* Query to get the recent chapters of manga from the library up to a date.
|
||||||
|
@ -22,6 +22,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||||
import eu.kanade.tachiyomi.ui.library.LibraryGroup
|
import eu.kanade.tachiyomi.ui.library.LibraryGroup
|
||||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||||
import eu.kanade.tachiyomi.util.prepUpdateCover
|
import eu.kanade.tachiyomi.util.prepUpdateCover
|
||||||
@ -30,9 +31,12 @@ import eu.kanade.tachiyomi.util.storage.getUriCompat
|
|||||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
import exh.LIBRARY_UPDATE_EXCLUDED_SOURCES
|
import exh.LIBRARY_UPDATE_EXCLUDED_SOURCES
|
||||||
|
import exh.MERGED_SOURCE_ID
|
||||||
|
import exh.util.asObservable
|
||||||
import exh.util.nullIfBlank
|
import exh.util.nullIfBlank
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
@ -385,7 +389,12 @@ class LibraryUpdateService(
|
|||||||
private fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
private fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
||||||
// We don't want to start downloading while the library is updating, because websites
|
// We don't want to start downloading while the library is updating, because websites
|
||||||
// may don't like it and they could ban the user.
|
// may don't like it and they could ban the user.
|
||||||
downloadManager.downloadChapters(manga, chapters, false)
|
// SY -->
|
||||||
|
val chapterFilter = if (manga.source == MERGED_SOURCE_ID) {
|
||||||
|
db.getMergedMangaReferences(manga.id!!).executeAsBlocking().filterNot { it.downloadChapters }.mapNotNull { it.mangaId }
|
||||||
|
} else emptyList()
|
||||||
|
// SY <--
|
||||||
|
downloadManager.downloadChapters(manga, /* SY --> */ chapters.filter { it.manga_id !in chapterFilter } /* SY <-- */, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -417,7 +426,8 @@ class LibraryUpdateService(
|
|||||||
.subscribe()
|
.subscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
return source.fetchChapterList(manga)
|
return /* SY --> */ if (source is MergedSource) runBlocking { source.fetchChaptersAndSync(manga, false).asObservable() }
|
||||||
|
else /* SY <-- */ source.fetchChapterList(manga)
|
||||||
.map { syncChaptersWithSource(db, it, manga, source) }
|
.map { syncChaptersWithSource(db, it, manga, source) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,7 +8,10 @@ import eu.kanade.tachiyomi.source.model.MangasPage
|
|||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import exh.util.asObservable
|
||||||
import kotlin.jvm.Throws
|
import kotlin.jvm.Throws
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
@ -25,13 +28,17 @@ abstract class SuspendHttpSource : HttpSource() {
|
|||||||
*
|
*
|
||||||
* @param page the page number to retrieve.
|
* @param page the page number to retrieve.
|
||||||
*/
|
*/
|
||||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
final override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||||
return Observable.just(runBlocking { fetchPopularMangaSuspended(page) })
|
return fetchPopularMangaFlow(page).asObservable()
|
||||||
}
|
}
|
||||||
|
|
||||||
open suspend fun fetchPopularMangaSuspended(page: Int): MangasPage {
|
open fun fetchPopularMangaFlow(page: Int): Flow<MangasPage> {
|
||||||
val response = client.newCall(popularMangaRequestSuspended(page)).await()
|
return flow {
|
||||||
return popularMangaParseSuspended(response)
|
val response = client.newCall(popularMangaRequestSuspended(page)).await()
|
||||||
|
emit(
|
||||||
|
popularMangaParseSuspended(response)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -39,7 +46,7 @@ abstract class SuspendHttpSource : HttpSource() {
|
|||||||
*
|
*
|
||||||
* @param page the page number to retrieve.
|
* @param page the page number to retrieve.
|
||||||
*/
|
*/
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
final override fun popularMangaRequest(page: Int): Request {
|
||||||
return runBlocking { popularMangaRequestSuspended(page) }
|
return runBlocking { popularMangaRequestSuspended(page) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,7 +57,7 @@ abstract class SuspendHttpSource : HttpSource() {
|
|||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
override fun popularMangaParse(response: Response): MangasPage {
|
final override fun popularMangaParse(response: Response): MangasPage {
|
||||||
return runBlocking { popularMangaParseSuspended(response) }
|
return runBlocking { popularMangaParseSuspended(response) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,13 +71,17 @@ abstract class SuspendHttpSource : HttpSource() {
|
|||||||
* @param query the search query.
|
* @param query the search query.
|
||||||
* @param filters the list of filters to apply.
|
* @param filters the list of filters to apply.
|
||||||
*/
|
*/
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
final override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
return Observable.just(runBlocking { fetchSearchMangaSuspended(page, query, filters) })
|
return fetchSearchMangaSuspended(page, query, filters).asObservable()
|
||||||
}
|
}
|
||||||
|
|
||||||
open suspend fun fetchSearchMangaSuspended(page: Int, query: String, filters: FilterList): MangasPage {
|
open fun fetchSearchMangaSuspended(page: Int, query: String, filters: FilterList): Flow<MangasPage> {
|
||||||
val response = client.newCall(searchMangaRequestSuspended(page, query, filters)).await()
|
return flow {
|
||||||
return searchMangaParseSuspended(response)
|
val response = client.newCall(searchMangaRequestSuspended(page, query, filters)).await()
|
||||||
|
emit(
|
||||||
|
searchMangaParseSuspended(response)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -80,7 +91,7 @@ abstract class SuspendHttpSource : HttpSource() {
|
|||||||
* @param query the search query.
|
* @param query the search query.
|
||||||
* @param filters the list of filters to apply.
|
* @param filters the list of filters to apply.
|
||||||
*/
|
*/
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
final override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
return runBlocking { searchMangaRequestSuspended(page, query, filters) }
|
return runBlocking { searchMangaRequestSuspended(page, query, filters) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,7 +102,7 @@ abstract class SuspendHttpSource : HttpSource() {
|
|||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
final override fun searchMangaParse(response: Response): MangasPage {
|
||||||
return runBlocking { searchMangaParseSuspended(response) }
|
return runBlocking { searchMangaParseSuspended(response) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,13 +113,17 @@ abstract class SuspendHttpSource : HttpSource() {
|
|||||||
*
|
*
|
||||||
* @param page the page number to retrieve.
|
* @param page the page number to retrieve.
|
||||||
*/
|
*/
|
||||||
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
|
final override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
|
||||||
return Observable.just(runBlocking { fetchLatestUpdatesSuspended(page) })
|
return fetchLatestUpdatesFlow(page).asObservable()
|
||||||
}
|
}
|
||||||
|
|
||||||
open suspend fun fetchLatestUpdatesSuspended(page: Int): MangasPage {
|
open fun fetchLatestUpdatesFlow(page: Int): Flow<MangasPage> {
|
||||||
val response = client.newCall(latestUpdatesRequestSuspended(page)).await()
|
return flow {
|
||||||
return latestUpdatesParseSuspended(response)
|
val response = client.newCall(latestUpdatesRequestSuspended(page)).await()
|
||||||
|
emit(
|
||||||
|
latestUpdatesParseSuspended(response)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -116,7 +131,7 @@ abstract class SuspendHttpSource : HttpSource() {
|
|||||||
*
|
*
|
||||||
* @param page the page number to retrieve.
|
* @param page the page number to retrieve.
|
||||||
*/
|
*/
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
final override fun latestUpdatesRequest(page: Int): Request {
|
||||||
return runBlocking { latestUpdatesRequestSuspended(page) }
|
return runBlocking { latestUpdatesRequestSuspended(page) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,7 +142,7 @@ abstract class SuspendHttpSource : HttpSource() {
|
|||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
final override fun latestUpdatesParse(response: Response): MangasPage {
|
||||||
return runBlocking { latestUpdatesParseSuspended(response) }
|
return runBlocking { latestUpdatesParseSuspended(response) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,13 +154,17 @@ abstract class SuspendHttpSource : HttpSource() {
|
|||||||
*
|
*
|
||||||
* @param manga the manga to be updated.
|
* @param manga the manga to be updated.
|
||||||
*/
|
*/
|
||||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
final override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||||
return Observable.just(runBlocking { fetchMangaDetailsSuspended(manga) })
|
return fetchMangaDetailsFlow(manga).asObservable()
|
||||||
}
|
}
|
||||||
|
|
||||||
open suspend fun fetchMangaDetailsSuspended(manga: SManga): SManga {
|
open fun fetchMangaDetailsFlow(manga: SManga): Flow<SManga> {
|
||||||
val response = client.newCall(mangaDetailsRequestSuspended(manga)).await()
|
return flow {
|
||||||
return mangaDetailsParseSuspended(response).apply { initialized = true }
|
val response = client.newCall(mangaDetailsRequestSuspended(manga)).await()
|
||||||
|
emit(
|
||||||
|
mangaDetailsParseSuspended(response).apply { initialized = true }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -154,7 +173,7 @@ abstract class SuspendHttpSource : HttpSource() {
|
|||||||
*
|
*
|
||||||
* @param manga the manga to be updated.
|
* @param manga the manga to be updated.
|
||||||
*/
|
*/
|
||||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
final override fun mangaDetailsRequest(manga: SManga): Request {
|
||||||
return runBlocking { mangaDetailsRequestSuspended(manga) }
|
return runBlocking { mangaDetailsRequestSuspended(manga) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,7 +186,7 @@ abstract class SuspendHttpSource : HttpSource() {
|
|||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
final override fun mangaDetailsParse(response: Response): SManga {
|
||||||
return runBlocking { mangaDetailsParseSuspended(response) }
|
return runBlocking { mangaDetailsParseSuspended(response) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,21 +198,25 @@ abstract class SuspendHttpSource : HttpSource() {
|
|||||||
*
|
*
|
||||||
* @param manga the manga to look for chapters.
|
* @param manga the manga to look for chapters.
|
||||||
*/
|
*/
|
||||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
final override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||||
return try {
|
return try {
|
||||||
Observable.just(runBlocking { fetchChapterListSuspended(manga) })
|
fetchChapterListFlow(manga).asObservable()
|
||||||
} catch (e: LicencedException) {
|
} catch (e: LicencedException) {
|
||||||
Observable.error(Exception("Licensed - No chapters to show"))
|
Observable.error(Exception("Licensed - No chapters to show"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(LicencedException::class)
|
@Throws(LicencedException::class)
|
||||||
open suspend fun fetchChapterListSuspended(manga: SManga): List<SChapter> {
|
open fun fetchChapterListFlow(manga: SManga): Flow<List<SChapter>> {
|
||||||
return if (manga.status != SManga.LICENSED) {
|
return flow {
|
||||||
val response = client.newCall(chapterListRequestSuspended(manga)).await()
|
if (manga.status != SManga.LICENSED) {
|
||||||
chapterListParseSuspended(response)
|
val response = client.newCall(chapterListRequestSuspended(manga)).await()
|
||||||
} else {
|
emit(
|
||||||
throw LicencedException("Licensed - No chapters to show")
|
chapterListParseSuspended(response)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
throw LicencedException("Licensed - No chapters to show")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -203,7 +226,7 @@ abstract class SuspendHttpSource : HttpSource() {
|
|||||||
*
|
*
|
||||||
* @param manga the manga to look for chapters.
|
* @param manga the manga to look for chapters.
|
||||||
*/
|
*/
|
||||||
override fun chapterListRequest(manga: SManga): Request {
|
final override fun chapterListRequest(manga: SManga): Request {
|
||||||
return runBlocking { chapterListRequestSuspended(manga) }
|
return runBlocking { chapterListRequestSuspended(manga) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,7 +239,7 @@ abstract class SuspendHttpSource : HttpSource() {
|
|||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
final override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
return runBlocking { chapterListParseSuspended(response) }
|
return runBlocking { chapterListParseSuspended(response) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -227,13 +250,17 @@ abstract class SuspendHttpSource : HttpSource() {
|
|||||||
*
|
*
|
||||||
* @param chapter the chapter whose page list has to be fetched.
|
* @param chapter the chapter whose page list has to be fetched.
|
||||||
*/
|
*/
|
||||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
final override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||||
return Observable.just(runBlocking { fetchPageListSuspended(chapter) })
|
return fetchPageListFlow(chapter).asObservable()
|
||||||
}
|
}
|
||||||
|
|
||||||
open suspend fun fetchPageListSuspended(chapter: SChapter): List<Page> {
|
open fun fetchPageListFlow(chapter: SChapter): Flow<List<Page>> {
|
||||||
val response = client.newCall(pageListRequestSuspended(chapter)).await()
|
return flow {
|
||||||
return pageListParseSuspended(response)
|
val response = client.newCall(pageListRequestSuspended(chapter)).await()
|
||||||
|
emit(
|
||||||
|
pageListParseSuspended(response)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -242,7 +269,7 @@ abstract class SuspendHttpSource : HttpSource() {
|
|||||||
*
|
*
|
||||||
* @param chapter the chapter whose page list has to be fetched.
|
* @param chapter the chapter whose page list has to be fetched.
|
||||||
*/
|
*/
|
||||||
override fun pageListRequest(chapter: SChapter): Request {
|
final override fun pageListRequest(chapter: SChapter): Request {
|
||||||
return runBlocking { pageListRequestSuspended(chapter) }
|
return runBlocking { pageListRequestSuspended(chapter) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -255,7 +282,7 @@ abstract class SuspendHttpSource : HttpSource() {
|
|||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
final override fun pageListParse(response: Response): List<Page> {
|
||||||
return runBlocking { pageListParseSuspended(response) }
|
return runBlocking { pageListParseSuspended(response) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -267,13 +294,17 @@ abstract class SuspendHttpSource : HttpSource() {
|
|||||||
*
|
*
|
||||||
* @param page the page whose source image has to be fetched.
|
* @param page the page whose source image has to be fetched.
|
||||||
*/
|
*/
|
||||||
override fun fetchImageUrl(page: Page): Observable<String> {
|
final override fun fetchImageUrl(page: Page): Observable<String> {
|
||||||
return Observable.just(runBlocking { fetchImageUrlSuspended(page) })
|
return fetchImageUrlFlow(page).asObservable()
|
||||||
}
|
}
|
||||||
|
|
||||||
open suspend fun fetchImageUrlSuspended(page: Page): String {
|
open fun fetchImageUrlFlow(page: Page): Flow<String> {
|
||||||
val response = client.newCall(imageUrlRequestSuspended(page)).await()
|
return flow {
|
||||||
return imageUrlParseSuspended(response)
|
val response = client.newCall(imageUrlRequestSuspended(page)).await()
|
||||||
|
emit(
|
||||||
|
imageUrlParseSuspended(response)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -282,7 +313,7 @@ abstract class SuspendHttpSource : HttpSource() {
|
|||||||
*
|
*
|
||||||
* @param page the chapter whose page list has to be fetched
|
* @param page the chapter whose page list has to be fetched
|
||||||
*/
|
*/
|
||||||
override fun imageUrlRequest(page: Page): Request {
|
final override fun imageUrlRequest(page: Page): Request {
|
||||||
return runBlocking { imageUrlRequestSuspended(page) }
|
return runBlocking { imageUrlRequestSuspended(page) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -295,7 +326,7 @@ abstract class SuspendHttpSource : HttpSource() {
|
|||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
override fun imageUrlParse(response: Response): String {
|
final override fun imageUrlParse(response: Response): String {
|
||||||
return runBlocking { imageUrlParseSuspended(response) }
|
return runBlocking { imageUrlParseSuspended(response) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -306,12 +337,16 @@ abstract class SuspendHttpSource : HttpSource() {
|
|||||||
*
|
*
|
||||||
* @param page the page whose source image has to be downloaded.
|
* @param page the page whose source image has to be downloaded.
|
||||||
*/
|
*/
|
||||||
override fun fetchImage(page: Page): Observable<Response> {
|
final override fun fetchImage(page: Page): Observable<Response> {
|
||||||
return Observable.just(runBlocking { fetchImageSuspended(page) })
|
return fetchImageFlow(page).asObservable()
|
||||||
}
|
}
|
||||||
|
|
||||||
open suspend fun fetchImageSuspended(page: Page): Response {
|
open fun fetchImageFlow(page: Page): Flow<Response> {
|
||||||
return client.newCallWithProgress(imageRequestSuspended(page), page).await()
|
return flow {
|
||||||
|
emit(
|
||||||
|
client.newCallWithProgress(imageRequestSuspended(page), page).await()
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -320,7 +355,7 @@ abstract class SuspendHttpSource : HttpSource() {
|
|||||||
*
|
*
|
||||||
* @param page the chapter whose page list has to be fetched
|
* @param page the chapter whose page list has to be fetched
|
||||||
*/
|
*/
|
||||||
override fun imageRequest(page: Page): Request {
|
final override fun imageRequest(page: Page): Request {
|
||||||
return runBlocking { imageRequestSuspended(page) }
|
return runBlocking { imageRequestSuspended(page) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -335,7 +370,7 @@ abstract class SuspendHttpSource : HttpSource() {
|
|||||||
* @param chapter the chapter to be added.
|
* @param chapter the chapter to be added.
|
||||||
* @param manga the manga of the chapter.
|
* @param manga the manga of the chapter.
|
||||||
*/
|
*/
|
||||||
override fun prepareNewChapter(chapter: SChapter, manga: SManga) {
|
final override fun prepareNewChapter(chapter: SChapter, manga: SManga) {
|
||||||
runBlocking { prepareNewChapterSuspended(chapter, manga) }
|
runBlocking { prepareNewChapterSuspended(chapter, manga) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,43 +1,44 @@
|
|||||||
package eu.kanade.tachiyomi.source.online.all
|
package eu.kanade.tachiyomi.source.online.all
|
||||||
|
|
||||||
import com.elvishew.xlog.XLog
|
import com.elvishew.xlog.XLog
|
||||||
import com.github.salomonbrys.kotson.fromJson
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.annotations.SerializedName
|
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
||||||
import eu.kanade.tachiyomi.source.online.SuspendHttpSource
|
import eu.kanade.tachiyomi.source.online.SuspendHttpSource
|
||||||
|
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||||
|
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
||||||
import exh.MERGED_SOURCE_ID
|
import exh.MERGED_SOURCE_ID
|
||||||
|
import exh.merged.sql.models.MergedMangaReference
|
||||||
import exh.util.asFlow
|
import exh.util.asFlow
|
||||||
import exh.util.await
|
import exh.util.await
|
||||||
import exh.util.awaitSingle
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.asFlow
|
import kotlinx.coroutines.flow.asFlow
|
||||||
import kotlinx.coroutines.flow.buffer
|
import kotlinx.coroutines.flow.buffer
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.flatMapMerge
|
import kotlinx.coroutines.flow.flatMapMerge
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.take
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.toList
|
import kotlinx.coroutines.flow.single
|
||||||
|
import kotlinx.coroutines.flow.singleOrNull
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
// TODO LocalSource compatibility
|
|
||||||
// TODO Disable clear database option
|
|
||||||
class MergedSource : SuspendHttpSource() {
|
class MergedSource : SuspendHttpSource() {
|
||||||
private val db: DatabaseHelper by injectLazy()
|
private val db: DatabaseHelper by injectLazy()
|
||||||
private val sourceManager: SourceManager by injectLazy()
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
private val gson: Gson by injectLazy()
|
private val downloadManager: DownloadManager by injectLazy()
|
||||||
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
override val id: Long = MERGED_SOURCE_ID
|
override val id: Long = MERGED_SOURCE_ID
|
||||||
|
|
||||||
@ -49,150 +50,140 @@ class MergedSource : SuspendHttpSource() {
|
|||||||
override suspend fun searchMangaParseSuspended(response: Response) = throw UnsupportedOperationException()
|
override suspend fun searchMangaParseSuspended(response: Response) = throw UnsupportedOperationException()
|
||||||
override suspend fun latestUpdatesRequestSuspended(page: Int) = throw UnsupportedOperationException()
|
override suspend fun latestUpdatesRequestSuspended(page: Int) = throw UnsupportedOperationException()
|
||||||
override suspend fun latestUpdatesParseSuspended(response: Response) = throw UnsupportedOperationException()
|
override suspend fun latestUpdatesParseSuspended(response: Response) = throw UnsupportedOperationException()
|
||||||
|
override suspend fun mangaDetailsParseSuspended(response: Response) = throw UnsupportedOperationException()
|
||||||
|
override suspend fun chapterListParseSuspended(response: Response) = throw UnsupportedOperationException()
|
||||||
|
override suspend fun pageListParseSuspended(response: Response) = throw UnsupportedOperationException()
|
||||||
|
override suspend fun imageUrlParseSuspended(response: Response) = throw UnsupportedOperationException()
|
||||||
|
override fun fetchChapterListFlow(manga: SManga) = throw UnsupportedOperationException()
|
||||||
|
override fun fetchImageFlow(page: Page) = throw UnsupportedOperationException()
|
||||||
|
override fun fetchImageUrlFlow(page: Page) = throw UnsupportedOperationException()
|
||||||
|
override fun fetchPageListFlow(chapter: SChapter) = throw UnsupportedOperationException()
|
||||||
|
override fun fetchLatestUpdatesFlow(page: Int) = throw UnsupportedOperationException()
|
||||||
|
override fun fetchPopularMangaFlow(page: Int) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
override suspend fun fetchMangaDetailsSuspended(manga: SManga): SManga {
|
override fun fetchMangaDetailsFlow(manga: SManga): Flow<SManga> {
|
||||||
return readMangaConfig(manga).load(db, sourceManager).take(1).map { loaded ->
|
return flow {
|
||||||
SManga.create().apply {
|
val mergedManga = db.getManga(manga.url, id).await() ?: throw Exception("merged manga not in db")
|
||||||
this.copyFrom(loaded.manga)
|
val mangaReferences = mergedManga.id?.let { withContext(Dispatchers.IO) { db.getMergedMangaReferences(it).await() } } ?: throw Exception("merged manga id is null")
|
||||||
url = manga.url
|
if (mangaReferences.isEmpty()) throw IllegalArgumentException("Manga references are empty, info unavailable, merge is likely corrupted")
|
||||||
}
|
if (mangaReferences.size == 1 || {
|
||||||
}.first()
|
val mangaReference = mangaReferences.firstOrNull()
|
||||||
|
mangaReference == null || (mangaReference.mangaSourceId == MERGED_SOURCE_ID)
|
||||||
|
}()
|
||||||
|
) throw IllegalArgumentException("Manga references contain only the merged reference, merge is likely corrupted")
|
||||||
|
|
||||||
|
emit(
|
||||||
|
SManga.create().apply {
|
||||||
|
val mangaInfoReference = mangaReferences.firstOrNull { it.isInfoManga } ?: mangaReferences.firstOrNull { it.mangaId != it.mergeId }
|
||||||
|
val dbManga = mangaInfoReference?.let { withContext(Dispatchers.IO) { db.getManga(it.mangaUrl, it.mangaSourceId).await() } }
|
||||||
|
this.copyFrom(dbManga ?: mergedManga)
|
||||||
|
url = manga.url
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun fetchChapterListSuspended(manga: SManga): List<SChapter> {
|
fun getChaptersFromDB(manga: Manga, editScanlators: Boolean = false, dedupe: Boolean = true): Flow<List<Chapter>> {
|
||||||
val loadedMangas = readMangaConfig(manga).load(db, sourceManager).buffer()
|
// TODO more chapter dedupe
|
||||||
return loadedMangas.flatMapMerge { loadedManga ->
|
return db.getChaptersByMergedMangaId(manga.id!!).asRxObservable()
|
||||||
withContext(Dispatchers.IO) {
|
.asFlow()
|
||||||
loadedManga.source.fetchChapterList(loadedManga.manga).asFlow().map { chapterList ->
|
.map { chapterList ->
|
||||||
chapterList.map { chapter ->
|
val mangaReferences = withContext(Dispatchers.IO) { db.getMergedMangaReferences(manga.id!!).await() }
|
||||||
chapter.apply {
|
val sources = mangaReferences.map { sourceManager.getOrStub(it.mangaSourceId) to it.mangaId }
|
||||||
url = writeUrlConfig(
|
if (editScanlators) {
|
||||||
UrlConfig(
|
chapterList.onEach { chapter ->
|
||||||
loadedManga.source.id,
|
val source = sources.firstOrNull { chapter.manga_id == it.second }?.first
|
||||||
url,
|
if (source != null) {
|
||||||
loadedManga.manga.url
|
chapter.scanlator = if (chapter.scanlator.isNullOrBlank()) source.name
|
||||||
)
|
else "$source: ${chapter.scanlator}"
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (dedupe) dedupeChapterList(mangaReferences, chapterList) else chapterList
|
||||||
}
|
}
|
||||||
}.buffer().toList().flatten()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun mangaDetailsParseSuspended(response: Response) = throw UnsupportedOperationException()
|
private fun dedupeChapterList(mangaReferences: List<MergedMangaReference>, chapterList: List<Chapter>): List<Chapter> {
|
||||||
override suspend fun chapterListParseSuspended(response: Response) = throw UnsupportedOperationException()
|
return when (mangaReferences.firstOrNull { it.mangaSourceId == MERGED_SOURCE_ID }?.chapterSortMode) {
|
||||||
|
MergedMangaReference.CHAPTER_SORT_NO_DEDUPE, MergedMangaReference.CHAPTER_SORT_NONE -> chapterList
|
||||||
override suspend fun fetchPageListSuspended(chapter: SChapter): List<Page> {
|
MergedMangaReference.CHAPTER_SORT_PRIORITY -> chapterList
|
||||||
val config = readUrlConfig(chapter.url)
|
MergedMangaReference.CHAPTER_SORT_MOST_CHAPTERS -> {
|
||||||
val source = sourceManager.getOrStub(config.source)
|
findSourceWithMostChapters(chapterList)?.let { mangaId ->
|
||||||
return source.fetchPageList(
|
chapterList.filter { it.manga_id == mangaId }
|
||||||
SChapter.create().apply {
|
} ?: chapterList
|
||||||
copyFrom(chapter)
|
|
||||||
url = config.url
|
|
||||||
}
|
}
|
||||||
).map { pages ->
|
MergedMangaReference.CHAPTER_SORT_HIGHEST_CHAPTER_NUMBER -> {
|
||||||
pages.map { page ->
|
findSourceWithHighestChapterNumber(chapterList)?.let { mangaId ->
|
||||||
page.copyWithUrl(writeUrlConfig(UrlConfig(config.source, page.url, config.mangaUrl)))
|
chapterList.filter { it.manga_id == mangaId }
|
||||||
|
} ?: chapterList
|
||||||
}
|
}
|
||||||
}.awaitSingle()
|
else -> chapterList
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun fetchImageUrlSuspended(page: Page): String {
|
|
||||||
val config = readUrlConfig(page.url)
|
|
||||||
val source = sourceManager.getOrStub(config.source) as? HttpSource ?: throw UnsupportedOperationException("This source does not support this operation!")
|
|
||||||
return source.fetchImageUrl(page.copyWithUrl(config.url)).awaitSingle()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun pageListParseSuspended(response: Response) = throw UnsupportedOperationException()
|
|
||||||
override suspend fun imageUrlParseSuspended(response: Response) = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
override fun fetchImage(page: Page): Observable<Response> {
|
|
||||||
val config = readUrlConfig(page.url)
|
|
||||||
val source = sourceManager.getOrStub(config.source) as? HttpSource
|
|
||||||
?: throw UnsupportedOperationException("This source does not support this operation!")
|
|
||||||
return source.fetchImage(page.copyWithUrl(config.url))
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun prepareNewChapterSuspended(chapter: SChapter, manga: SManga) {
|
|
||||||
val chapterConfig = readUrlConfig(chapter.url)
|
|
||||||
val source = sourceManager.getOrStub(chapterConfig.source) as? HttpSource ?: throw UnsupportedOperationException("This source does not support this operation!")
|
|
||||||
val copiedManga = SManga.create().apply {
|
|
||||||
this.copyFrom(manga)
|
|
||||||
url = chapterConfig.mangaUrl
|
|
||||||
}
|
|
||||||
chapter.url = chapterConfig.url
|
|
||||||
source.prepareNewChapter(chapter, copiedManga)
|
|
||||||
chapter.url = writeUrlConfig(UrlConfig(source.id, chapter.url, chapterConfig.mangaUrl))
|
|
||||||
chapter.scanlator = if (chapter.scanlator.isNullOrBlank()) source.name
|
|
||||||
else "$source: ${chapter.scanlator}"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun readMangaConfig(manga: SManga): MangaConfig {
|
|
||||||
return MangaConfig.readFromUrl(gson, manga.url)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun readUrlConfig(url: String): UrlConfig {
|
|
||||||
return gson.fromJson(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun writeUrlConfig(urlConfig: UrlConfig): String {
|
|
||||||
return gson.toJson(urlConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class LoadedMangaSource(val source: Source, val manga: Manga)
|
|
||||||
data class MangaSource(
|
|
||||||
@SerializedName("s")
|
|
||||||
val source: Long,
|
|
||||||
@SerializedName("u")
|
|
||||||
val url: String
|
|
||||||
) {
|
|
||||||
suspend fun load(db: DatabaseHelper, sourceManager: SourceManager): LoadedMangaSource? {
|
|
||||||
val manga = db.getManga(url, source).await() ?: return null
|
|
||||||
val source = sourceManager.getOrStub(source)
|
|
||||||
return LoadedMangaSource(source, manga)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class MangaConfig(
|
private fun findSourceWithMostChapters(chapterList: List<Chapter>): Long? {
|
||||||
@SerializedName("c")
|
return chapterList.groupBy { it.manga_id }.maxByOrNull { it.value.size }?.key
|
||||||
val children: List<MangaSource>
|
}
|
||||||
) {
|
|
||||||
fun load(db: DatabaseHelper, sourceManager: SourceManager): Flow<LoadedMangaSource> {
|
private fun findSourceWithHighestChapterNumber(chapterList: List<Chapter>): Long? {
|
||||||
return children.asFlow().map { mangaSource ->
|
return chapterList.maxByOrNull { it.chapter_number }?.manga_id
|
||||||
mangaSource.load(db, sourceManager) ?: run {
|
}
|
||||||
XLog.w("> Missing source manga: $mangaSource")
|
|
||||||
throw IllegalStateException("Missing source manga: $mangaSource")
|
fun fetchChaptersForMergedManga(manga: Manga, downloadChapters: Boolean = true, editScanlators: Boolean = false, dedupe: Boolean = true): Flow<List<Chapter>> {
|
||||||
|
return flow {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
fetchChaptersAndSync(manga, downloadChapters).collect()
|
||||||
|
}
|
||||||
|
emit(
|
||||||
|
getChaptersFromDB(manga, editScanlators, dedupe).singleOrNull() ?: emptyList<Chapter>()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun fetchChaptersAndSync(manga: Manga, downloadChapters: Boolean = true): Flow<Pair<List<Chapter>, List<Chapter>>> {
|
||||||
|
val mangaReferences = db.getMergedMangaReferences(manga.id!!).await()
|
||||||
|
if (mangaReferences.isEmpty()) throw IllegalArgumentException("Manga references are empty, chapters unavailable, merge is likely corrupted")
|
||||||
|
|
||||||
|
val ifDownloadNewChapters = downloadChapters && manga.shouldDownloadNewChapters(db, preferences)
|
||||||
|
return mangaReferences.filter { it.mangaSourceId != MERGED_SOURCE_ID }.asFlow().map {
|
||||||
|
load(db, sourceManager, it)
|
||||||
|
}.buffer().flatMapMerge { loadedManga ->
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
if (loadedManga.manga != null && loadedManga.reference.getChapterUpdates) {
|
||||||
|
loadedManga.source.fetchChapterList(loadedManga.manga).asFlow()
|
||||||
|
.map { syncChaptersWithSource(db, it, loadedManga.manga, loadedManga.source) }
|
||||||
|
.onEach {
|
||||||
|
if (ifDownloadNewChapters && loadedManga.reference.downloadChapters) {
|
||||||
|
downloadManager.downloadChapters(loadedManga.manga, it.first)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emptyList<Pair<List<Chapter>, List<Chapter>>>().asFlow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}.buffer()
|
||||||
|
|
||||||
fun writeAsUrl(gson: Gson): String {
|
|
||||||
return gson.toJson(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun readFromUrl(gson: Gson, url: String): MangaConfig {
|
|
||||||
return gson.fromJson(url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class UrlConfig(
|
suspend fun load(db: DatabaseHelper, sourceManager: SourceManager, reference: MergedMangaReference): LoadedMangaSource {
|
||||||
@SerializedName("s")
|
var manga = db.getManga(reference.mangaUrl, reference.mangaSourceId).await()
|
||||||
val source: Long,
|
val source = sourceManager.getOrStub(manga?.source ?: reference.mangaSourceId)
|
||||||
@SerializedName("u")
|
if (manga == null) {
|
||||||
val url: String,
|
manga = Manga.create(reference.mangaSourceId).apply {
|
||||||
@SerializedName("m")
|
url = reference.mangaUrl
|
||||||
val mangaUrl: String
|
}
|
||||||
)
|
manga.copyFrom(source.fetchMangaDetails(manga).asFlow().single())
|
||||||
|
try {
|
||||||
|
manga.id = db.insertManga(manga).await().insertedId()
|
||||||
|
reference.mangaId = manga.id
|
||||||
|
db.insertNewMergedMangaId(reference).await()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
XLog.st(e.stackTrace.contentToString(), 5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return LoadedMangaSource(source, manga, reference)
|
||||||
|
}
|
||||||
|
|
||||||
fun Page.copyWithUrl(newUrl: String) = Page(
|
data class LoadedMangaSource(val source: Source, val manga: Manga?, val reference: MergedMangaReference)
|
||||||
index,
|
|
||||||
newUrl,
|
|
||||||
imageUrl,
|
|
||||||
uri
|
|
||||||
)
|
|
||||||
|
|
||||||
override val lang = "all"
|
override val lang = "all"
|
||||||
override val supportsLatest = false
|
override val supportsLatest = false
|
||||||
|
@ -5,7 +5,6 @@ import android.widget.PopupMenu
|
|||||||
import androidx.core.view.isInvisible
|
import androidx.core.view.isInvisible
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
import com.google.gson.Gson
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
@ -13,7 +12,6 @@ import eu.kanade.tachiyomi.data.glide.GlideApp
|
|||||||
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
|
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
@ -21,6 +19,7 @@ import eu.kanade.tachiyomi.util.lang.launchUI
|
|||||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||||
import eu.kanade.tachiyomi.util.view.setVectorCompat
|
import eu.kanade.tachiyomi.util.view.setVectorCompat
|
||||||
import exh.MERGED_SOURCE_ID
|
import exh.MERGED_SOURCE_ID
|
||||||
|
import exh.util.await
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
import kotlinx.android.synthetic.main.migration_manga_card.view.gradient
|
import kotlinx.android.synthetic.main.migration_manga_card.view.gradient
|
||||||
import kotlinx.android.synthetic.main.migration_manga_card.view.loading_group
|
import kotlinx.android.synthetic.main.migration_manga_card.view.loading_group
|
||||||
@ -51,7 +50,6 @@ class MigrationProcessHolder(
|
|||||||
private val db: DatabaseHelper by injectLazy()
|
private val db: DatabaseHelper by injectLazy()
|
||||||
private val sourceManager: SourceManager by injectLazy()
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
private var item: MigrationProcessItem? = null
|
private var item: MigrationProcessItem? = null
|
||||||
private val gson: Gson by injectLazy()
|
|
||||||
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@ -154,7 +152,7 @@ class MigrationProcessHolder(
|
|||||||
migration_manga_card_to.clicks()
|
migration_manga_card_to.clicks()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun View.attachManga(manga: Manga, source: Source) {
|
private suspend fun View.attachManga(manga: Manga, source: Source) {
|
||||||
loading_group.isVisible = false
|
loading_group.isVisible = false
|
||||||
GlideApp.with(view.context.applicationContext)
|
GlideApp.with(view.context.applicationContext)
|
||||||
.load(manga.toMangaThumbnail())
|
.load(manga.toMangaThumbnail())
|
||||||
@ -171,8 +169,8 @@ class MigrationProcessHolder(
|
|||||||
|
|
||||||
gradient.isVisible = true
|
gradient.isVisible = true
|
||||||
manga_source_label.text = if (source.id == MERGED_SOURCE_ID) {
|
manga_source_label.text = if (source.id == MERGED_SOURCE_ID) {
|
||||||
MergedSource.MangaConfig.readFromUrl(gson, manga.url).children.map {
|
db.getMergedMangaReferences(manga.id!!).await().map {
|
||||||
sourceManager.getOrStub(it.source).toString()
|
sourceManager.getOrStub(it.mangaSourceId).toString()
|
||||||
}.distinct().joinToString()
|
}.distinct().joinToString()
|
||||||
} else {
|
} else {
|
||||||
source.toString()
|
source.toString()
|
||||||
|
@ -441,6 +441,7 @@ class SourceController(bundle: Bundle? = null) :
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val SMART_SEARCH_CONFIG = "SMART_SEARCH_CONFIG"
|
const val SMART_SEARCH_CONFIG = "SMART_SEARCH_CONFIG"
|
||||||
|
const val SMART_SEARCH_SOURCE_TAG = "smart_search_source_tag"
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.source.model.Filter.TriState.Companion.STATE_IGNORE
|
|||||||
import eu.kanade.tachiyomi.source.model.Filter.TriState.Companion.STATE_INCLUDE
|
import eu.kanade.tachiyomi.source.model.Filter.TriState.Companion.STATE_INCLUDE
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
import eu.kanade.tachiyomi.util.isLocal
|
import eu.kanade.tachiyomi.util.isLocal
|
||||||
import eu.kanade.tachiyomi.util.lang.combineLatest
|
import eu.kanade.tachiyomi.util.lang.combineLatest
|
||||||
@ -27,11 +28,15 @@ import eu.kanade.tachiyomi.util.lang.launchIO
|
|||||||
import eu.kanade.tachiyomi.util.removeCovers
|
import eu.kanade.tachiyomi.util.removeCovers
|
||||||
import exh.EH_SOURCE_ID
|
import exh.EH_SOURCE_ID
|
||||||
import exh.EXH_SOURCE_ID
|
import exh.EXH_SOURCE_ID
|
||||||
|
import exh.MERGED_SOURCE_ID
|
||||||
import exh.favorites.FavoritesSyncHelper
|
import exh.favorites.FavoritesSyncHelper
|
||||||
|
import exh.util.await
|
||||||
import exh.util.isLewd
|
import exh.util.isLewd
|
||||||
import exh.util.nullIfBlank
|
import exh.util.nullIfBlank
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import java.util.Comparator
|
import java.util.Comparator
|
||||||
|
import kotlinx.coroutines.flow.singleOrNull
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
@ -241,7 +246,10 @@ class LibraryPresenter(
|
|||||||
for ((_, itemList) in map) {
|
for ((_, itemList) in map) {
|
||||||
for (item in itemList) {
|
for (item in itemList) {
|
||||||
item.downloadCount = if (showDownloadBadges) {
|
item.downloadCount = if (showDownloadBadges) {
|
||||||
downloadManager.getDownloadCount(item.manga)
|
// SY -->
|
||||||
|
if (item.manga.source == MERGED_SOURCE_ID) {
|
||||||
|
item.manga.id?.let { mergeMangaId -> db.getMergedMangas(mergeMangaId).executeAsBlocking().map { downloadManager.getDownloadCount(it) }.sum() } ?: 0
|
||||||
|
} else /* SY <-- */ downloadManager.getDownloadCount(item.manga)
|
||||||
} else {
|
} else {
|
||||||
// Unset download count if not enabled
|
// Unset download count if not enabled
|
||||||
-1
|
-1
|
||||||
@ -455,8 +463,10 @@ class LibraryPresenter(
|
|||||||
mangas.forEach { manga ->
|
mangas.forEach { manga ->
|
||||||
launchIO {
|
launchIO {
|
||||||
/* SY --> */ val chapters = if (manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID) {
|
/* SY --> */ val chapters = if (manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID) {
|
||||||
val chapter = db.getChapters(manga).executeAsBlocking().minByOrNull { it.source_order }
|
val chapter = db.getChapters(manga).await().minByOrNull { it.source_order }
|
||||||
if (chapter != null && !chapter.read) listOf(chapter) else emptyList()
|
if (chapter != null && !chapter.read) listOf(chapter) else emptyList()
|
||||||
|
} else if (manga.source == MERGED_SOURCE_ID) {
|
||||||
|
(sourceManager.getOrStub(MERGED_SOURCE_ID) as? MergedSource)?.getChaptersFromDB(manga)?.singleOrNull()?.filter { !it.read } ?: emptyList()
|
||||||
} else /* SY <-- */ db.getChapters(manga).executeAsBlocking()
|
} else /* SY <-- */ db.getChapters(manga).executeAsBlocking()
|
||||||
.filter { !it.read }
|
.filter { !it.read }
|
||||||
|
|
||||||
@ -501,7 +511,7 @@ class LibraryPresenter(
|
|||||||
fun markReadStatus(mangas: List<Manga>, read: Boolean) {
|
fun markReadStatus(mangas: List<Manga>, read: Boolean) {
|
||||||
mangas.forEach { manga ->
|
mangas.forEach { manga ->
|
||||||
launchIO {
|
launchIO {
|
||||||
val chapters = db.getChapters(manga).executeAsBlocking()
|
val chapters = if (manga.source == MERGED_SOURCE_ID) (sourceManager.get(MERGED_SOURCE_ID) as? MergedSource)?.getChaptersFromDB(manga)?.singleOrNull() ?: emptyList() else db.getChapters(manga).executeAsBlocking()
|
||||||
chapters.forEach {
|
chapters.forEach {
|
||||||
it.read = read
|
it.read = read
|
||||||
if (!read) {
|
if (!read) {
|
||||||
@ -519,7 +529,16 @@ class LibraryPresenter(
|
|||||||
|
|
||||||
private fun deleteChapters(manga: Manga, chapters: List<Chapter>) {
|
private fun deleteChapters(manga: Manga, chapters: List<Chapter>) {
|
||||||
sourceManager.get(manga.source)?.let { source ->
|
sourceManager.get(manga.source)?.let { source ->
|
||||||
downloadManager.deleteChapters(chapters, manga, source)
|
// SY -->
|
||||||
|
if (source is MergedSource) {
|
||||||
|
val mergedMangas = db.getMergedMangas(manga.id!!).executeAsBlocking()
|
||||||
|
val sources = mergedMangas.distinctBy { it.source }.map { sourceManager.getOrStub(it.source) }
|
||||||
|
chapters.groupBy { it.manga_id }.forEach { map ->
|
||||||
|
val mergedManga = mergedMangas.firstOrNull { it.id == map.key } ?: return@forEach
|
||||||
|
val mergedMangaSource = sources.firstOrNull { it.id == mergedManga.source } ?: return@forEach
|
||||||
|
downloadManager.deleteChapters(map.value, mergedManga, mergedMangaSource)
|
||||||
|
}
|
||||||
|
} else /* SY <-- */ downloadManager.deleteChapters(chapters, manga, source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -543,7 +562,14 @@ class LibraryPresenter(
|
|||||||
mangaToDelete.forEach { manga ->
|
mangaToDelete.forEach { manga ->
|
||||||
val source = sourceManager.get(manga.source) as? HttpSource
|
val source = sourceManager.get(manga.source) as? HttpSource
|
||||||
if (source != null) {
|
if (source != null) {
|
||||||
downloadManager.deleteManga(manga, source)
|
if (source is MergedSource) {
|
||||||
|
val mergedMangas = db.getMergedMangas(manga.id!!).await()
|
||||||
|
val sources = mergedMangas.distinctBy { it.source }.map { sourceManager.getOrStub(it.source) }
|
||||||
|
mergedMangas.forEach merge@{ mergedManga ->
|
||||||
|
val mergedSource = sources.firstOrNull { mergedManga.source == it.id } ?: return@merge
|
||||||
|
downloadManager.deleteManga(mergedManga, mergedSource)
|
||||||
|
}
|
||||||
|
} else downloadManager.deleteManga(manga, source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -571,7 +597,7 @@ class LibraryPresenter(
|
|||||||
// SY -->
|
// SY -->
|
||||||
/** Returns first unread chapter of a manga */
|
/** Returns first unread chapter of a manga */
|
||||||
fun getFirstUnread(manga: Manga): Chapter? {
|
fun getFirstUnread(manga: Manga): Chapter? {
|
||||||
val chapters = db.getChapters(manga).executeAsBlocking()
|
val chapters = (if (manga.source == MERGED_SOURCE_ID) (sourceManager.get(MERGED_SOURCE_ID) as? MergedSource).let { runBlocking { it?.getChaptersFromDB(manga)?.singleOrNull() } ?: emptyList() } else db.getChapters(manga).executeAsBlocking())
|
||||||
return if (manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID) {
|
return if (manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID) {
|
||||||
val chapter = chapters.sortedBy { it.source_order }.getOrNull(0)
|
val chapter = chapters.sortedBy { it.source_order }.getOrNull(0)
|
||||||
if (chapter?.read == false) chapter else null
|
if (chapter?.read == false) chapter else null
|
||||||
|
@ -54,9 +54,11 @@ import eu.kanade.tachiyomi.source.online.MetadataSource.Companion.getMetadataSou
|
|||||||
import eu.kanade.tachiyomi.ui.base.controller.FabController
|
import eu.kanade.tachiyomi.ui.base.controller.FabController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.ToolbarLiftOnScrollController
|
import eu.kanade.tachiyomi.ui.base.controller.ToolbarLiftOnScrollController
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.popControllerWithTag
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController
|
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.SourceController
|
import eu.kanade.tachiyomi.ui.browse.source.SourceController
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.source.SourceController.Companion.SMART_SEARCH_SOURCE_TAG
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
||||||
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
|
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
|
||||||
@ -74,6 +76,7 @@ import eu.kanade.tachiyomi.ui.manga.chapter.MangaChaptersHeaderAdapter
|
|||||||
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoButtonsAdapter
|
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoButtonsAdapter
|
||||||
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoHeaderAdapter
|
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoHeaderAdapter
|
||||||
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoItemAdapter
|
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoItemAdapter
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.merged.EditMergedSettingsDialog
|
||||||
import eu.kanade.tachiyomi.ui.manga.track.TrackController
|
import eu.kanade.tachiyomi.ui.manga.track.TrackController
|
||||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||||
import eu.kanade.tachiyomi.ui.recent.history.HistoryController
|
import eu.kanade.tachiyomi.ui.recent.history.HistoryController
|
||||||
@ -85,6 +88,7 @@ import eu.kanade.tachiyomi.util.system.toast
|
|||||||
import eu.kanade.tachiyomi.util.view.getCoordinates
|
import eu.kanade.tachiyomi.util.view.getCoordinates
|
||||||
import eu.kanade.tachiyomi.util.view.shrinkOnScroll
|
import eu.kanade.tachiyomi.util.view.shrinkOnScroll
|
||||||
import eu.kanade.tachiyomi.util.view.snack
|
import eu.kanade.tachiyomi.util.view.snack
|
||||||
|
import exh.MERGED_SOURCE_ID
|
||||||
import exh.isEhBasedSource
|
import exh.isEhBasedSource
|
||||||
import exh.metadata.metadata.base.FlatMetadata
|
import exh.metadata.metadata.base.FlatMetadata
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@ -195,6 +199,8 @@ class MangaController :
|
|||||||
|
|
||||||
private var editMangaDialog: EditMangaDialog? = null
|
private var editMangaDialog: EditMangaDialog? = null
|
||||||
|
|
||||||
|
private var editMergedSettingsDialog: EditMergedSettingsDialog? = null
|
||||||
|
|
||||||
private var currentAnimator: Animator? = null
|
private var currentAnimator: Animator? = null
|
||||||
// EXH <--
|
// EXH <--
|
||||||
|
|
||||||
@ -423,6 +429,8 @@ class MangaController :
|
|||||||
// SY -->
|
// SY -->
|
||||||
if (presenter.manga.favorite) menu.findItem(R.id.action_edit).isVisible = true
|
if (presenter.manga.favorite) menu.findItem(R.id.action_edit).isVisible = true
|
||||||
if (preferences.recommendsInOverflow().get()) menu.findItem(R.id.action_recommend).isVisible = true
|
if (preferences.recommendsInOverflow().get()) menu.findItem(R.id.action_recommend).isVisible = true
|
||||||
|
menu.findItem(R.id.action_merged).isVisible = presenter.manga.source == MERGED_SOURCE_ID
|
||||||
|
menu.findItem(R.id.action_toggle_dedupe).isVisible = false // presenter.manga.source == MERGED_SOURCE_ID
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -443,6 +451,16 @@ class MangaController :
|
|||||||
R.id.action_recommend -> {
|
R.id.action_recommend -> {
|
||||||
openRecommends()
|
openRecommends()
|
||||||
}
|
}
|
||||||
|
R.id.action_merged -> {
|
||||||
|
editMergedSettingsDialog = EditMergedSettingsDialog(
|
||||||
|
this, presenter.manga
|
||||||
|
)
|
||||||
|
editMergedSettingsDialog?.showDialog(router)
|
||||||
|
}
|
||||||
|
R.id.action_toggle_dedupe -> {
|
||||||
|
presenter.dedupe = !presenter.dedupe
|
||||||
|
presenter.toggleDedupe()
|
||||||
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
R.id.action_edit_categories -> onCategoriesClick()
|
R.id.action_edit_categories -> onCategoriesClick()
|
||||||
@ -633,7 +651,7 @@ class MangaController :
|
|||||||
Bundle().apply {
|
Bundle().apply {
|
||||||
putParcelable(SourceController.SMART_SEARCH_CONFIG, smartSearchConfig)
|
putParcelable(SourceController.SMART_SEARCH_CONFIG, smartSearchConfig)
|
||||||
}
|
}
|
||||||
).withFadeTransaction()
|
).withFadeTransaction().tag(SMART_SEARCH_SOURCE_TAG)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -643,7 +661,9 @@ class MangaController :
|
|||||||
presenter.smartSearchMerge(presenter.manga, smartSearchConfig?.origMangaId!!)
|
presenter.smartSearchMerge(presenter.manga, smartSearchConfig?.origMangaId!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
router?.pushController(
|
router?.popControllerWithTag(SMART_SEARCH_SOURCE_TAG)
|
||||||
|
router?.popCurrentController()
|
||||||
|
router?.replaceTopController(
|
||||||
MangaController(
|
MangaController(
|
||||||
mergedManga,
|
mergedManga,
|
||||||
true,
|
true,
|
||||||
|
@ -4,7 +4,6 @@ import android.content.Context
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.elvishew.xlog.XLog
|
import com.elvishew.xlog.XLog
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
import com.jakewharton.rxrelay.BehaviorRelay
|
||||||
import com.jakewharton.rxrelay.PublishRelay
|
import com.jakewharton.rxrelay.PublishRelay
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
@ -20,6 +19,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
import eu.kanade.tachiyomi.source.LocalSource
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||||
import eu.kanade.tachiyomi.source.online.MetadataSource.Companion.isMetadataSource
|
import eu.kanade.tachiyomi.source.online.MetadataSource.Companion.isMetadataSource
|
||||||
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||||
@ -37,10 +37,12 @@ import exh.MERGED_SOURCE_ID
|
|||||||
import exh.debug.DebugToggles
|
import exh.debug.DebugToggles
|
||||||
import exh.eh.EHentaiUpdateHelper
|
import exh.eh.EHentaiUpdateHelper
|
||||||
import exh.isEhBasedSource
|
import exh.isEhBasedSource
|
||||||
|
import exh.merged.sql.models.MergedMangaReference
|
||||||
import exh.metadata.metadata.base.FlatMetadata
|
import exh.metadata.metadata.base.FlatMetadata
|
||||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||||
import exh.metadata.metadata.base.getFlatMetadataForManga
|
import exh.metadata.metadata.base.getFlatMetadataForManga
|
||||||
import exh.source.EnhancedHttpSource
|
import exh.source.EnhancedHttpSource
|
||||||
|
import exh.util.asObservable
|
||||||
import exh.util.await
|
import exh.util.await
|
||||||
import exh.util.trimOrNull
|
import exh.util.trimOrNull
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
@ -63,9 +65,7 @@ class MangaPresenter(
|
|||||||
private val trackManager: TrackManager = Injekt.get(),
|
private val trackManager: TrackManager = Injekt.get(),
|
||||||
private val downloadManager: DownloadManager = Injekt.get(),
|
private val downloadManager: DownloadManager = Injekt.get(),
|
||||||
private val coverCache: CoverCache = Injekt.get(),
|
private val coverCache: CoverCache = Injekt.get(),
|
||||||
// SY -->
|
private val sourceManager: SourceManager = Injekt.get()
|
||||||
private val gson: Gson = Injekt.get()
|
|
||||||
// SY <--
|
|
||||||
) : BasePresenter<MangaController>() {
|
) : BasePresenter<MangaController>() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -112,6 +112,10 @@ class MangaPresenter(
|
|||||||
data class EXHRedirect(val manga: Manga, val update: Boolean)
|
data class EXHRedirect(val manga: Manga, val update: Boolean)
|
||||||
|
|
||||||
var meta: RaisedSearchMetadata? = null
|
var meta: RaisedSearchMetadata? = null
|
||||||
|
|
||||||
|
private var mergedManga = emptyList<Manga>()
|
||||||
|
|
||||||
|
var dedupe: Boolean = true
|
||||||
// EXH <--
|
// EXH <--
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
@ -121,6 +125,10 @@ class MangaPresenter(
|
|||||||
if (manga.initialized && source.isMetadataSource()) {
|
if (manga.initialized && source.isMetadataSource()) {
|
||||||
getMangaMetaObservable().subscribeLatestCache({ view, flatMetadata -> if (flatMetadata != null) view.onNextMetaInfo(flatMetadata) else XLog.d("Invalid metadata") })
|
getMangaMetaObservable().subscribeLatestCache({ view, flatMetadata -> if (flatMetadata != null) view.onNextMetaInfo(flatMetadata) else XLog.d("Invalid metadata") })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (source is MergedSource) {
|
||||||
|
launchIO { mergedManga = db.getMergedMangas(manga.id!!).await() }
|
||||||
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
// Manga info - start
|
// Manga info - start
|
||||||
@ -145,7 +153,7 @@ class MangaPresenter(
|
|||||||
// Add the subscription that retrieves the chapters from the database, keeps subscribed to
|
// Add the subscription that retrieves the chapters from the database, keeps subscribed to
|
||||||
// changes, and sends the list of chapters to the relay.
|
// changes, and sends the list of chapters to the relay.
|
||||||
add(
|
add(
|
||||||
db.getChapters(manga).asRxObservable()
|
(/* SY --> */if (source is MergedSource) source.getChaptersFromDB(manga, true, dedupe).asObservable() else /* SY <-- */ db.getChapters(manga).asRxObservable())
|
||||||
.map { chapters ->
|
.map { chapters ->
|
||||||
// Convert every chapter to a model.
|
// Convert every chapter to a model.
|
||||||
chapters.map { it.toModel() }
|
chapters.map { it.toModel() }
|
||||||
@ -334,64 +342,138 @@ class MangaPresenter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun smartSearchMerge(manga: Manga, originalMangaId: Long): Manga {
|
suspend fun smartSearchMerge(manga: Manga, originalMangaId: Long): Manga {
|
||||||
val originalManga = db.getManga(originalMangaId).await()
|
val originalManga = db.getManga(originalMangaId).await() ?: throw IllegalArgumentException("Unknown manga ID: $originalMangaId")
|
||||||
?: throw IllegalArgumentException("Unknown manga ID: $originalMangaId")
|
if (originalManga.source == MERGED_SOURCE_ID) {
|
||||||
val toInsert = if (originalManga.source == MERGED_SOURCE_ID) {
|
val children = db.getMergedMangaReferences(originalMangaId).await()
|
||||||
originalManga.apply {
|
if (children.any { it.mangaSourceId == manga.source && it.mangaUrl == manga.url }) {
|
||||||
val originalChildren = MergedSource.MangaConfig.readFromUrl(gson, url).children
|
throw IllegalArgumentException("This manga is already merged with the current manga!")
|
||||||
if (originalChildren.any { it.source == manga.source && it.url == manga.url }) {
|
|
||||||
throw IllegalArgumentException("This manga is already merged with the current manga!")
|
|
||||||
}
|
|
||||||
|
|
||||||
url = MergedSource.MangaConfig(
|
|
||||||
originalChildren + MergedSource.MangaSource(
|
|
||||||
manga.source,
|
|
||||||
manga.url
|
|
||||||
)
|
|
||||||
).writeAsUrl(gson)
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
val newMangaConfig = MergedSource.MangaConfig(
|
val mangaReferences = mutableListOf(
|
||||||
listOf(
|
MergedMangaReference(
|
||||||
MergedSource.MangaSource(
|
id = null,
|
||||||
originalManga.source,
|
isInfoManga = false,
|
||||||
originalManga.url
|
getChapterUpdates = true,
|
||||||
),
|
chapterSortMode = 0,
|
||||||
MergedSource.MangaSource(
|
chapterPriority = 0,
|
||||||
manga.source,
|
downloadChapters = true,
|
||||||
manga.url
|
mergeId = originalManga.id!!,
|
||||||
)
|
mergeUrl = originalManga.url,
|
||||||
|
mangaId = manga.id!!,
|
||||||
|
mangaUrl = manga.url,
|
||||||
|
mangaSourceId = manga.source
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
Manga.create(newMangaConfig.writeAsUrl(gson), originalManga.title, MERGED_SOURCE_ID).apply {
|
|
||||||
|
if (children.isEmpty() || children.all { it.mangaSourceId != MERGED_SOURCE_ID }) {
|
||||||
|
mangaReferences += MergedMangaReference(
|
||||||
|
id = null,
|
||||||
|
isInfoManga = false,
|
||||||
|
getChapterUpdates = false,
|
||||||
|
chapterSortMode = 0,
|
||||||
|
chapterPriority = -1,
|
||||||
|
downloadChapters = false,
|
||||||
|
mergeId = originalManga.id!!,
|
||||||
|
mergeUrl = originalManga.url,
|
||||||
|
mangaId = originalManga.id!!,
|
||||||
|
mangaUrl = originalManga.url,
|
||||||
|
mangaSourceId = MERGED_SOURCE_ID
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
db.insertMergedMangas(mangaReferences).await()
|
||||||
|
|
||||||
|
return originalManga
|
||||||
|
} else {
|
||||||
|
val mergedManga = Manga.create(originalManga.url, originalManga.title, MERGED_SOURCE_ID).apply {
|
||||||
copyFrom(originalManga)
|
copyFrom(originalManga)
|
||||||
favorite = true
|
favorite = true
|
||||||
last_update = originalManga.last_update
|
last_update = originalManga.last_update
|
||||||
viewer = originalManga.viewer
|
viewer = originalManga.viewer
|
||||||
chapter_flags = originalManga.chapter_flags
|
chapter_flags = originalManga.chapter_flags
|
||||||
sorting = Manga.SORTING_NUMBER
|
sorting = Manga.SORTING_NUMBER
|
||||||
|
date_added = Date().time
|
||||||
}
|
}
|
||||||
|
var existingManga = db.getManga(mergedManga.url, mergedManga.source).await()
|
||||||
|
while (existingManga != null) {
|
||||||
|
if (existingManga.favorite) {
|
||||||
|
throw IllegalArgumentException("This merged manga is a duplicate!")
|
||||||
|
} else if (!existingManga.favorite) {
|
||||||
|
withContext(NonCancellable) {
|
||||||
|
db.deleteManga(existingManga!!).await()
|
||||||
|
db.deleteMangaForMergedManga(existingManga!!.id!!).await()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
existingManga = db.getManga(mergedManga.url, mergedManga.source).await()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload chapters immediately
|
||||||
|
mergedManga.initialized = false
|
||||||
|
|
||||||
|
val newId = db.insertManga(mergedManga).await().insertedId()
|
||||||
|
if (newId != null) mergedManga.id = newId
|
||||||
|
|
||||||
|
val originalMangaReference = MergedMangaReference(
|
||||||
|
id = null,
|
||||||
|
isInfoManga = true,
|
||||||
|
getChapterUpdates = true,
|
||||||
|
chapterSortMode = 0,
|
||||||
|
chapterPriority = 0,
|
||||||
|
downloadChapters = true,
|
||||||
|
mergeId = mergedManga.id!!,
|
||||||
|
mergeUrl = mergedManga.url,
|
||||||
|
mangaId = originalManga.id!!,
|
||||||
|
mangaUrl = originalManga.url,
|
||||||
|
mangaSourceId = originalManga.source
|
||||||
|
)
|
||||||
|
|
||||||
|
val newMangaReference = MergedMangaReference(
|
||||||
|
id = null,
|
||||||
|
isInfoManga = false,
|
||||||
|
getChapterUpdates = true,
|
||||||
|
chapterSortMode = 0,
|
||||||
|
chapterPriority = 0,
|
||||||
|
downloadChapters = true,
|
||||||
|
mergeId = mergedManga.id!!,
|
||||||
|
mergeUrl = mergedManga.url,
|
||||||
|
mangaId = manga.id!!,
|
||||||
|
mangaUrl = manga.url,
|
||||||
|
mangaSourceId = manga.source
|
||||||
|
)
|
||||||
|
|
||||||
|
val mergedMangaReference = MergedMangaReference(
|
||||||
|
id = null,
|
||||||
|
isInfoManga = false,
|
||||||
|
getChapterUpdates = false,
|
||||||
|
chapterSortMode = 0,
|
||||||
|
chapterPriority = -1,
|
||||||
|
downloadChapters = false,
|
||||||
|
mergeId = mergedManga.id!!,
|
||||||
|
mergeUrl = mergedManga.url,
|
||||||
|
mangaId = mergedManga.id!!,
|
||||||
|
mangaUrl = mergedManga.url,
|
||||||
|
mangaSourceId = MERGED_SOURCE_ID
|
||||||
|
)
|
||||||
|
|
||||||
|
db.insertMergedMangas(listOf(originalMangaReference, newMangaReference, mergedMangaReference)).await()
|
||||||
|
|
||||||
|
return mergedManga
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note that if the manga are merged in a different order, this won't trigger, but I don't care lol
|
// Note that if the manga are merged in a different order, this won't trigger, but I don't care lol
|
||||||
val existingManga = db.getManga(toInsert.url, toInsert.source).await()
|
}
|
||||||
if (existingManga != null) {
|
|
||||||
withContext(NonCancellable) {
|
fun updateMergeSettings(mergeReference: MergedMangaReference?, mergedMangaReferences: List<MergedMangaReference>) {
|
||||||
if (toInsert.id != null) {
|
launchIO {
|
||||||
db.deleteManga(toInsert).await()
|
mergeReference?.let {
|
||||||
}
|
db.updateMergeMangaSettings(it).await()
|
||||||
}
|
}
|
||||||
|
if (mergedMangaReferences.isNotEmpty()) db.updateMergedMangaSettings(mergedMangaReferences).await()
|
||||||
return existingManga
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Reload chapters immediately
|
fun toggleDedupe() {
|
||||||
toInsert.initialized = false
|
// I cant find any way to call the chapter list subscription to get the chapters again
|
||||||
|
|
||||||
val newId = db.insertManga(toInsert).await().insertedId()
|
|
||||||
if (newId != null) toInsert.id = newId
|
|
||||||
|
|
||||||
return toInsert
|
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
@ -424,7 +506,13 @@ class MangaPresenter(
|
|||||||
* Deletes all the downloads for the manga.
|
* Deletes all the downloads for the manga.
|
||||||
*/
|
*/
|
||||||
fun deleteDownloads() {
|
fun deleteDownloads() {
|
||||||
downloadManager.deleteManga(manga, source)
|
// SY -->
|
||||||
|
if (source is MergedSource) {
|
||||||
|
val mergedManga = mergedManga.map { it to sourceManager.getOrStub(it.source) }
|
||||||
|
mergedManga.forEach { (manga, source) ->
|
||||||
|
downloadManager.deleteManga(manga, source)
|
||||||
|
}
|
||||||
|
} else /* SY <-- */ downloadManager.deleteManga(manga, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -515,10 +603,14 @@ class MangaPresenter(
|
|||||||
// Chapters list - start
|
// Chapters list - start
|
||||||
|
|
||||||
private fun observeDownloads() {
|
private fun observeDownloads() {
|
||||||
|
// SY -->
|
||||||
|
val isMergedSource = source is MergedSource
|
||||||
|
val mergedIds = if (isMergedSource) mergedManga.mapNotNull { it.id } else emptyList()
|
||||||
|
// SY <--
|
||||||
observeDownloadsSubscription?.let { remove(it) }
|
observeDownloadsSubscription?.let { remove(it) }
|
||||||
observeDownloadsSubscription = downloadManager.queue.getStatusObservable()
|
observeDownloadsSubscription = downloadManager.queue.getStatusObservable()
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.filter { download -> download.manga.id == manga.id }
|
.filter { download -> /* SY --> */ if (isMergedSource) download.manga.id in mergedIds else /* SY <-- */ download.manga.id == manga.id }
|
||||||
.doOnNext { onDownloadStatusChange(it) }
|
.doOnNext { onDownloadStatusChange(it) }
|
||||||
.subscribeLatestCache(MangaController::onChapterStatusChange) { _, error ->
|
.subscribeLatestCache(MangaController::onChapterStatusChange) { _, error ->
|
||||||
Timber.e(error)
|
Timber.e(error)
|
||||||
@ -548,8 +640,11 @@ class MangaPresenter(
|
|||||||
* @param chapters the list of chapter from the database.
|
* @param chapters the list of chapter from the database.
|
||||||
*/
|
*/
|
||||||
private fun setDownloadedChapters(chapters: List<ChapterItem>) {
|
private fun setDownloadedChapters(chapters: List<ChapterItem>) {
|
||||||
|
// SY -->
|
||||||
|
val isMergedSource = source is MergedSource
|
||||||
|
// SY <--
|
||||||
chapters
|
chapters
|
||||||
.filter { downloadManager.isChapterDownloaded(it, manga) }
|
.filter { downloadManager.isChapterDownloaded(it, /* SY --> */ if (isMergedSource) mergedManga.firstOrNull { manga -> it.manga_id == manga.id } ?: manga else /* SY <-- */ manga) }
|
||||||
.forEach { it.status = Download.DOWNLOADED }
|
.forEach { it.status = Download.DOWNLOADED }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -560,21 +655,38 @@ class MangaPresenter(
|
|||||||
hasRequested = true
|
hasRequested = true
|
||||||
|
|
||||||
if (!fetchChaptersSubscription.isNullOrUnsubscribed()) return
|
if (!fetchChaptersSubscription.isNullOrUnsubscribed()) return
|
||||||
fetchChaptersSubscription = Observable.defer { source.fetchChapterList(manga) }
|
fetchChaptersSubscription = /* SY --> */ if (source !is MergedSource) {
|
||||||
.subscribeOn(Schedulers.io())
|
// SY <--
|
||||||
.map { syncChaptersWithSource(db, it, manga, source) }
|
Observable.defer { source.fetchChapterList(manga) }
|
||||||
.doOnNext {
|
.subscribeOn(Schedulers.io())
|
||||||
if (manualFetch) {
|
.map { syncChaptersWithSource(db, it, manga, source) }
|
||||||
downloadNewChapters(it.first)
|
.doOnNext {
|
||||||
|
if (manualFetch) {
|
||||||
|
downloadNewChapters(it.first)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.subscribeFirst(
|
||||||
.subscribeFirst(
|
{ view, _ ->
|
||||||
{ view, _ ->
|
view.onFetchChaptersDone()
|
||||||
view.onFetchChaptersDone()
|
},
|
||||||
},
|
MangaController::onFetchChaptersError
|
||||||
MangaController::onFetchChaptersError
|
)
|
||||||
)
|
// SY -->
|
||||||
|
} else {
|
||||||
|
Observable.defer { source.fetchChaptersForMergedManga(manga, manualFetch, true, dedupe).asObservable() }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.doOnNext {
|
||||||
|
}
|
||||||
|
.subscribeFirst(
|
||||||
|
{ view, _ ->
|
||||||
|
view.onFetchChaptersDone()
|
||||||
|
},
|
||||||
|
MangaController::onFetchChaptersError
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -680,7 +792,13 @@ class MangaPresenter(
|
|||||||
* @param chapters the list of chapters to download.
|
* @param chapters the list of chapters to download.
|
||||||
*/
|
*/
|
||||||
fun downloadChapters(chapters: List<Chapter>) {
|
fun downloadChapters(chapters: List<Chapter>) {
|
||||||
downloadManager.downloadChapters(manga, chapters)
|
// SY -->
|
||||||
|
if (source is MergedSource) {
|
||||||
|
chapters.groupBy { it.manga_id }.forEach { map ->
|
||||||
|
val manga = mergedManga.firstOrNull { it.id == map.key } ?: return@forEach
|
||||||
|
downloadManager.downloadChapters(manga, map.value)
|
||||||
|
}
|
||||||
|
} else /* SY <-- */ downloadManager.downloadChapters(manga, chapters)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -8,6 +8,7 @@ import androidx.core.view.isVisible
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||||
import eu.kanade.tachiyomi.data.glide.MangaThumbnail
|
import eu.kanade.tachiyomi.data.glide.MangaThumbnail
|
||||||
@ -18,7 +19,6 @@ import eu.kanade.tachiyomi.source.Source
|
|||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
import eu.kanade.tachiyomi.util.view.setTooltip
|
import eu.kanade.tachiyomi.util.view.setTooltip
|
||||||
@ -41,6 +41,10 @@ class MangaInfoHeaderAdapter(
|
|||||||
RecyclerView.Adapter<MangaInfoHeaderAdapter.HeaderViewHolder>() {
|
RecyclerView.Adapter<MangaInfoHeaderAdapter.HeaderViewHolder>() {
|
||||||
|
|
||||||
private val trackManager: TrackManager by injectLazy()
|
private val trackManager: TrackManager by injectLazy()
|
||||||
|
// SY -->
|
||||||
|
private val db: DatabaseHelper by injectLazy()
|
||||||
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
|
// SY <--
|
||||||
|
|
||||||
private var manga: Manga = controller.presenter.manga
|
private var manga: Manga = controller.presenter.manga
|
||||||
private var source: Source = controller.presenter.source
|
private var source: Source = controller.presenter.source
|
||||||
@ -254,9 +258,9 @@ class MangaInfoHeaderAdapter(
|
|||||||
val mangaSource = source?.toString()
|
val mangaSource = source?.toString()
|
||||||
with(binding.mangaSource) {
|
with(binding.mangaSource) {
|
||||||
// SY -->
|
// SY -->
|
||||||
if (source != null && source.id == MERGED_SOURCE_ID) {
|
if (source?.id == MERGED_SOURCE_ID) {
|
||||||
text = MergedSource.MangaConfig.readFromUrl(Injekt.get(), manga.url).children.map {
|
text = db.getMergedMangaReferences(manga.id!!).executeAsBlocking().map {
|
||||||
Injekt.get<SourceManager>().getOrStub(it.source).toString()
|
sourceManager.getOrStub(it.mangaSourceId).toString()
|
||||||
}.distinct().joinToString()
|
}.distinct().joinToString()
|
||||||
} else /* SY <-- */ if (mangaSource != null) {
|
} else /* SY <-- */ if (mangaSource != null) {
|
||||||
text = mangaSource
|
text = mangaSource
|
||||||
|
@ -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.LocalSource
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
import eu.kanade.tachiyomi.ui.reader.chapter.ReaderChapterItem
|
import eu.kanade.tachiyomi.ui.reader.chapter.ReaderChapterItem
|
||||||
import eu.kanade.tachiyomi.ui.reader.loader.ChapterLoader
|
import eu.kanade.tachiyomi.ui.reader.loader.ChapterLoader
|
||||||
@ -32,6 +33,7 @@ import eu.kanade.tachiyomi.util.system.ImageUtil
|
|||||||
import eu.kanade.tachiyomi.util.updateCoverLastModified
|
import eu.kanade.tachiyomi.util.updateCoverLastModified
|
||||||
import exh.EH_SOURCE_ID
|
import exh.EH_SOURCE_ID
|
||||||
import exh.EXH_SOURCE_ID
|
import exh.EXH_SOURCE_ID
|
||||||
|
import exh.MERGED_SOURCE_ID
|
||||||
import exh.util.defaultReaderType
|
import exh.util.defaultReaderType
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
@ -39,6 +41,8 @@ import java.text.DecimalFormatSymbols
|
|||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.singleOrNull
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import rx.Completable
|
import rx.Completable
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
@ -97,7 +101,7 @@ class ReaderPresenter(
|
|||||||
*/
|
*/
|
||||||
private val chapterList by lazy {
|
private val chapterList by lazy {
|
||||||
val manga = manga!!
|
val manga = manga!!
|
||||||
val dbChapters = db.getChapters(manga).executeAsBlocking()
|
val dbChapters = if (manga.source == MERGED_SOURCE_ID) runBlocking { (sourceManager.get(MERGED_SOURCE_ID) as? MergedSource)?.getChaptersFromDB(manga)?.singleOrNull() ?: emptyList() } else db.getChapters(manga).executeAsBlocking()
|
||||||
|
|
||||||
val selectedChapter = dbChapters.find { it.id == chapterId }
|
val selectedChapter = dbChapters.find { it.id == chapterId }
|
||||||
?: error("Requested chapter of id $chapterId not found in chapter list")
|
?: error("Requested chapter of id $chapterId not found in chapter list")
|
||||||
@ -236,7 +240,9 @@ class ReaderPresenter(
|
|||||||
|
|
||||||
val context = Injekt.get<Application>()
|
val context = Injekt.get<Application>()
|
||||||
val source = sourceManager.getOrStub(manga.source)
|
val source = sourceManager.getOrStub(manga.source)
|
||||||
loader = ChapterLoader(context, downloadManager, manga, source)
|
val mergedReferences = if (source is MergedSource) db.getMergedMangaReferences(manga.id!!).executeAsBlocking() else emptyList()
|
||||||
|
val mergedManga = if (source is MergedSource) db.getMergedMangas(manga.id!!).executeAsBlocking() else emptyList()
|
||||||
|
loader = ChapterLoader(context, downloadManager, manga, source, sourceManager, mergedReferences, mergedManga)
|
||||||
|
|
||||||
Observable.just(manga).subscribeLatestCache(ReaderActivity::setManga)
|
Observable.just(manga).subscribeLatestCache(ReaderActivity::setManga)
|
||||||
viewerChaptersRelay.subscribeLatestCache(ReaderActivity::setChapters)
|
viewerChaptersRelay.subscribeLatestCache(ReaderActivity::setChapters)
|
||||||
|
@ -6,9 +6,12 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
|||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
import eu.kanade.tachiyomi.source.LocalSource
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||||
import exh.debug.DebugFunctions.prefs
|
import exh.debug.DebugFunctions.prefs
|
||||||
|
import exh.merged.sql.models.MergedMangaReference
|
||||||
import rx.Completable
|
import rx.Completable
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
@ -22,7 +25,12 @@ class ChapterLoader(
|
|||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val downloadManager: DownloadManager,
|
private val downloadManager: DownloadManager,
|
||||||
private val manga: Manga,
|
private val manga: Manga,
|
||||||
private val source: Source
|
private val source: Source,
|
||||||
|
// SY -->
|
||||||
|
private val sourceManager: SourceManager,
|
||||||
|
private val mergedReferences: List<MergedMangaReference>,
|
||||||
|
private val mergedManga: List<Manga>
|
||||||
|
// SY <--
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -81,6 +89,27 @@ class ChapterLoader(
|
|||||||
private fun getPageLoader(chapter: ReaderChapter): PageLoader {
|
private fun getPageLoader(chapter: ReaderChapter): PageLoader {
|
||||||
val isDownloaded = downloadManager.isChapterDownloaded(chapter.chapter, manga, true)
|
val isDownloaded = downloadManager.isChapterDownloaded(chapter.chapter, manga, true)
|
||||||
return when {
|
return when {
|
||||||
|
// SY -->
|
||||||
|
source is MergedSource -> {
|
||||||
|
val mangaReference = mergedReferences.firstOrNull { it.mangaId == chapter.chapter.manga_id } ?: throw Exception("Merge reference null")
|
||||||
|
val source = sourceManager.get(mangaReference.mangaSourceId) ?: throw Exception("Source ${mangaReference.mangaSourceId} was null")
|
||||||
|
val manga = mergedManga.firstOrNull { it.id == chapter.chapter.manga_id } ?: throw Exception("Manga for merged chapter was null")
|
||||||
|
val isMergedMangaDownloaded = downloadManager.isChapterDownloaded(chapter.chapter, manga, true)
|
||||||
|
when {
|
||||||
|
isMergedMangaDownloaded -> DownloadPageLoader(chapter, manga, source, downloadManager)
|
||||||
|
source is HttpSource -> HttpPageLoader(chapter, source)
|
||||||
|
source is LocalSource -> source.getFormat(chapter.chapter).let { format ->
|
||||||
|
when (format) {
|
||||||
|
is LocalSource.Format.Directory -> DirectoryPageLoader(format.file)
|
||||||
|
is LocalSource.Format.Zip -> ZipPageLoader(format.file)
|
||||||
|
is LocalSource.Format.Rar -> RarPageLoader(format.file)
|
||||||
|
is LocalSource.Format.Epub -> EpubPageLoader(format.file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> error(context.getString(R.string.loader_not_implemented_error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
isDownloaded -> DownloadPageLoader(chapter, manga, source, downloadManager)
|
isDownloaded -> DownloadPageLoader(chapter, manga, source, downloadManager)
|
||||||
source is HttpSource -> HttpPageLoader(chapter, source)
|
source is HttpSource -> HttpPageLoader(chapter, source)
|
||||||
source is LocalSource -> source.getFormat(chapter.chapter).let { format ->
|
source is LocalSource -> source.getFormat(chapter.chapter).let { format ->
|
||||||
|
@ -2,6 +2,9 @@ package exh
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.elvishew.xlog.XLog
|
import com.elvishew.xlog.XLog
|
||||||
|
import com.github.salomonbrys.kotson.fromJson
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||||
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
@ -12,13 +15,18 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
|||||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaUrlPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaUrlPutResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.all.Hitomi
|
import eu.kanade.tachiyomi.source.online.all.Hitomi
|
||||||
import eu.kanade.tachiyomi.source.online.all.NHentai
|
import eu.kanade.tachiyomi.source.online.all.NHentai
|
||||||
|
import exh.merged.sql.models.MergedMangaReference
|
||||||
import exh.source.BlacklistedSources
|
import exh.source.BlacklistedSources
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
@ -27,6 +35,8 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
|
|
||||||
object EXHMigrations {
|
object EXHMigrations {
|
||||||
private val db: DatabaseHelper by injectLazy()
|
private val db: DatabaseHelper by injectLazy()
|
||||||
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
|
private val gson: Gson by injectLazy()
|
||||||
|
|
||||||
private val logger = XLog.tag("EXHMigrations")
|
private val logger = XLog.tag("EXHMigrations")
|
||||||
|
|
||||||
@ -143,6 +153,106 @@ object EXHMigrations {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 7) {
|
||||||
|
db.inTransaction {
|
||||||
|
val mergedMangas = db.db.get()
|
||||||
|
.listOfObjects(Manga::class.java)
|
||||||
|
.withQuery(
|
||||||
|
Query.builder()
|
||||||
|
.table(MangaTable.TABLE)
|
||||||
|
.where("${MangaTable.COL_SOURCE} = $MERGED_SOURCE_ID")
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
|
.executeAsBlocking()
|
||||||
|
|
||||||
|
if (mergedMangas.isNotEmpty()) {
|
||||||
|
val mangaConfigs = mergedMangas.mapNotNull { mergedManga -> readMangaConfig(mergedManga, gson)?.let { mergedManga to it } }
|
||||||
|
if (mangaConfigs.isNotEmpty()) {
|
||||||
|
val mangaToUpdate = mutableListOf<Manga>()
|
||||||
|
val mergedMangaReferences = mutableListOf<MergedMangaReference>()
|
||||||
|
mangaConfigs.onEach { mergedManga ->
|
||||||
|
mergedManga.second.children.firstOrNull()?.url?.let {
|
||||||
|
if (db.getManga(it, MERGED_SOURCE_ID).executeAsBlocking() != null) return@onEach
|
||||||
|
mergedManga.first.url = it
|
||||||
|
}
|
||||||
|
mangaToUpdate += mergedManga.first
|
||||||
|
mergedMangaReferences += MergedMangaReference(
|
||||||
|
id = null,
|
||||||
|
isInfoManga = false,
|
||||||
|
getChapterUpdates = false,
|
||||||
|
chapterSortMode = 0,
|
||||||
|
chapterPriority = 0,
|
||||||
|
downloadChapters = false,
|
||||||
|
mergeId = mergedManga.first.id!!,
|
||||||
|
mergeUrl = mergedManga.first.url,
|
||||||
|
mangaId = mergedManga.first.id!!,
|
||||||
|
mangaUrl = mergedManga.first.url,
|
||||||
|
mangaSourceId = MERGED_SOURCE_ID
|
||||||
|
)
|
||||||
|
mergedManga.second.children.distinct().forEachIndexed { index, mangaSource ->
|
||||||
|
val load = mangaSource.load(db, sourceManager) ?: return@forEachIndexed
|
||||||
|
mergedMangaReferences += MergedMangaReference(
|
||||||
|
id = null,
|
||||||
|
isInfoManga = index == 0,
|
||||||
|
getChapterUpdates = true,
|
||||||
|
chapterSortMode = 0,
|
||||||
|
chapterPriority = 0,
|
||||||
|
downloadChapters = true,
|
||||||
|
mergeId = mergedManga.first.id!!,
|
||||||
|
mergeUrl = mergedManga.first.url,
|
||||||
|
mangaId = load.manga.id!!,
|
||||||
|
mangaUrl = load.manga.url,
|
||||||
|
mangaSourceId = load.source.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
db.db.put()
|
||||||
|
.objects(mangaToUpdate)
|
||||||
|
// Extremely slow without the resolver :/
|
||||||
|
.withPutResolver(MangaUrlPutResolver())
|
||||||
|
.prepare()
|
||||||
|
.executeAsBlocking()
|
||||||
|
db.insertMergedMangas(mergedMangaReferences).executeAsBlocking()
|
||||||
|
|
||||||
|
val loadedMangaList = mangaConfigs.map { it.second.children }.flatten().mapNotNull { it.load(db, sourceManager) }.distinct()
|
||||||
|
val chapters = db.db.get()
|
||||||
|
.listOfObjects(Chapter::class.java)
|
||||||
|
.withQuery(
|
||||||
|
Query.builder()
|
||||||
|
.table(ChapterTable.TABLE)
|
||||||
|
.where("${ChapterTable.COL_MANGA_ID} IN (${mergedMangas.filter { it.id != null }.joinToString { it.id.toString() }})")
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
|
.executeAsBlocking()
|
||||||
|
val mergedMangaChapters = db.db.get()
|
||||||
|
.listOfObjects(Chapter::class.java)
|
||||||
|
.withQuery(
|
||||||
|
Query.builder()
|
||||||
|
.table(ChapterTable.TABLE)
|
||||||
|
.where("${ChapterTable.COL_MANGA_ID} IN (${loadedMangaList.filter { it.manga.id != null }.joinToString { it.manga.id.toString() }})")
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
|
.executeAsBlocking()
|
||||||
|
val mergedMangaChaptersMatched = mergedMangaChapters.mapNotNull { chapter -> loadedMangaList.firstOrNull { it.manga.id == chapter.id }?.let { it to chapter } }
|
||||||
|
val parsedChapters = chapters.filter { it.read || it.last_page_read != 0 }.mapNotNull { chapter -> readUrlConfig(chapter.url, gson)?.let { chapter to it } }
|
||||||
|
val chaptersToUpdate = mutableListOf<Chapter>()
|
||||||
|
parsedChapters.forEach { parsedChapter ->
|
||||||
|
mergedMangaChaptersMatched.firstOrNull { it.second.url == parsedChapter.second.url && it.first.source.id == parsedChapter.second.source && it.first.manga.url == parsedChapter.second.mangaUrl }?.let {
|
||||||
|
chaptersToUpdate += it.second.apply {
|
||||||
|
read = parsedChapter.first.read
|
||||||
|
last_page_read = parsedChapter.first.last_page_read
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
db.deleteChapters(mergedMangaChapters).executeAsBlocking()
|
||||||
|
db.updateChaptersProgress(chaptersToUpdate).executeAsBlocking()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// if (oldVersion < 1) { } (1 is current release version)
|
// if (oldVersion < 1) { } (1 is current release version)
|
||||||
// do stuff here when releasing changed crap
|
// do stuff here when releasing changed crap
|
||||||
@ -228,6 +338,57 @@ object EXHMigrations {
|
|||||||
orig
|
orig
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private data class UrlConfig(
|
||||||
|
@SerializedName("s")
|
||||||
|
val source: Long,
|
||||||
|
@SerializedName("u")
|
||||||
|
val url: String,
|
||||||
|
@SerializedName("m")
|
||||||
|
val mangaUrl: String
|
||||||
|
)
|
||||||
|
|
||||||
|
private data class MangaConfig(
|
||||||
|
@SerializedName("c")
|
||||||
|
val children: List<MangaSource>
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun readFromUrl(gson: Gson, url: String): MangaConfig? {
|
||||||
|
return try {
|
||||||
|
gson.fromJson(url)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readMangaConfig(manga: SManga, gson: Gson): MangaConfig? {
|
||||||
|
return MangaConfig.readFromUrl(gson, manga.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class MangaSource(
|
||||||
|
@SerializedName("s")
|
||||||
|
val source: Long,
|
||||||
|
@SerializedName("u")
|
||||||
|
val url: String
|
||||||
|
) {
|
||||||
|
fun load(db: DatabaseHelper, sourceManager: SourceManager): LoadedMangaSource? {
|
||||||
|
val manga = db.getManga(url, source).executeAsBlocking() ?: return null
|
||||||
|
val source = sourceManager.getOrStub(source)
|
||||||
|
return LoadedMangaSource(source, manga)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readUrlConfig(url: String, gson: Gson): UrlConfig? {
|
||||||
|
return try {
|
||||||
|
gson.fromJson(url)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class LoadedMangaSource(val source: Source, val manga: Manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class BackupEntry(
|
data class BackupEntry(
|
||||||
|
@ -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 ->
|
suspend fun Completable.awaitCompleted(): Unit = suspendCancellableCoroutine { cont ->
|
||||||
subscribe(object : CompletableSubscriber {
|
subscribe(object : CompletableSubscriber {
|
||||||
override fun onSubscribe(s: Subscription) { cont.unsubscribeOnCancellation(s) }
|
override fun onSubscribe(s: Subscription) {
|
||||||
override fun onCompleted() { cont.resume(Unit) }
|
cont.unsubscribeOnCancellation(s)
|
||||||
override fun onError(e: Throwable) { cont.resumeWithException(e) }
|
}
|
||||||
|
|
||||||
|
override fun onCompleted() {
|
||||||
|
cont.resume(Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(e: Throwable) {
|
||||||
|
cont.resumeWithException(e)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun <T> Single<T>.await(): T = suspendCancellableCoroutine { cont ->
|
suspend fun <T> Single<T>.await(): T = suspendCancellableCoroutine { cont ->
|
||||||
cont.unsubscribeOnCancellation(
|
cont.unsubscribeOnCancellation(
|
||||||
subscribe(object : SingleSubscriber<T>() {
|
subscribe(object : SingleSubscriber<T>() {
|
||||||
override fun onSuccess(t: T) { cont.resume(t) }
|
override fun onSuccess(t: T) {
|
||||||
override fun onError(error: Throwable) { cont.resumeWithException(error) }
|
cont.resume(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(error: Throwable) {
|
||||||
|
cont.resumeWithException(error)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -129,7 +142,11 @@ suspend fun <T> Observable<T>.awaitFirstOrDefault(default: T): T = firstOrDefaul
|
|||||||
suspend fun <T> Observable<T>.awaitFirstOrNull(): T? = firstOrDefault(null).awaitOne()
|
suspend fun <T> Observable<T>.awaitFirstOrNull(): T? = firstOrDefault(null).awaitOne()
|
||||||
|
|
||||||
@OptIn(InternalCoroutinesApi::class, ExperimentalCoroutinesApi::class)
|
@OptIn(InternalCoroutinesApi::class, ExperimentalCoroutinesApi::class)
|
||||||
suspend fun <T> Observable<T>.awaitFirstOrElse(defaultValue: () -> T): T = switchIfEmpty(Observable.fromCallable(defaultValue)).first().awaitOne()
|
suspend fun <T> Observable<T>.awaitFirstOrElse(defaultValue: () -> T): T = switchIfEmpty(
|
||||||
|
Observable.fromCallable(
|
||||||
|
defaultValue
|
||||||
|
)
|
||||||
|
).first().awaitOne()
|
||||||
|
|
||||||
@OptIn(InternalCoroutinesApi::class, ExperimentalCoroutinesApi::class)
|
@OptIn(InternalCoroutinesApi::class, ExperimentalCoroutinesApi::class)
|
||||||
suspend fun <T> Observable<T>.awaitLast(): T = last().awaitOne()
|
suspend fun <T> Observable<T>.awaitLast(): T = last().awaitOne()
|
||||||
@ -141,11 +158,24 @@ suspend fun <T> Observable<T>.awaitSingle(): T = single().awaitOne()
|
|||||||
private suspend fun <T> Observable<T>.awaitOne(): T = suspendCancellableCoroutine { cont ->
|
private suspend fun <T> Observable<T>.awaitOne(): T = suspendCancellableCoroutine { cont ->
|
||||||
cont.unsubscribeOnCancellation(
|
cont.unsubscribeOnCancellation(
|
||||||
subscribe(object : Subscriber<T>() {
|
subscribe(object : Subscriber<T>() {
|
||||||
override fun onStart() { request(1) }
|
override fun onStart() {
|
||||||
override fun onNext(t: T) { cont.resume(t) }
|
request(1)
|
||||||
override fun onCompleted() { if (cont.isActive) cont.resumeWithException(IllegalStateException("Should have invoked onNext")) }
|
}
|
||||||
|
|
||||||
|
override fun onNext(t: T) {
|
||||||
|
cont.resume(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCompleted() {
|
||||||
|
if (cont.isActive) cont.resumeWithException(
|
||||||
|
IllegalStateException(
|
||||||
|
"Should have invoked onNext"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onError(e: Throwable) {
|
override fun onError(e: Throwable) {
|
||||||
/*
|
/*
|
||||||
* Rx1 observable throws NoSuchElementException if cancellation happened before
|
* Rx1 observable throws NoSuchElementException if cancellation happened before
|
||||||
* element emission. To mitigate this we try to atomically resume continuation with exception:
|
* element emission. To mitigate this we try to atomically resume continuation with exception:
|
||||||
* if resume failed, then we know that continuation successfully cancelled itself
|
* if resume failed, then we know that continuation successfully cancelled itself
|
||||||
@ -185,7 +215,7 @@ fun <T : Any> Observable<T>.asFlow(): Flow<T> = callbackFlow {
|
|||||||
fun <T : Any> Flow<T>.asObservable(backpressureMode: Emitter.BackpressureMode = Emitter.BackpressureMode.NONE): Observable<T> {
|
fun <T : Any> Flow<T>.asObservable(backpressureMode: Emitter.BackpressureMode = Emitter.BackpressureMode.NONE): Observable<T> {
|
||||||
return Observable.create(
|
return Observable.create(
|
||||||
{ emitter ->
|
{ emitter ->
|
||||||
/*
|
/*
|
||||||
* ATOMIC is used here to provide stable behaviour of subscribe+dispose pair even if
|
* ATOMIC is used here to provide stable behaviour of subscribe+dispose pair even if
|
||||||
* asObservable is already invoked from unconfined
|
* asObservable is already invoked from unconfined
|
||||||
*/
|
*/
|
||||||
|
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_height="wrap_content"
|
||||||
android:layout_marginBottom="8dp"
|
android:layout_marginBottom="8dp"
|
||||||
android:text="Searching source..."
|
android:text="Searching source..."
|
||||||
android:textAppearance="@style/TextAppearance.Medium.Title"
|
android:textAppearance="@style/TextAppearance.Medium.Title" />
|
||||||
android:textColor="@android:color/white" />
|
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
android:id="@+id/intercept_progress"
|
android:id="@+id/intercept_progress"
|
||||||
style="?android:attr/progressBarStyleLarge"
|
style="?android:attr/progressBarStyleLarge"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center" />
|
||||||
android:indeterminateTint="@android:color/white" />
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
@ -57,4 +57,18 @@
|
|||||||
android:title="@string/az_recommends"
|
android:title="@string/az_recommends"
|
||||||
android:visible="false"
|
android:visible="false"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_merged"
|
||||||
|
android:icon="@drawable/ic_edit_24dp"
|
||||||
|
android:title="@string/merge_settings"
|
||||||
|
android:visible="false"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_toggle_dedupe"
|
||||||
|
android:icon="@drawable/ic_edit_24dp"
|
||||||
|
android:title="@string/toggle_dedupe"
|
||||||
|
android:visible="false"
|
||||||
|
app:showAsAction="never" />
|
||||||
</menu>
|
</menu>
|
||||||
|
@ -483,4 +483,23 @@
|
|||||||
<item quantity="other">%2$s, %1$d pages</item>
|
<item quantity="other">%2$s, %1$d pages</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
|
<!-- Merged manga -->
|
||||||
|
<string name="merge_settings">Merge settings</string>
|
||||||
|
<string name="fetch_chapter_updates">Fetch chapter updates</string>
|
||||||
|
<string name="delete_merged_manga">Are you sure?</string>
|
||||||
|
<string name="delete_merged_manga_desc">This will remove the manga from the merge, using this will also lose any unsaved changes applied to the merged manga</string>
|
||||||
|
<string name="chapter_updates_merged_manga">Toggle chapter updates</string>
|
||||||
|
<string name="chapter_updates_merged_manga_desc">Toggling this will disable or enable chapter updates for this merged manga</string>
|
||||||
|
<string name="download_merged_manga">Toggle new chapter downloads</string>
|
||||||
|
<string name="download_merged_manga_desc">Toggling this will disable or enable chapter downloads for this merged manga</string>
|
||||||
|
<string name="merged_references_invalid">Merged references invalid</string>
|
||||||
|
<string name="merged_chapter_updates_error">Toggle chapter updates error</string>
|
||||||
|
<string name="merged_toggle_chapter_updates_find_error">Could not find manga to toggle chapter updates</string>
|
||||||
|
<string name="merged_toggle_download_chapters_error">Toggle download chapters error</string>
|
||||||
|
<string name="merged_toggle_download_chapters_find_error">Could not find manga to toggle chapter downloads</string>
|
||||||
|
<string name="allow_deduplication">Allow deduplication:</string>
|
||||||
|
<string name="deduplication_mode">Dedupe mode:</string>
|
||||||
|
<string name="manga_info_manga">Info manga:</string>
|
||||||
|
<string name="toggle_dedupe">Toggle dedupe</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
Loading…
x
Reference in New Issue
Block a user