mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-11-14 04:15:09 +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>
|
</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"/>
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -43,7 +65,7 @@ open class SourceManager(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun createInternalSources(): List<Source> = listOf(
|
private fun createInternalSources(): List<Source> = listOf(
|
||||||
LocalSource(context)
|
LocalSource(context)
|
||||||
)
|
)
|
||||||
|
|
||||||
private inner class StubSource(override val id: Long) : Source {
|
private inner class StubSource(override val id: Long) : 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)
|
||||||
|
@ -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()
|
||||||
|
@ -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.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,13 +201,18 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (presenter.needsInit()) {
|
if (presenter.needsInit()) {
|
||||||
val manga = intent.extras!!.getLong("manga", -1)
|
fromUrl = handleIntentAction(intent)
|
||||||
val chapter = intent.extras!!.getLong("chapter", -1)
|
if (!fromUrl) {
|
||||||
if (manga == -1L || chapter == -1L) {
|
val manga = intent.extras!!.getLong("manga", -1)
|
||||||
finish()
|
val chapter = intent.extras!!.getLong("chapter", -1)
|
||||||
return
|
if (manga == -1L || chapter == -1L) {
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
presenter.init(manga, chapter)
|
||||||
|
} else {
|
||||||
|
please_wait.visible()
|
||||||
}
|
}
|
||||||
presenter.init(manga, chapter)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user