diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f44415d819..4407a8ccfb 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -55,8 +55,35 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt
index 873769172e..84ecb3ffa5 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt
@@ -65,6 +65,15 @@ interface ChapterQueries : DbProvider {
.build())
.prepare()
+ fun getChapters(url: String) = db.get()
+ .listOfObjects(Chapter::class.java)
+ .withQuery(Query.builder()
+ .table(ChapterTable.TABLE)
+ .where("${ChapterTable.COL_URL} = ?")
+ .whereArgs(url)
+ .build())
+ .prepare()
+
fun getChapter(url: String, mangaId: Long) = db.get()
.`object`(Chapter::class.java)
.withQuery(Query.builder()
diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt b/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt
index 83c719428b..8ba52becb6 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt
@@ -5,7 +5,12 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
+import eu.kanade.tachiyomi.source.online.DelegatedHttpSource
import eu.kanade.tachiyomi.source.online.HttpSource
+import eu.kanade.tachiyomi.source.online.all.MangaDex
+import eu.kanade.tachiyomi.source.online.english.FoolSlide
+import eu.kanade.tachiyomi.source.online.english.KireiCake
+import eu.kanade.tachiyomi.source.online.english.MangaPlus
import rx.Observable
open class SourceManager(private val context: Context) {
@@ -14,6 +19,18 @@ open class SourceManager(private val context: Context) {
private val stubSourcesMap = mutableMapOf()
+ private val delegatedSources = listOf(
+ DelegatedSource(
+ "reader.kireicake.com", 5509224355268673176, KireiCake()
+ ), DelegatedSource(
+ "jaiminisbox.com", 9064882169246918586, FoolSlide("jaiminis", "/reader")
+ ), DelegatedSource(
+ "mangadex.org", 2499283573021220255, MangaDex()
+ ), DelegatedSource(
+ "mangaplus.shueisha.co.jp", 1998944621602463790, MangaPlus()
+ )
+ ).associateBy { it.sourceId }
+
init {
createInternalSources().forEach { registerSource(it) }
}
@@ -28,12 +45,17 @@ open class SourceManager(private val context: Context) {
}
}
+ fun getDelegatedSource(urlName: String): DelegatedHttpSource? {
+ return delegatedSources.values.find { it.urlName == urlName }?.delegatedHttpSource
+ }
+
fun getOnlineSources() = sourcesMap.values.filterIsInstance()
fun getCatalogueSources() = sourcesMap.values.filterIsInstance()
internal fun registerSource(source: Source, overwrite: Boolean = false) {
if (overwrite || !sourcesMap.containsKey(source.id)) {
+ delegatedSources[source.id]?.delegatedHttpSource?.delegate = source as? HttpSource
sourcesMap[source.id] = source
}
}
@@ -43,7 +65,7 @@ open class SourceManager(private val context: Context) {
}
private fun createInternalSources(): List = listOf(
- LocalSource(context)
+ LocalSource(context)
)
private inner class StubSource(override val id: Long) : Source {
@@ -68,14 +90,23 @@ open class SourceManager(private val context: Context) {
}
private fun getSourceNotInstalledException(): Exception {
- return SourceNotFoundException(context.getString(R.string.source_not_installed_, id
- .toString()), id)
+ return SourceNotFoundException(
+ context.getString(
+ R.string.source_not_installed_, id.toString()
+ ), id
+ )
}
override fun hashCode(): Int {
return id.hashCode()
}
}
+
+ private data class DelegatedSource(
+ val urlName: String,
+ val sourceId: Long,
+ val delegatedHttpSource: DelegatedHttpSource
+ )
}
class SourceNotFoundException(message: String, val id: Long) : Exception(message)
diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapter.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapter.kt
index f53bbe8f0a..756b8f2c84 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapter.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapter.kt
@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.source.model
+import eu.kanade.tachiyomi.data.database.models.ChapterImpl
import java.io.Serializable
interface SChapter : Serializable {
@@ -22,6 +23,16 @@ interface SChapter : Serializable {
scanlator = other.scanlator
}
+ fun toChapter(): ChapterImpl {
+ return ChapterImpl().apply {
+ name = this@SChapter.name
+ url = this@SChapter.url
+ date_upload = this@SChapter.date_upload
+ chapter_number = this@SChapter.chapter_number
+ scanlator = this@SChapter.scanlator
+ }
+ }
+
companion object {
fun create(): SChapter {
return SChapterImpl()
diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/DelegatedHttpSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/DelegatedHttpSource.kt
new file mode 100644
index 0000000000..936d5eaa4c
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/DelegatedHttpSource.kt
@@ -0,0 +1,45 @@
+package eu.kanade.tachiyomi.source.online
+
+import android.net.Uri
+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.MangaImpl
+import eu.kanade.tachiyomi.network.NetworkHelper
+import eu.kanade.tachiyomi.source.fetchChapterListAsync
+import eu.kanade.tachiyomi.source.model.SChapter
+import uy.kohesive.injekt.injectLazy
+
+abstract class DelegatedHttpSource {
+
+ var delegate: HttpSource? = null
+ abstract val domainName: String
+
+ protected val db: DatabaseHelper by injectLazy()
+
+ protected val network: NetworkHelper by injectLazy()
+
+ abstract fun canOpenUrl(uri: Uri): Boolean
+ abstract fun chapterUrl(uri: Uri): String?
+ open fun pageNumber(uri: Uri): Int? = uri.pathSegments.lastOrNull()?.toIntOrNull()
+ abstract suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple>?
+
+ protected open fun getMangaInfo(url: String): Manga? {
+ val id = delegate?.id ?: return null
+ val manga = Manga.create(url, "", id)
+ val networkManga = delegate?.fetchMangaDetails(manga)?.toBlocking()?.single() ?: return null
+ val newManga = MangaImpl().apply {
+ this.url = url
+ title = try { networkManga.title } catch (e: Exception) { "" }
+ source = id
+ }
+ newManga.copyFrom(networkManga)
+ return newManga
+ }
+
+ suspend fun getChapters(url: String): List? {
+ val id = delegate?.id ?: return null
+ val manga = Manga.create(url, "", id)
+ return delegate?.fetchChapterListAsync(manga)
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt
new file mode 100644
index 0000000000..3185549fa2
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt
@@ -0,0 +1,105 @@
+package eu.kanade.tachiyomi.source.online.all
+
+import android.net.Uri
+import com.github.salomonbrys.kotson.nullInt
+import com.github.salomonbrys.kotson.nullString
+import com.github.salomonbrys.kotson.obj
+import com.google.gson.JsonParser
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Chapter
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.await
+import eu.kanade.tachiyomi.source.SourceManager
+import eu.kanade.tachiyomi.source.model.SChapter
+import eu.kanade.tachiyomi.source.online.DelegatedHttpSource
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.withContext
+import okhttp3.CacheControl
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import uy.kohesive.injekt.injectLazy
+
+class MangaDex : DelegatedHttpSource() {
+
+ override val domainName: String = "mangadex"
+
+ val sourceManager: SourceManager by injectLazy()
+
+ override fun canOpenUrl(uri: Uri): Boolean {
+ return uri.pathSegments?.lastOrNull() != "comments"
+ }
+
+ override fun chapterUrl(uri: Uri): String? {
+ val chapterNumber = uri.pathSegments.getOrNull(1) ?: return null
+ return "/api/chapter/$chapterNumber"
+ }
+
+ override fun pageNumber(uri: Uri): Int? {
+ return uri.pathSegments.getOrNull(2)?.toIntOrNull()
+ }
+
+ override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple>? {
+ val url = chapterUrl(uri) ?: return null
+ val request =
+ GET("https://mangadex.org$url", delegate!!.headers, CacheControl.FORCE_NETWORK)
+ val response = network.client.newCall(request).await()
+ if (response.code != 200) throw Exception("HTTP error ${response.code}")
+ val body = response.body?.string().orEmpty()
+ if (body.isEmpty()) {
+ throw Exception("Null Response")
+ }
+
+ val jsonObject = JsonParser.parseString(body).obj
+ val mangaId = jsonObject["manga_id"]?.nullInt ?: throw Exception(
+ "No manga associated with chapter"
+ )
+ val langCode = getRealLangCode(jsonObject["lang_code"]?.nullString ?: "en").toUpperCase()
+ // Use the correct MangaDex source based on the language code, or the api will not return
+ // the correct chapter list
+ delegate = sourceManager.getOnlineSources().find { it.toString() == "MangaDex ($langCode)" }
+ ?: return error("Source not found")
+ val mangaUrl = "/manga/$mangaId/"
+ return withContext(Dispatchers.IO) {
+ val deferredManga = async {
+ db.getManga(mangaUrl, delegate?.id!!).executeAsBlocking() ?: getMangaInfo(mangaUrl)
+ }
+ val deferredChapters = async { getChapters(mangaUrl) }
+ val manga = deferredManga.await()
+ val chapters = deferredChapters.await()
+ val context = Injekt.get().context
+ val trueChapter = chapters?.find { it.url == url }?.toChapter() ?: error(
+ context.getString(R.string.chapter_not_found)
+ )
+ if (manga != null) {
+ Triple(trueChapter, manga, chapters.orEmpty())
+ } else null
+ }
+ }
+
+ fun getRealLangCode(langCode: String): String {
+ return when (langCode.toLowerCase()) {
+ "gb" -> "en"
+ "vn" -> "vi"
+ "mx" -> "es-419"
+ "br" -> "pt-BR"
+ "ph" -> "fil"
+ "sa" -> "ar"
+ "bd" -> "bn"
+ "mm" -> "my"
+ "cz" -> "cs"
+ "dk" -> "da"
+ "gr" -> "el"
+ "jp" -> "ja"
+ "kr" -> "ko"
+ "my" -> "ms"
+ "ir" -> "fa"
+ "rs" -> "sh"
+ "ua" -> "uk"
+ "cn" -> "zh-Hans" "hk" -> "zh-Hant"
+ else -> langCode
+ }
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/FoolSlide.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/FoolSlide.kt
new file mode 100644
index 0000000000..9edbaac03f
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/FoolSlide.kt
@@ -0,0 +1,102 @@
+package eu.kanade.tachiyomi.source.online.english
+
+import android.net.Uri
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Chapter
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.database.models.MangaImpl
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.POST
+import eu.kanade.tachiyomi.network.await
+import eu.kanade.tachiyomi.source.model.SChapter
+import eu.kanade.tachiyomi.source.online.DelegatedHttpSource
+import eu.kanade.tachiyomi.util.asJsoup
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.withContext
+import okhttp3.FormBody
+import okhttp3.Request
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+open class FoolSlide(override val domainName: String, private val urlModifier: String = "") :
+DelegatedHttpSource
+ () {
+
+ override fun canOpenUrl(uri: Uri): Boolean = true
+
+ override fun chapterUrl(uri: Uri): String? {
+ val offset = if (urlModifier.isEmpty()) 0 else 1
+ val mangaName = uri.pathSegments.getOrNull(1 + offset) ?: return null
+ val lang = uri.pathSegments.getOrNull(2 + offset) ?: return null
+ val volume = uri.pathSegments.getOrNull(3 + offset) ?: return null
+ val chapterNumber = uri.pathSegments.getOrNull(4 + offset) ?: return null
+ val subChapterNumber = uri.pathSegments.getOrNull(5 + offset)?.toIntOrNull()?.toString()
+ return "$urlModifier/read/" + listOfNotNull(
+ mangaName, lang, volume, chapterNumber, subChapterNumber
+ ).joinToString("/") + "/"
+ }
+
+ override fun pageNumber(uri: Uri): Int? {
+ val count = uri.pathSegments.count()
+ if (count > 2 && uri.pathSegments[count - 2] == "page") {
+ return super.pageNumber(uri)
+ }
+ return null
+ }
+
+ override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple>? {
+ val offset = if (urlModifier.isEmpty()) 0 else 1
+ val mangaName = uri.pathSegments.getOrNull(1 + offset) ?: return null
+ var chapterNumber = uri.pathSegments.getOrNull(4 + offset) ?: return null
+ val subChapterNumber = uri.pathSegments.getOrNull(5 + offset)?.toIntOrNull()
+ if (subChapterNumber != null) {
+ chapterNumber += ".$subChapterNumber"
+ }
+ return withContext(Dispatchers.IO) {
+ val mangaUrl = "$urlModifier/series/$mangaName/"
+ val sourceId = delegate?.id ?: return@withContext null
+ val dbManga = db.getManga(mangaUrl, sourceId).executeAsBlocking()
+ val deferredManga = async {
+ dbManga ?: getManga(mangaUrl)
+ }
+ val chapterUrl = chapterUrl(uri)
+ val deferredChapters = async { getChapters(mangaUrl) }
+ val manga = deferredManga.await()
+ val chapters = deferredChapters.await()
+ val context = Injekt.get().context
+ val trueChapter = chapters?.find { it.url == chapterUrl }?.toChapter() ?: error(
+ context.getString(R.string.chapter_not_found)
+ )
+ if (manga != null) Triple(trueChapter, manga, chapters) else null
+ }
+ }
+
+ open suspend fun getManga(url: String): Manga? {
+ val request = GET("${delegate!!.baseUrl}$url")
+ val document = network.client.newCall(allowAdult(request)).await().asJsoup()
+ val mangaDetailsInfoSelector = "div.info"
+ val infoElement = document.select(mangaDetailsInfoSelector).first().text()
+ return MangaImpl().apply {
+ this.url = url
+ source = delegate?.id ?: -1
+ title = infoElement.substringAfter("Title:").substringBefore("Author:").trim()
+ author = infoElement.substringAfter("Author:").substringBefore("Artist:").trim()
+ artist = infoElement.substringAfter("Artist:").substringBefore("Synopsis:").trim()
+ description = infoElement.substringAfter("Synopsis:").trim()
+ thumbnail_url = document.select("div.thumbnail img").firstOrNull()?.attr("abs:src")?.trim()
+ }
+ }
+
+ /**
+ * Transform a GET request into a POST request that automatically authorizes all adult content
+ */
+ private fun allowAdult(request: Request) = allowAdult(request.url.toString())
+
+ private fun allowAdult(url: String): Request {
+ return POST(url, body = FormBody.Builder()
+ .add("adult", "true")
+ .build())
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/KireiCake.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/KireiCake.kt
new file mode 100644
index 0000000000..31d6f1a3fe
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/KireiCake.kt
@@ -0,0 +1,29 @@
+package eu.kanade.tachiyomi.source.online.english
+
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.database.models.MangaImpl
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.await
+import eu.kanade.tachiyomi.util.asJsoup
+import eu.kanade.tachiyomi.util.lang.capitalizeWords
+
+class KireiCake : FoolSlide("kireicake") {
+
+ override suspend fun getManga(url: String): Manga? {
+ val request = GET("${delegate!!.baseUrl}$url")
+ val document = network.client.newCall(request).await().asJsoup()
+ val mangaDetailsInfoSelector = "div.info"
+ return MangaImpl().apply {
+ this.url = url
+ source = delegate?.id ?: -1
+ title = document.select("$mangaDetailsInfoSelector li:has(b:contains(title))").first()
+ ?.ownText()?.substringAfter(":")?.trim() ?: url.split("/").last().replace(
+ "_", " " + ""
+ ).capitalizeWords()
+ description =
+ document.select("$mangaDetailsInfoSelector li:has(b:contains(description))").first()
+ ?.ownText()?.substringAfter(":")
+ thumbnail_url = document.select("div.thumbnail img").firstOrNull()?.attr("abs:src")
+ }
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/MangaPlus.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/MangaPlus.kt
new file mode 100644
index 0000000000..24877d08be
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/MangaPlus.kt
@@ -0,0 +1,70 @@
+package eu.kanade.tachiyomi.source.online.english
+
+import android.net.Uri
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Chapter
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.await
+import eu.kanade.tachiyomi.source.model.SChapter
+import eu.kanade.tachiyomi.source.online.DelegatedHttpSource
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.withContext
+import okhttp3.CacheControl
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+class MangaPlus : DelegatedHttpSource() {
+ override val domainName: String = "jumpg-webapi.tokyo-cdn"
+
+ private val titleIdRegex =
+ Regex("https:\\/\\/mangaplus\\.shueisha\\.co\\.jp\\/drm\\/title\\/\\d*")
+ private val titleRegex = Regex("#MANGA_Plus .*\u0012")
+
+ private val chapterUrlTemplate =
+ "https://jumpg-webapi.tokyo-cdn.com/api/manga_viewer?chapter_id=##&split=no&img_quality=low"
+
+ override fun canOpenUrl(uri: Uri): Boolean = true
+
+ override fun chapterUrl(uri: Uri): String? = "#/viewer/${uri.pathSegments[1]}"
+
+ override fun pageNumber(uri: Uri): Int? = null
+
+ override suspend fun fetchMangaFromChapterUrl(uri: Uri): Triple>? {
+ val url = chapterUrl(uri) ?: return null
+ val request = GET(
+ chapterUrlTemplate.replace("##", uri.pathSegments[1]),
+ delegate!!.headers,
+ CacheControl.FORCE_NETWORK
+ )
+ return withContext(Dispatchers.IO) {
+ val response = network.client.newCall(request).await()
+ if (response.code != 200) throw Exception("HTTP error ${response.code}")
+ val body = response.body!!.string()
+ val match = titleIdRegex.find(body)
+ val titleId = match?.groupValues?.firstOrNull()?.substringAfterLast("/")
+ ?: error("Title not found")
+ val title = titleRegex.find(body)?.groups?.firstOrNull()?.value?.substringAfter("Plus ")
+ ?: error("Title not found")
+ val trimmedTitle = title.substring(0, title.length - 1)
+ val mangaUrl = "#/titles/$titleId"
+ val deferredManga = async {
+ db.getManga(mangaUrl, delegate?.id!!).executeAsBlocking() ?: getMangaInfo(mangaUrl)
+ }
+ val deferredChapters = async { getChapters(mangaUrl) }
+ val manga = deferredManga.await()
+ val chapters = deferredChapters.await()
+ val context = Injekt.get().context
+ val trueChapter = chapters?.find { it.url == url }?.toChapter() ?: error(
+ context.getString(R.string.chapter_not_found)
+ )
+ if (manga != null) {
+ Triple(trueChapter, manga.apply {
+ this.title = trimmedTitle
+ }, chapters.orEmpty())
+ } else null
+ }
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
index d3d1858ef4..4d7e66ff15 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
@@ -35,6 +35,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.base.MaterialMenuSheet
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
+import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.main.SearchActivity
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.AddToLibraryFirst
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.Error
@@ -57,6 +58,7 @@ import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.hasSideNavBar
import eu.kanade.tachiyomi.util.system.isBottomTappable
import eu.kanade.tachiyomi.util.system.launchUI
+import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.collapse
import eu.kanade.tachiyomi.util.view.doOnApplyWindowInsets
@@ -71,12 +73,15 @@ import eu.kanade.tachiyomi.widget.SimpleAnimationListener
import eu.kanade.tachiyomi.widget.SimpleSeekBarListener
import kotlinx.android.synthetic.main.reader_activity.*
import kotlinx.android.synthetic.main.reader_chapters_sheet.*
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.sample
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import me.zhanghai.android.systemuihelper.SystemUiHelper
import nucleus.factory.RequiresPresenter
import timber.log.Timber
@@ -125,6 +130,8 @@ class ReaderActivity : BaseRxActivity(),
private var coroutine: Job? = null
+ var fromUrl = false
+
/**
* System UI helper to hide status & navigation bar on all different API levels.
*/
@@ -152,6 +159,8 @@ class ReaderActivity : BaseRxActivity(),
private var snackbar: Snackbar? = null
+ var intentPageNumber: Int? = null
+
companion object {
@Suppress("unused")
const val LEFT_TO_RIGHT = 1
@@ -192,13 +201,18 @@ class ReaderActivity : BaseRxActivity(),
}
if (presenter.needsInit()) {
- val manga = intent.extras!!.getLong("manga", -1)
- val chapter = intent.extras!!.getLong("chapter", -1)
- if (manga == -1L || chapter == -1L) {
- finish()
- return
+ fromUrl = handleIntentAction(intent)
+ if (!fromUrl) {
+ val manga = intent.extras!!.getLong("manga", -1)
+ val chapter = intent.extras!!.getLong("chapter", -1)
+ if (manga == -1L || chapter == -1L) {
+ finish()
+ return
+ }
+ presenter.init(manga, chapter)
+ } else {
+ please_wait.visible()
}
- presenter.init(manga, chapter)
}
if (savedInstanceState != null) {
@@ -282,6 +296,19 @@ class ReaderActivity : BaseRxActivity(),
return true
}
+ private fun popToMain() {
+ presenter.onBackPressed()
+ if (fromUrl) {
+ val intent = Intent(this, MainActivity::class.java).apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ }
+ startActivity(intent)
+ finishAfterTransition()
+ } else {
+ finish()
+ }
+ }
+
/**
* Called when the user clicks the back key or the button on the toolbar. The call is
* delegated to the presenter.
@@ -292,7 +319,7 @@ class ReaderActivity : BaseRxActivity(),
return
}
presenter.onBackPressed()
- super.onBackPressed()
+ finish()
}
/**
@@ -325,7 +352,7 @@ class ReaderActivity : BaseRxActivity(),
window.statusBarColor = Color.TRANSPARENT
supportActionBar?.setDisplayHomeAsUpEnabled(true)
toolbar.setNavigationOnClickListener {
- onBackPressed()
+ popToMain()
}
toolbar.setOnClickListener {
@@ -505,6 +532,8 @@ class ReaderActivity : BaseRxActivity(),
fun setChapters(viewerChapters: ViewerChapters) {
please_wait.gone()
viewer?.setChapters(viewerChapters)
+ intentPageNumber?.let { moveToPageIndex(it) }
+ intentPageNumber = null
toolbar.subtitle = viewerChapters.currChapter.chapter.name
}
@@ -762,6 +791,27 @@ class ReaderActivity : BaseRxActivity(),
}
}
+ private fun handleIntentAction(intent: Intent): Boolean {
+ val uri = intent.data ?: return false
+ if (!presenter.canLoadUrl(uri)) {
+ openInBrowser(intent.data!!.toString(), true)
+ finishAfterTransition()
+ return true
+ }
+ setMenuVisibility(visible = false, animate = true)
+ scope.launch(Dispatchers.IO) {
+ try {
+ intentPageNumber = presenter.intentPageNumber(uri)
+ presenter.loadChapterURL(uri)
+ } catch (e: Exception) {
+ withContext(Dispatchers.Main) {
+ setInitialChapterError(e)
+ }
+ }
+ }
+ return true
+ }
+
fun openMangaInBrowser() {
val source = presenter.getSource() ?: return
val url = try {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt
index afc61aea30..9c9880e199 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt
@@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.reader
import android.app.Application
+import android.net.Uri
import android.os.Bundle
import android.os.Environment
import com.jakewharton.rxrelay.BehaviorRelay
@@ -27,8 +28,10 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
import eu.kanade.tachiyomi.util.chapter.ChapterFilter
+import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.system.ImageUtil
+import eu.kanade.tachiyomi.util.system.executeOnIO
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@@ -288,6 +291,68 @@ class ReaderPresenter(
}
}
+ fun canLoadUrl(uri: Uri): Boolean {
+ val host = uri.host ?: return false
+ val delegatedSource = sourceManager.getDelegatedSource(host) ?: return false
+ return delegatedSource.canOpenUrl(uri)
+ }
+
+ fun intentPageNumber(url: Uri): Int? {
+ val host = url.host ?: return null
+ val delegatedSource = sourceManager.getDelegatedSource(host) ?: error(
+ preferences.context.getString(R.string.source_not_installed)
+ )
+ return delegatedSource.pageNumber(url)?.minus(1)
+ }
+
+ suspend fun loadChapterURL(url: Uri) {
+ val host = url.host ?: return
+ val delegatedSource = sourceManager.getDelegatedSource(host) ?: error(
+ preferences.context.getString(R.string.source_not_installed)
+ )
+ val chapterUrl = delegatedSource.chapterUrl(url)
+ val sourceId = delegatedSource.delegate?.id ?: error(
+ preferences.context.getString(R.string.source_not_installed)
+ )
+ if (chapterUrl != null) {
+ val dbChapter = db.getChapters(chapterUrl).executeOnIO().find {
+ val source = db.getManga(it.manga_id!!).executeOnIO()?.source ?: return@find false
+ if (source == sourceId) {
+ true
+ } else {
+ val httpSource = sourceManager.getOrStub(source) as? HttpSource
+ val host = delegatedSource.domainName
+ httpSource?.baseUrl?.contains(host) == true
+ }
+ }
+ if (dbChapter?.manga_id != null) {
+ val dbManga = db.getManga(dbChapter.manga_id!!).executeOnIO()
+ if (dbManga != null) {
+ withContext(Dispatchers.Main) {
+ init(dbManga, dbChapter.id!!)
+ }
+ return
+ }
+ }
+ }
+ val info = delegatedSource.fetchMangaFromChapterUrl(url)
+ if (info != null) {
+ val (chapter, manga, chapters) = info
+ val id = db.insertManga(manga).executeOnIO().insertedId()
+ manga.id = id ?: manga.id
+ chapter.manga_id = manga.id
+ val chapterId = db.insertChapter(chapter).executeOnIO().insertedId() ?: return
+ if (chapters.isNotEmpty()) {
+ syncChaptersWithSource(
+ db, chapters, manga, delegatedSource.delegate!!
+ )
+ }
+ withContext(Dispatchers.Main) {
+ init(manga, chapterId)
+ }
+ } else error(preferences.context.getString(R.string.unknown_error))
+ }
+
/**
* Called when the user changed to the given [chapter] when changing pages from the viewer.
* It's used only to set this chapter as active.
diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt
index cbd2b5f760..5b983ffe96 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt
@@ -8,12 +8,12 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
import android.content.res.Configuration
import android.content.res.Resources
import android.graphics.drawable.Drawable
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
-import android.net.Uri
import android.os.Build
import android.os.PowerManager
import android.view.View
@@ -23,6 +23,7 @@ import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.browser.customtabs.CustomTabsIntent
+import androidx.browser.customtabs.CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
@@ -222,18 +223,47 @@ fun Context.isServiceRunning(serviceClass: Class<*>): Boolean {
/**
* Opens a URL in a custom tab.
*/
-fun Context.openInBrowser(url: String) {
+fun Context.openInBrowser(url: String, forceBrowser: Boolean = false): Boolean {
try {
val parsedUrl = url.toUri()
val intent = CustomTabsIntent.Builder()
- .setToolbarColor(getResourceColor(R.attr.colorPrimaryVariant))
- .build()
+ .setToolbarColor(getResourceColor(R.attr.colorPrimaryVariant))
+ .build()
+ if (forceBrowser) {
+ val packages = getCustomTabsPackages().maxBy { it.preferredOrder }
+ val processName = packages?.activityInfo?.processName ?: return false
+ intent.intent.`package` = processName
+ }
intent.launchUrl(this, parsedUrl)
+ return true
} catch (e: Exception) {
toast(e.message)
+ return false
}
}
+/**
+ * Returns a list of packages that support Custom Tabs.
+ */
+fun Context.getCustomTabsPackages(): ArrayList {
+ val pm = packageManager
+ // Get default VIEW intent handler.
+ val activityIntent = Intent(Intent.ACTION_VIEW, "http://www.example.com".toUri())
+ // Get all apps that can handle VIEW intents.
+ val resolvedActivityList = pm.queryIntentActivities(activityIntent, 0)
+ val packagesSupportingCustomTabs = ArrayList()
+ for (info in resolvedActivityList) {
+ val serviceIntent = Intent()
+ serviceIntent.action = ACTION_CUSTOM_TABS_CONNECTION
+ serviceIntent.setPackage(info.activityInfo.packageName)
+ // Check if this package also resolves the Custom Tabs service.
+ if (pm.resolveService(serviceIntent, 0) != null) {
+ packagesSupportingCustomTabs.add(info)
+ }
+ }
+ return packagesSupportingCustomTabs
+}
+
fun Context.isInNightMode(): Boolean {
val currentNightMode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
return currentNightMode == Configuration.UI_MODE_NIGHT_YES
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 0219da7732..9765ac7308 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -48,6 +48,7 @@
Marked as unread
Removed bookmark
Chapters removed.
+ Chapter not found
- Remove %1$d downloaded chapter?
- Remove %1$d downloaded chapters?