Initial MergedSource creation UI

This commit is contained in:
NerdNumber9 2019-07-30 01:41:30 -04:00
parent 4c9be5557d
commit 10d6b3a6ca
13 changed files with 256 additions and 44 deletions

View File

@ -8,8 +8,6 @@ apply plugin: 'kotlin-kapt'
apply plugin: 'com.github.zellius.shortcut-helper' apply plugin: 'com.github.zellius.shortcut-helper'
// Realm (EH) // Realm (EH)
apply plugin: 'realm-android' apply plugin: 'realm-android'
// Firebase (EH)
apply plugin: 'io.fabric'
shortcutHelper.filePath = './shortcuts.xml' shortcutHelper.filePath = './shortcuts.xml'
@ -139,7 +137,8 @@ dependencies {
implementation 'com.android.support:multidex:1.0.3' implementation 'com.android.support:multidex:1.0.3'
standardImplementation 'com.google.firebase:firebase-core:17.0.1' // DO NOT UPGRADE TO 17.0, IT REQUIRES ANDROIDX
standardImplementation 'com.google.firebase:firebase-core:16.0.8'
// ReactiveX // ReactiveX
implementation 'io.reactivex:rxandroid:1.2.1' implementation 'io.reactivex:rxandroid:1.2.1'
@ -174,6 +173,7 @@ dependencies {
// Job scheduling // Job scheduling
implementation 'com.evernote:android-job:1.2.5' implementation 'com.evernote:android-job:1.2.5'
// DO NOT UPGRADE TO 17.0, IT REQUIRES ANDROIDX
implementation 'com.google.android.gms:play-services-gcm:15.0.1' implementation 'com.google.android.gms:play-services-gcm:15.0.1'
// Changelog // Changelog
@ -274,6 +274,7 @@ dependencies {
// RxJava 2 interop for Realm (EH) // RxJava 2 interop for Realm (EH)
implementation 'com.lvla.android:rxjava2-interop-kt:0.2.1' implementation 'com.lvla.android:rxjava2-interop-kt:0.2.1'
implementation 'com.github.akarnokd:rxjava2-interop:0.13.7'
// Debug network interceptor (EH) // Debug network interceptor (EH)
implementation "com.squareup.okhttp3:logging-interceptor:3.12.1" implementation "com.squareup.okhttp3:logging-interceptor:3.12.1"
@ -297,6 +298,8 @@ dependencies {
// Humanize (EH) // Humanize (EH)
implementation 'com.github.mfornos:humanize-slim:1.2.2' implementation 'com.github.mfornos:humanize-slim:1.2.2'
implementation 'com.android.support:gridlayout-v7:28.0.0'
} }
buildscript { buildscript {
@ -319,4 +322,6 @@ androidExtensions {
if (getGradle().getStartParameter().getTaskRequests().toString().contains("Standard")) { if (getGradle().getStartParameter().getTaskRequests().toString().contains("Standard")) {
apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.gms.google-services'
// Firebase (EH)
apply plugin: 'io.fabric'
} }

View File

@ -9,10 +9,7 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.all.EHentai import eu.kanade.tachiyomi.source.online.all.*
import eu.kanade.tachiyomi.source.online.all.Hitomi
import eu.kanade.tachiyomi.source.online.all.NHentai
import eu.kanade.tachiyomi.source.online.all.PervEden
import eu.kanade.tachiyomi.source.online.english.HentaiCafe import eu.kanade.tachiyomi.source.online.english.HentaiCafe
import eu.kanade.tachiyomi.source.online.english.Tsumino import eu.kanade.tachiyomi.source.online.english.Tsumino
import rx.Observable import rx.Observable
@ -51,6 +48,8 @@ open class SourceManager(private val context: Context) {
Observable.merge(prefEntries).skip(prefEntries.size - 1).subscribe { Observable.merge(prefEntries).skip(prefEntries.size - 1).subscribe {
createEHSources().forEach { registerSource(it) } createEHSources().forEach { registerSource(it) }
} }
registerSource(MergedSource())
} }
open fun get(sourceKey: Long): Source? { open fun get(sourceKey: Long): Source? {

View File

@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.* import eu.kanade.tachiyomi.source.model.*
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import exh.MERGED_SOURCE_ID
import exh.util.await import exh.util.await
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@ -30,6 +31,8 @@ class MergedSource : HttpSource() {
private val sourceManager: SourceManager by injectLazy() private val sourceManager: SourceManager by injectLazy()
private val gson: Gson by injectLazy() private val gson: Gson by injectLazy()
override val id: Long = MERGED_SOURCE_ID
override val baseUrl = "" override val baseUrl = ""
override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException() override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException()
@ -51,14 +54,16 @@ class MergedSource : HttpSource() {
return GlobalScope.async(Dispatchers.IO) { return GlobalScope.async(Dispatchers.IO) {
val loadedMangas = readMangaConfig(manga).load(db, sourceManager).buffer() val loadedMangas = readMangaConfig(manga).load(db, sourceManager).buffer()
loadedMangas.map { loadedManga -> loadedMangas.map { loadedManga ->
loadedManga.source.fetchChapterList(loadedManga.manga).map { chapterList -> async(Dispatchers.IO) {
chapterList.map { chapter -> loadedManga.source.fetchChapterList(loadedManga.manga).map { chapterList ->
chapter.apply { chapterList.map { chapter ->
url = writeUrlConfig(UrlConfig(loadedManga.source.id, url, loadedManga.manga.url)) chapter.apply {
url = writeUrlConfig(UrlConfig(loadedManga.source.id, url, loadedManga.manga.url))
}
} }
} }.toSingle().await(Schedulers.io())
} }
}.buffer().map { it.toSingle().await(Schedulers.io()) }.toList().flatten() }.buffer().map { it.await() }.toList().flatten()
}.asSingle(Dispatchers.IO).toV1Single().toObservable() }.asSingle(Dispatchers.IO).toV1Single().toObservable()
} }
override fun mangaDetailsParse(response: Response) = throw UnsupportedOperationException() override fun mangaDetailsParse(response: Response) = throw UnsupportedOperationException()
@ -102,11 +107,12 @@ class MergedSource : HttpSource() {
chapter.url = chapterConfig.url chapter.url = chapterConfig.url
source.prepareNewChapter(chapter, copiedManga) source.prepareNewChapter(chapter, copiedManga)
chapter.url = writeUrlConfig(UrlConfig(source.id, chapter.url, chapterConfig.mangaUrl)) chapter.url = writeUrlConfig(UrlConfig(source.id, chapter.url, chapterConfig.mangaUrl))
chapter.scanlator = "${source.name}: ${chapter.scanlator}" chapter.scanlator = if(chapter.scanlator.isNullOrBlank()) source.name
else "${source.name}: ${chapter.scanlator}"
} }
fun readMangaConfig(manga: SManga): MangaConfig { fun readMangaConfig(manga: SManga): MangaConfig {
return gson.fromJson(manga.url) return MangaConfig.readFromUrl(gson, manga.url)
} }
fun readUrlConfig(url: String): UrlConfig { fun readUrlConfig(url: String): UrlConfig {
return gson.fromJson(url) return gson.fromJson(url)
@ -126,11 +132,6 @@ class MergedSource : HttpSource() {
val source = sourceManager.getOrStub(source) val source = sourceManager.getOrStub(source)
return LoadedMangaSource(source, manga) return LoadedMangaSource(source, manga)
} }
companion object {
fun fromLoadedMangaSource(loadedMS: LoadedMangaSource): MangaSource {
return MangaSource(loadedMS.source.id, loadedMS.manga.url)
}
}
} }
data class MangaConfig( data class MangaConfig(
@SerializedName("c") @SerializedName("c")
@ -142,6 +143,16 @@ class MergedSource : HttpSource() {
?: throw IllegalStateException("Missing source manga: $mangaSource") ?: throw IllegalStateException("Missing source manga: $mangaSource")
} }
} }
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( data class UrlConfig(
@SerializedName("s") @SerializedName("s")

View File

@ -257,7 +257,7 @@ class CatalogueController(bundle: Bundle? = null) : NucleusController<CatalogueP
// EXH --> // EXH -->
@Parcelize @Parcelize
data class SmartSearchConfig(val title: String) : Parcelable data class SmartSearchConfig(val origTitle: String, val origMangaId: Long) : Parcelable
// EXH <-- // EXH <--
enum class Mode { enum class Mode {

View File

@ -22,6 +22,7 @@ import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.manga.info.MangaWebViewController import eu.kanade.tachiyomi.ui.manga.info.MangaWebViewController
@ -50,11 +51,15 @@ open class BrowseCatalogueController(bundle: Bundle) :
ChangeMangaCategoriesDialog.Listener { ChangeMangaCategoriesDialog.Listener {
constructor(source: CatalogueSource, constructor(source: CatalogueSource,
searchQuery: String? = null) : this(Bundle().apply { searchQuery: String? = null,
smartSearchConfig: CatalogueController.SmartSearchConfig? = null) : this(Bundle().apply {
putLong(SOURCE_ID_KEY, source.id) putLong(SOURCE_ID_KEY, source.id)
if(searchQuery != null) if(searchQuery != null)
putString(SEARCH_QUERY_KEY, searchQuery) putString(SEARCH_QUERY_KEY, searchQuery)
if (smartSearchConfig != null)
putParcelable(SMART_SEARCH_CONFIG_KEY, smartSearchConfig)
}) })
/** /**
@ -561,7 +566,9 @@ open class BrowseCatalogueController(bundle: Bundle) :
*/ */
override fun onItemClick(view: View, position: Int): Boolean { override fun onItemClick(view: View, position: Int): Boolean {
val item = adapter?.getItem(position) as? CatalogueItem ?: return false val item = adapter?.getItem(position) as? CatalogueItem ?: return false
router.pushController(MangaController(item.manga, true).withFadeTransaction()) router.pushController(MangaController(item.manga,
true,
args.getParcelable(SMART_SEARCH_CONFIG_KEY)).withFadeTransaction())
return false return false
} }
@ -628,6 +635,9 @@ open class BrowseCatalogueController(bundle: Bundle) :
protected companion object { protected companion object {
const val SOURCE_ID_KEY = "sourceId" const val SOURCE_ID_KEY = "sourceId"
const val SEARCH_QUERY_KEY = "searchQuery" const val SEARCH_QUERY_KEY = "searchQuery"
// EXH -->
const val SMART_SEARCH_CONFIG_KEY = "smartSearchConfig"
// EXH <--
} }
} }

View File

@ -25,6 +25,7 @@ import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.base.controller.RxController import eu.kanade.tachiyomi.ui.base.controller.RxController
import eu.kanade.tachiyomi.ui.base.controller.TabbedController import eu.kanade.tachiyomi.ui.base.controller.TabbedController
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersController import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersController
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersPresenter import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersPresenter
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoController import eu.kanade.tachiyomi.ui.manga.info.MangaInfoController
@ -39,9 +40,14 @@ import java.util.Date
class MangaController : RxController, TabbedController { class MangaController : RxController, TabbedController {
constructor(manga: Manga?, fromCatalogue: Boolean = false) : super(Bundle().apply { constructor(manga: Manga?,
fromCatalogue: Boolean = false,
smartSearchConfig: CatalogueController.SmartSearchConfig? = null,
update: Boolean = false) : super(Bundle().apply {
putLong(MANGA_EXTRA, manga?.id ?: 0) putLong(MANGA_EXTRA, manga?.id ?: 0)
putBoolean(FROM_CATALOGUE_EXTRA, fromCatalogue) putBoolean(FROM_CATALOGUE_EXTRA, fromCatalogue)
putParcelable(SMART_SEARCH_CONFIG_EXTRA, smartSearchConfig)
putBoolean(UPDATE_EXTRA, update)
}) { }) {
this.manga = manga this.manga = manga
if (manga != null) { if (manga != null) {
@ -79,6 +85,10 @@ class MangaController : RxController, TabbedController {
val update = args.getBoolean(UPDATE_EXTRA, false) val update = args.getBoolean(UPDATE_EXTRA, false)
// EXH -->
val smartSearchConfig: CatalogueController.SmartSearchConfig? = args.getParcelable(SMART_SEARCH_CONFIG_EXTRA)
// EXH <--
val lastUpdateRelay: BehaviorRelay<Date> = BehaviorRelay.create() val lastUpdateRelay: BehaviorRelay<Date> = BehaviorRelay.create()
val chapterCountRelay: BehaviorRelay<Float> = BehaviorRelay.create() val chapterCountRelay: BehaviorRelay<Float> = BehaviorRelay.create()
@ -197,6 +207,7 @@ class MangaController : RxController, TabbedController {
// EXH --> // EXH -->
const val UPDATE_EXTRA = "update" const val UPDATE_EXTRA = "update"
const val SMART_SEARCH_CONFIG_EXTRA = "smartSearchConfig"
// EXH <-- // EXH <--
const val FROM_CATALOGUE_EXTRA = "from_catalogue" const val FROM_CATALOGUE_EXTRA = "from_catalogue"
const val MANGA_EXTRA = "manga" const val MANGA_EXTRA = "manga"

View File

@ -22,6 +22,7 @@ import com.bumptech.glide.request.target.SimpleTarget
import com.bumptech.glide.request.transition.Transition import com.bumptech.glide.request.transition.Transition
import com.bumptech.glide.signature.ObjectKey import com.bumptech.glide.signature.ObjectKey
import com.elvishew.xlog.XLog import com.elvishew.xlog.XLog
import com.google.gson.Gson
import com.jakewharton.rxbinding.support.v4.widget.refreshes import com.jakewharton.rxbinding.support.v4.widget.refreshes
import com.jakewharton.rxbinding.view.clicks import com.jakewharton.rxbinding.view.clicks
import com.jakewharton.rxbinding.view.longClicks import com.jakewharton.rxbinding.view.longClicks
@ -33,8 +34,10 @@ import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
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.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
@ -43,21 +46,21 @@ import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.openInBrowser import eu.kanade.tachiyomi.util.*
import eu.kanade.tachiyomi.util.snack
import eu.kanade.tachiyomi.util.toast
import eu.kanade.tachiyomi.util.truncateCenter
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.NHENTAI_SOURCE_ID import exh.NHENTAI_SOURCE_ID
import exh.ui.webview.WebViewActivity import exh.ui.webview.WebViewActivity
import jp.wasabeef.glide.transformations.CropSquareTransformation import jp.wasabeef.glide.transformations.CropSquareTransformation
import jp.wasabeef.glide.transformations.MaskTransformation import jp.wasabeef.glide.transformations.MaskTransformation
import kotlinx.android.synthetic.main.manga_info_controller.* import kotlinx.android.synthetic.main.manga_info_controller.*
import kotlinx.coroutines.*
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.text.DateFormat import java.text.DateFormat
import java.text.DecimalFormat import java.text.DecimalFormat
import java.util.* import java.util.*
import kotlin.coroutines.CoroutineContext
/** /**
* Fragment that shows manga information. * Fragment that shows manga information.
@ -65,7 +68,7 @@ import java.util.*
* UI related actions should be called from here. * UI related actions should be called from here.
*/ */
class MangaInfoController : NucleusController<MangaInfoPresenter>(), class MangaInfoController : NucleusController<MangaInfoPresenter>(),
ChangeMangaCategoriesDialog.Listener { ChangeMangaCategoriesDialog.Listener, CoroutineScope {
/** /**
* Preferences helper. * Preferences helper.
@ -74,6 +77,14 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
// EXH --> // EXH -->
private var lastMangaThumbnail: String? = null private var lastMangaThumbnail: String? = null
private val smartSearchConfig get() = (parentController as MangaController).smartSearchConfig
override val coroutineContext: CoroutineContext = Job() + Dispatchers.Main
private val gson: Gson by injectLazy()
private val sourceManager: SourceManager by injectLazy()
// EXH <-- // EXH <--
init { init {
@ -83,7 +94,7 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
override fun createPresenter(): MangaInfoPresenter { override fun createPresenter(): MangaInfoPresenter {
val ctrl = parentController as MangaController val ctrl = parentController as MangaController
return MangaInfoPresenter(ctrl.manga!!, ctrl.source!!, return MangaInfoPresenter(ctrl.manga!!, ctrl.source!!, ctrl.smartSearchConfig,
ctrl.chapterCountRelay, ctrl.lastUpdateRelay, ctrl.mangaFavoriteRelay) ctrl.chapterCountRelay, ctrl.lastUpdateRelay, ctrl.mangaFavoriteRelay)
} }
@ -153,6 +164,34 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
copyToClipboard(view.context.getString(R.string.title), presenter.manga.title) copyToClipboard(view.context.getString(R.string.title), presenter.manga.title)
} }
// EXH -->
smartSearchConfig?.let { smartSearchConfig ->
smartsearch_buttons.visible()
smartsearch_merge_btn.clicks().subscribeUntilDestroy {
// Init presenter here to avoid threading issues
presenter
launch {
try {
val mergedManga = withContext(Dispatchers.IO + NonCancellable) {
presenter.smartSearchMerge(presenter.manga, smartSearchConfig.origMangaId)
}
parentController?.router?.pushController(MangaController(mergedManga,
true,
update = true).withFadeTransaction())
applicationContext?.toast("Manga merged!")
} catch(e: Exception) {
if(e is CancellationException) throw e
else {
applicationContext?.toast("Failed to merge manga: ${e.message}")
}
}
}
}
}
// EXH <--
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@ -176,7 +215,7 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
// EXH --> // EXH -->
private fun openSmartSearch() { private fun openSmartSearch() {
val smartSearchConfig = CatalogueController.SmartSearchConfig(presenter.manga.title) val smartSearchConfig = CatalogueController.SmartSearchConfig(presenter.manga.title, presenter.manga.id!!)
parentController?.router?.pushController(CatalogueController(Bundle().apply { parentController?.router?.pushController(CatalogueController(Bundle().apply {
putParcelable(CatalogueController.SMART_SEARCH_CONFIG, smartSearchConfig) putParcelable(CatalogueController.SMART_SEARCH_CONFIG, smartSearchConfig)
@ -237,10 +276,24 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
// If manga source is known update source TextView. // If manga source is known update source TextView.
manga_source.text = if (source == null) { manga_source.text = if (source == null) {
view.context.getString(R.string.unknown) view.context.getString(R.string.unknown)
// EXH -->
} else if(source.id == MERGED_SOURCE_ID) {
MergedSource.MangaConfig.readFromUrl(gson, manga.url).children.map {
sourceManager.getOrStub(it.source).toString()
}.distinct().joinToString()
// EXH <--
} else { } else {
source.toString() source.toString()
} }
// EXH -->
if(source?.id == MERGED_SOURCE_ID) {
manga_source_label.text = "Sources"
} else {
manga_source_label.setText(R.string.manga_info_source_label)
}
// EXH <--
// Update genres list // Update genres list
if (manga.genre.isNullOrBlank().not()) { if (manga.genre.isNullOrBlank().not()) {
manga_genres_tags.setTags(manga.genre?.split(", ")) manga_genres_tags.setTags(manga.genre?.split(", "))
@ -295,6 +348,13 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
super.onDestroyView(view) super.onDestroyView(view)
} }
// EXH -->
override fun onDestroy() {
super.onDestroy()
cancel()
}
// EXH <--
/** /**
* Update chapter count TextView. * Update chapter count TextView.
* *

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.manga.info package eu.kanade.tachiyomi.ui.manga.info
import android.os.Bundle import android.os.Bundle
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
@ -10,8 +11,14 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
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.catalogue.CatalogueController
import eu.kanade.tachiyomi.util.isNullOrUnsubscribed import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
import exh.MERGED_SOURCE_ID
import exh.util.await
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.withContext
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
@ -28,12 +35,14 @@ import java.util.*
class MangaInfoPresenter( class MangaInfoPresenter(
val manga: Manga, val manga: Manga,
val source: Source, val source: Source,
val smartSearchConfig: CatalogueController.SmartSearchConfig?,
private val chapterCountRelay: BehaviorRelay<Float>, private val chapterCountRelay: BehaviorRelay<Float>,
private val lastUpdateRelay: BehaviorRelay<Date>, private val lastUpdateRelay: BehaviorRelay<Date>,
private val mangaFavoriteRelay: PublishRelay<Boolean>, private val mangaFavoriteRelay: PublishRelay<Boolean>,
private val db: DatabaseHelper = Injekt.get(), private val db: DatabaseHelper = 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(),
private val gson: Gson = Injekt.get()
) : BasePresenter<MangaInfoController>() { ) : BasePresenter<MangaInfoController>() {
/** /**
@ -170,4 +179,59 @@ class MangaInfoPresenter(
moveMangaToCategories(manga, listOfNotNull(category)) moveMangaToCategories(manga, listOfNotNull(category))
} }
suspend fun smartSearchMerge(manga: Manga, originalMangaId: Long): Manga {
val originalManga = db.getManga(originalMangaId).await()
?: throw IllegalArgumentException("Unknown manga ID: $originalMangaId")
val toInsert = if(originalManga.source == MERGED_SOURCE_ID) {
originalManga.apply {
val originalChildren = MergedSource.MangaConfig.readFromUrl(gson, url).children
if(originalChildren.any { it.source == manga.source && it.url == manga.url })
throw IllegalArgumentException("This manga is already merged with the current manga!")
url = MergedSource.MangaConfig(originalChildren + MergedSource.MangaSource(
manga.source,
manga.url
)).writeAsUrl(gson)
}
} else {
val newMangaConfig = MergedSource.MangaConfig(listOf(
MergedSource.MangaSource(
originalManga.source,
originalManga.url
),
MergedSource.MangaSource(
manga.source,
manga.url
)
))
Manga.create(newMangaConfig.writeAsUrl(gson), originalManga.title, MERGED_SOURCE_ID).apply {
copyFrom(originalManga)
favorite = true
last_update = originalManga.last_update
viewer = originalManga.viewer
chapter_flags = originalManga.chapter_flags
sorting = Manga.SORTING_NUMBER
}
}
// Note that if the manga are merged in a different order, this won't trigger, but I don't care lol
val existingManga = db.getManga(toInsert.url, toInsert.source).await()
if(existingManga != null) {
withContext(NonCancellable) {
if(toInsert.id != null) {
db.deleteManga(toInsert).await()
}
}
return existingManga
}
// Reload chapters immediately
toInsert.initialized = false
val newId = db.insertManga(toInsert).await().insertedId()
if(newId != null) toInsert.id = newId
return toInsert
}
} }

View File

@ -18,6 +18,7 @@ const val NHENTAI_SOURCE_ID = LEWD_SOURCE_SERIES + 7
val HENTAI_CAFE_SOURCE_ID = delegatedSourceId<HentaiCafe>() val HENTAI_CAFE_SOURCE_ID = delegatedSourceId<HentaiCafe>()
const val TSUMINO_SOURCE_ID = LEWD_SOURCE_SERIES + 9 const val TSUMINO_SOURCE_ID = LEWD_SOURCE_SERIES + 9
const val HITOMI_SOURCE_ID = LEWD_SOURCE_SERIES + 10 const val HITOMI_SOURCE_ID = LEWD_SOURCE_SERIES + 10
const val MERGED_SOURCE_ID = LEWD_SOURCE_SERIES + 69
private val DELEGATED_LEWD_SOURCES = listOf( private val DELEGATED_LEWD_SOURCES = listOf(
HentaiCafe::class HentaiCafe::class

View File

@ -50,7 +50,7 @@ class SmartSearchController(bundle: Bundle? = null) : NucleusController<SmartSea
for(event in presenter.smartSearchChannel) { for(event in presenter.smartSearchChannel) {
withContext(NonCancellable) { withContext(NonCancellable) {
if (event is SmartSearchPresenter.SearchResults.Found) { if (event is SmartSearchPresenter.SearchResults.Found) {
val transaction = MangaController(event.manga, true).withFadeTransaction() val transaction = MangaController(event.manga, true, smartSearchConfig).withFadeTransaction()
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
router.replaceTopController(transaction) router.replaceTopController(transaction)
} }
@ -61,7 +61,7 @@ class SmartSearchController(bundle: Bundle? = null) : NucleusController<SmartSea
applicationContext?.toast("Error performing automatic search!") applicationContext?.toast("Error performing automatic search!")
} }
val transaction = BrowseCatalogueController(source, smartSearchConfig.title).withFadeTransaction() val transaction = BrowseCatalogueController(source, smartSearchConfig.origTitle, smartSearchConfig).withFadeTransaction()
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
router.replaceTopController(transaction) router.replaceTopController(transaction)
} }

View File

@ -54,7 +54,7 @@ class SmartSearchPresenter(private val source: CatalogueSource?, private val con
} }
private suspend fun smartSearch(source: CatalogueSource, config: CatalogueController.SmartSearchConfig): SManga? { private suspend fun smartSearch(source: CatalogueSource, config: CatalogueController.SmartSearchConfig): SManga? {
val cleanedTitle = cleanSmartSearchTitle(config.title) val cleanedTitle = cleanSmartSearchTitle(config.origTitle)
val queries = getSmartSearchQueries(cleanedTitle) val queries = getSmartSearchQueries(cleanedTitle)

View File

@ -254,6 +254,35 @@
app:atg_textColor="@color/md_blue_A400" app:atg_textColor="@color/md_blue_A400"
android:layout_marginRight="64dp"/> android:layout_marginRight="64dp"/>
<LinearLayout
android:id="@+id/smartsearch_buttons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@+id/manga_genres_tags"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
tools:visibility="visible">
<Button
android:id="@+id/smartsearch_merge_btn"
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Merge with current" />
<Button
android:id="@+id/smartsearch_replace_btn"
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Replace current" />
</LinearLayout>
</android.support.constraint.ConstraintLayout> </android.support.constraint.ConstraintLayout>

View File

@ -9,11 +9,8 @@
android:layout_height="match_parent"> android:layout_height="match_parent">
<android.support.constraint.ConstraintLayout <android.support.constraint.ConstraintLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
xmlns:android="http://schemas.android.com/apk/res/android">
<View <View
android:id="@+id/guideline" android:id="@+id/guideline"
@ -230,12 +227,10 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textIsSelectable="false" android:textIsSelectable="false"
app:layout_constraintBaseline_toBaselineOf="@+id/manga_source_label"
app:layout_constraintLeft_toRightOf="@+id/manga_source_label" app:layout_constraintLeft_toRightOf="@+id/manga_source_label"
app:layout_constraintRight_toRightOf="parent"/> app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/manga_status_label" />
</android.support.constraint.ConstraintLayout> </android.support.constraint.ConstraintLayout>
@ -287,7 +282,34 @@
app:atg_borderStrokeWidth="1dp" app:atg_borderStrokeWidth="1dp"
app:atg_backgroundColor="@android:color/transparent" app:atg_backgroundColor="@android:color/transparent"
app:atg_borderColor="@color/md_blue_A400" app:atg_borderColor="@color/md_blue_A400"
app:atg_textColor="@color/md_blue_A400" /> app:atg_textColor="@color/md_blue_A400" >
</me.gujun.android.taggroup.TagGroup>
<LinearLayout
android:id="@+id/smartsearch_buttons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone"
tools:visibility="visible">
<Button
android:id="@+id/smartsearch_merge_btn"
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Merge with current" />
<Button
android:id="@+id/smartsearch_replace_btn"
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Replace current" />
</LinearLayout>
</LinearLayout> </LinearLayout>