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> </intent-filter>
<meta-data android:name="android.app.searchable" android:resource="@xml/searchable"/> <meta-data android:name="android.app.searchable" android:resource="@xml/searchable"/>
</activity> </activity>
<activity <activity android:name=".ui.reader.ReaderActivity"
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 <activity
android:name=".ui.webview.WebViewActivity" android:name=".ui.webview.WebViewActivity"
android:configChanges="uiMode|orientation|screenSize"/> android:configChanges="uiMode|orientation|screenSize"/>

View File

@ -65,6 +65,15 @@ interface ChapterQueries : DbProvider {
.build()) .build())
.prepare() .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() fun getChapter(url: String, mangaId: Long) = db.get()
.`object`(Chapter::class.java) .`object`(Chapter::class.java)
.withQuery(Query.builder() .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.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.DelegatedHttpSource
import eu.kanade.tachiyomi.source.online.HttpSource 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 import rx.Observable
open class SourceManager(private val context: Context) { 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 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 { init {
createInternalSources().forEach { registerSource(it) } 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 getOnlineSources() = sourcesMap.values.filterIsInstance<HttpSource>()
fun getCatalogueSources() = sourcesMap.values.filterIsInstance<CatalogueSource>() fun getCatalogueSources() = sourcesMap.values.filterIsInstance<CatalogueSource>()
internal fun registerSource(source: Source, overwrite: Boolean = false) { internal fun registerSource(source: Source, overwrite: Boolean = false) {
if (overwrite || !sourcesMap.containsKey(source.id)) { if (overwrite || !sourcesMap.containsKey(source.id)) {
delegatedSources[source.id]?.delegatedHttpSource?.delegate = source as? HttpSource
sourcesMap[source.id] = source sourcesMap[source.id] = source
} }
} }
@ -68,14 +90,23 @@ open class SourceManager(private val context: Context) {
} }
private fun getSourceNotInstalledException(): Exception { private fun getSourceNotInstalledException(): Exception {
return SourceNotFoundException(context.getString(R.string.source_not_installed_, id return SourceNotFoundException(
.toString()), id) context.getString(
R.string.source_not_installed_, id.toString()
), id
)
} }
override fun hashCode(): Int { override fun hashCode(): Int {
return id.hashCode() 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) class SourceNotFoundException(message: String, val id: Long) : Exception(message)

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.source.model package eu.kanade.tachiyomi.source.model
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
import java.io.Serializable import java.io.Serializable
interface SChapter : Serializable { interface SChapter : Serializable {
@ -22,6 +23,16 @@ interface SChapter : Serializable {
scanlator = other.scanlator 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 { companion object {
fun create(): SChapter { fun create(): SChapter {
return SChapterImpl() 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.source.model.Page
import eu.kanade.tachiyomi.ui.base.MaterialMenuSheet import eu.kanade.tachiyomi.ui.base.MaterialMenuSheet
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity 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.main.SearchActivity
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.AddToLibraryFirst import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.AddToLibraryFirst
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.Error 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.hasSideNavBar
import eu.kanade.tachiyomi.util.system.isBottomTappable import eu.kanade.tachiyomi.util.system.isBottomTappable
import eu.kanade.tachiyomi.util.system.launchUI 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.system.toast
import eu.kanade.tachiyomi.util.view.collapse import eu.kanade.tachiyomi.util.view.collapse
import eu.kanade.tachiyomi.util.view.doOnApplyWindowInsets import eu.kanade.tachiyomi.util.view.doOnApplyWindowInsets
@ -71,12 +73,15 @@ import eu.kanade.tachiyomi.widget.SimpleAnimationListener
import eu.kanade.tachiyomi.widget.SimpleSeekBarListener import eu.kanade.tachiyomi.widget.SimpleSeekBarListener
import kotlinx.android.synthetic.main.reader_activity.* import kotlinx.android.synthetic.main.reader_activity.*
import kotlinx.android.synthetic.main.reader_chapters_sheet.* import kotlinx.android.synthetic.main.reader_chapters_sheet.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.sample import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.zhanghai.android.systemuihelper.SystemUiHelper import me.zhanghai.android.systemuihelper.SystemUiHelper
import nucleus.factory.RequiresPresenter import nucleus.factory.RequiresPresenter
import timber.log.Timber import timber.log.Timber
@ -125,6 +130,8 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>(),
private var coroutine: Job? = null private var coroutine: Job? = null
var fromUrl = false
/** /**
* System UI helper to hide status & navigation bar on all different API levels. * 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 private var snackbar: Snackbar? = null
var intentPageNumber: Int? = null
companion object { companion object {
@Suppress("unused") @Suppress("unused")
const val LEFT_TO_RIGHT = 1 const val LEFT_TO_RIGHT = 1
@ -192,6 +201,8 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>(),
} }
if (presenter.needsInit()) { if (presenter.needsInit()) {
fromUrl = handleIntentAction(intent)
if (!fromUrl) {
val manga = intent.extras!!.getLong("manga", -1) val manga = intent.extras!!.getLong("manga", -1)
val chapter = intent.extras!!.getLong("chapter", -1) val chapter = intent.extras!!.getLong("chapter", -1)
if (manga == -1L || chapter == -1L) { if (manga == -1L || chapter == -1L) {
@ -199,6 +210,9 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>(),
return return
} }
presenter.init(manga, chapter) presenter.init(manga, chapter)
} else {
please_wait.visible()
}
} }
if (savedInstanceState != null) { if (savedInstanceState != null) {
@ -282,6 +296,19 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>(),
return true 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 * Called when the user clicks the back key or the button on the toolbar. The call is
* delegated to the presenter. * delegated to the presenter.
@ -292,7 +319,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>(),
return return
} }
presenter.onBackPressed() presenter.onBackPressed()
super.onBackPressed() finish()
} }
/** /**
@ -325,7 +352,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>(),
window.statusBarColor = Color.TRANSPARENT window.statusBarColor = Color.TRANSPARENT
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
toolbar.setNavigationOnClickListener { toolbar.setNavigationOnClickListener {
onBackPressed() popToMain()
} }
toolbar.setOnClickListener { toolbar.setOnClickListener {
@ -505,6 +532,8 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>(),
fun setChapters(viewerChapters: ViewerChapters) { fun setChapters(viewerChapters: ViewerChapters) {
please_wait.gone() please_wait.gone()
viewer?.setChapters(viewerChapters) viewer?.setChapters(viewerChapters)
intentPageNumber?.let { moveToPageIndex(it) }
intentPageNumber = null
toolbar.subtitle = viewerChapters.currChapter.chapter.name 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() { fun openMangaInBrowser() {
val source = presenter.getSource() ?: return val source = presenter.getSource() ?: return
val url = try { val url = try {

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.reader package eu.kanade.tachiyomi.ui.reader
import android.app.Application import android.app.Application
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Environment import android.os.Environment
import com.jakewharton.rxrelay.BehaviorRelay 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.ReaderPage
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
import eu.kanade.tachiyomi.util.chapter.ChapterFilter 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.storage.DiskUtil
import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.system.ImageUtil
import eu.kanade.tachiyomi.util.system.executeOnIO
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch 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. * 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. * 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.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.content.res.Configuration import android.content.res.Configuration
import android.content.res.Resources import android.content.res.Resources
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.PowerManager import android.os.PowerManager
import android.view.View import android.view.View
@ -23,6 +23,7 @@ import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import androidx.browser.customtabs.CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.net.toUri import androidx.core.net.toUri
@ -222,18 +223,47 @@ fun Context.isServiceRunning(serviceClass: Class<*>): Boolean {
/** /**
* Opens a URL in a custom tab. * Opens a URL in a custom tab.
*/ */
fun Context.openInBrowser(url: String) { fun Context.openInBrowser(url: String, forceBrowser: Boolean = false): Boolean {
try { try {
val parsedUrl = url.toUri() val parsedUrl = url.toUri()
val intent = CustomTabsIntent.Builder() val intent = CustomTabsIntent.Builder()
.setToolbarColor(getResourceColor(R.attr.colorPrimaryVariant)) .setToolbarColor(getResourceColor(R.attr.colorPrimaryVariant))
.build() .build()
if (forceBrowser) {
val packages = getCustomTabsPackages().maxBy { it.preferredOrder }
val processName = packages?.activityInfo?.processName ?: return false
intent.intent.`package` = processName
}
intent.launchUrl(this, parsedUrl) intent.launchUrl(this, parsedUrl)
return true
} catch (e: Exception) { } catch (e: Exception) {
toast(e.message) 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 { fun Context.isInNightMode(): Boolean {
val currentNightMode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK val currentNightMode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
return currentNightMode == Configuration.UI_MODE_NIGHT_YES return currentNightMode == Configuration.UI_MODE_NIGHT_YES

View File

@ -48,6 +48,7 @@
<string name="marked_as_unread">Marked as unread</string> <string name="marked_as_unread">Marked as unread</string>
<string name="removed_bookmark">Removed bookmark</string> <string name="removed_bookmark">Removed bookmark</string>
<string name="chapters_removed">Chapters removed.</string> <string name="chapters_removed">Chapters removed.</string>
<string name="chapter_not_found">Chapter not found</string>
<plurals name="remove_n_chapters"> <plurals name="remove_n_chapters">
<item quantity="one">Remove %1$d downloaded chapter?</item> <item quantity="one">Remove %1$d downloaded chapter?</item>
<item quantity="other">Remove %1$d downloaded chapters?</item> <item quantity="other">Remove %1$d downloaded chapters?</item>