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