Deeplink into chapters for a few sources

So far just mangadex, jamini's, mangaplus, kireicake
This commit is contained in:
Jay 2020-08-09 16:46:02 -04:00
parent b4151e6761
commit ad57086d8c
13 changed files with 592 additions and 17 deletions

View File

@ -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"/>

View File

@ -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()

View File

@ -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
}
}
@ -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)

View File

@ -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()

View File

@ -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)
}
}

View File

@ -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
}
}
}

View File

@ -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())
}
}

View File

@ -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")
}
}
}

View File

@ -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
}
}
}

View File

@ -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,6 +201,8 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>(),
}
if (presenter.needsInit()) {
fromUrl = handleIntentAction(intent)
if (!fromUrl) {
val manga = intent.extras!!.getLong("manga", -1)
val chapter = intent.extras!!.getLong("chapter", -1)
if (manga == -1L || chapter == -1L) {
@ -199,6 +210,9 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>(),
return
}
presenter.init(manga, chapter)
} else {
please_wait.visible()
}
}
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 {

View File

@ -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.

View File

@ -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()
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

View File

@ -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>