mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-11-09 05:25:08 +01:00
Deeplink into chapters for a few sources
So far just mangadex, jamini's, mangaplus, kireicake
This commit is contained in:
parent
b4151e6761
commit
ad57086d8c
@ -55,8 +55,35 @@
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.app.searchable" android:resource="@xml/searchable"/>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.reader.ReaderActivity" />
|
||||
<activity android:name=".ui.reader.ReaderActivity"
|
||||
android:theme="@style/Theme.Splash">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="reader.kireicake.com"
|
||||
android:pathPattern="/read/..*/..*/..*/..*"
|
||||
android:scheme="https" />
|
||||
|
||||
<data
|
||||
android:host="jaiminisbox.com"
|
||||
android:pathPattern="/reader/read/..*/..*/..*/..*"
|
||||
android:scheme="https" />
|
||||
|
||||
<data
|
||||
android:host="mangadex.org"
|
||||
android:pathPattern="/chapter/..*"
|
||||
android:scheme="https" />
|
||||
|
||||
<data
|
||||
android:host="mangaplus.shueisha.co.jp"
|
||||
android:pathPattern="/viewer/..*"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.webview.WebViewActivity"
|
||||
android:configChanges="uiMode|orientation|screenSize"/>
|
||||
|
@ -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()
|
||||
|
@ -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<Long, StubSource>()
|
||||
|
||||
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<HttpSource>()
|
||||
|
||||
fun getCatalogueSources() = sourcesMap.values.filterIsInstance<CatalogueSource>()
|
||||
|
||||
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<Source> = 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)
|
||||
|
@ -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()
|
||||
|
@ -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<Chapter, Manga, List<SChapter>>?
|
||||
|
||||
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<SChapter>? {
|
||||
val id = delegate?.id ?: return null
|
||||
val manga = Manga.create(url, "", id)
|
||||
return delegate?.fetchChapterListAsync(manga)
|
||||
}
|
||||
}
|
@ -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<Chapter, Manga, List<SChapter>>? {
|
||||
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<PreferencesHelper>().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
|
||||
}
|
||||
}
|
||||
}
|
@ -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<Chapter, Manga, List<SChapter>>? {
|
||||
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<PreferencesHelper>().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())
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
@ -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<Chapter, Manga, List<SChapter>>? {
|
||||
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<PreferencesHelper>().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
|
||||
}
|
||||
}
|
||||
}
|
@ -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<ReaderPresenter>(),
|
||||
|
||||
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<ReaderPresenter>(),
|
||||
|
||||
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<ReaderPresenter>(),
|
||||
}
|
||||
|
||||
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<ReaderPresenter>(),
|
||||
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<ReaderPresenter>(),
|
||||
return
|
||||
}
|
||||
presenter.onBackPressed()
|
||||
super.onBackPressed()
|
||||
finish()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -325,7 +352,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>(),
|
||||
window.statusBarColor = Color.TRANSPARENT
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
toolbar.setNavigationOnClickListener {
|
||||
onBackPressed()
|
||||
popToMain()
|
||||
}
|
||||
|
||||
toolbar.setOnClickListener {
|
||||
@ -505,6 +532,8 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>(),
|
||||
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<ReaderPresenter>(),
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -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.
|
||||
|
@ -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<ResolveInfo> {
|
||||
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<ResolveInfo>()
|
||||
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
|
||||
|
@ -48,6 +48,7 @@
|
||||
<string name="marked_as_unread">Marked as unread</string>
|
||||
<string name="removed_bookmark">Removed bookmark</string>
|
||||
<string name="chapters_removed">Chapters removed.</string>
|
||||
<string name="chapter_not_found">Chapter not found</string>
|
||||
<plurals name="remove_n_chapters">
|
||||
<item quantity="one">Remove %1$d downloaded chapter?</item>
|
||||
<item quantity="other">Remove %1$d downloaded chapters?</item>
|
||||
|
Loading…
Reference in New Issue
Block a user