From eaf36bacd39df6927546afb055d3c0e6aa587cc6 Mon Sep 17 00:00:00 2001 From: h-hyuuga <83582211+h-hyuuga@users.noreply.github.com> Date: Fri, 14 May 2021 17:54:19 -0400 Subject: [PATCH] Facilitate Deeplinking for multisrc extensions (#6994) * ThemeSourceGenerator: Support for default AndroidManifest.xml + Added extra manifestPlaceholders * WPMangaReader: Add support for search by url * WPMangaReader: Add WPMangaReaderUrlActivity.kt * WPMangaReader: GSNation needs to set title in mangaParseDetails * Bump WPMangaReader * Bug fix: Add null guard when generating manifest placeholders * WPMangaReader: Support links directly to chapters * ThemeSourceGenerator: Remove placeholders unneeded for making intents --- .../wpmangareader/default/AndroidManifest.xml | 32 +++++++++ .../wpmangareader/gsnation/src/GSNation.kt | 1 + .../multisrc/wpmangareader/WPMangaReader.kt | 65 ++++++++++++++++++- .../wpmangareader/WPMangaReaderGenerator.kt | 2 +- .../wpmangareader/WPMangaReaderUrlActivity.kt | 36 ++++++++++ .../java/generator/ThemeSourceGenerator.kt | 22 ++++++- 6 files changed, 152 insertions(+), 6 deletions(-) create mode 100644 multisrc/overrides/wpmangareader/default/AndroidManifest.xml create mode 100644 multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/wpmangareader/WPMangaReaderUrlActivity.kt diff --git a/multisrc/overrides/wpmangareader/default/AndroidManifest.xml b/multisrc/overrides/wpmangareader/default/AndroidManifest.xml new file mode 100644 index 000000000..15a26d156 --- /dev/null +++ b/multisrc/overrides/wpmangareader/default/AndroidManifest.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="eu.kanade.tachiyomi.extension"> + + <application> + <activity + android:name="eu.kanade.tachiyomi.multisrc.wpmangareader.WPMangaReaderUrlActivity" + android:excludeFromRecents="true" + android:theme="@android:style/Theme.NoDisplay"> + <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="${SOURCEHOST}" + android:pathPattern="/.*/..*" + android:scheme="${SOURCESCHEME}" /> + </intent-filter> + <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="${SOURCEHOST}" + android:pathPattern="/..*" + android:scheme="${SOURCESCHEME}" /> + </intent-filter> + </activity> + </application> +</manifest> \ No newline at end of file diff --git a/multisrc/overrides/wpmangareader/gsnation/src/GSNation.kt b/multisrc/overrides/wpmangareader/gsnation/src/GSNation.kt index 76777bda2..77513108e 100644 --- a/multisrc/overrides/wpmangareader/gsnation/src/GSNation.kt +++ b/multisrc/overrides/wpmangareader/gsnation/src/GSNation.kt @@ -36,6 +36,7 @@ class GSNation : WPMangaReader("GS Nation", "http://gs-nation.fr", "fr", dateFor thumbnail_url = document.select("div.thumb img").attr("abs:src") description = document.select(".entry-content[itemprop=description]").joinToString("\n") { it.text() } + title = document.selectFirst("h1.entry-title").text() // add series type(manga/manhwa/manhua/other) thinggy to genre document.select(seriesTypeSelector).firstOrNull()?.ownText()?.let { diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/wpmangareader/WPMangaReader.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/wpmangareader/WPMangaReader.kt index 8743bf52f..c703776a2 100644 --- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/wpmangareader/WPMangaReader.kt +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/wpmangareader/WPMangaReader.kt @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.multisrc.wpmangareader import android.util.Log import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage @@ -18,6 +19,8 @@ import okhttp3.Response import org.json.JSONArray import org.jsoup.nodes.Document import org.jsoup.nodes.Element +import rx.Observable +import rx.Single import java.text.SimpleDateFormat import java.util.Locale @@ -41,7 +44,6 @@ abstract class WPMangaReader( override fun popularMangaSelector() = throw UnsupportedOperationException("Not used") override fun popularMangaNextPageSelector() = throw UnsupportedOperationException("Not used") - // latest override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", FilterList(OrderByFilter(3))) override fun latestUpdatesParse(response: Response) = searchMangaParse(response) @@ -53,6 +55,59 @@ abstract class WPMangaReader( // search override fun searchMangaSelector() = ".utao .uta .imgu, .listupd .bs .bsx, .listo .bs .bsx" + + /** + * Given some string which represents an http url, returns a identifier (id) for a manga + * which can be used to fetch its details at "$baseUrl$mangaUrlDirectory/$id" + * + * @param s: String - url + * + * @returns An identifier for a manga, or null if none could be found + */ + protected open fun mangaIdFromUrl(s: String): Single<String?> { + var baseMangaUrl = "$baseUrl$mangaUrlDirectory".toHttpUrlOrNull()!! + return s.toHttpUrlOrNull()?.let { url -> + fun pathLengthIs(url: HttpUrl, n: Int, strict: Boolean = false) = url.pathSegments.size == n && url.pathSegments[n - 1].isNotEmpty() || (!strict && url.pathSegments.size == n + 1 && url.pathSegments[n].isEmpty()) + val isMangaUrl = listOf( + baseMangaUrl.host == url.host, + pathLengthIs(url, 2), + url.pathSegments[0] == baseMangaUrl.pathSegments[0]).all { it } + val potentiallyChapterUrl = pathLengthIs(url, 1) + if (isMangaUrl) + Single.just(url.pathSegments[1]) + else if (potentiallyChapterUrl) + client.newCall(GET(s, headers)).asObservableSuccess().map { + val links = it.asJsoup().select("a[itemprop=item]") + if (links.size == 3) // near the top of page: home > manga > current chapter + links[1].attr("href").toHttpUrlOrNull()?.pathSegments?.get(1) + else + null + }.toSingle() + else + Single.just(null) + } ?: Single.just(null) + } + + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> { + if (!query.startsWith(URL_SEARCH_PREFIX)) + return super.fetchSearchManga(page, query, filters) + + return mangaIdFromUrl(query.substringAfter(URL_SEARCH_PREFIX)) + .toObservable() + .concatMap { id -> + if (id == null) + Observable.just(MangasPage(emptyList(), false)) + else + fetchMangaDetails(SManga.create().apply { this.url = "$mangaUrlDirectory/$id" }) + .map { + it.url = "$mangaUrlDirectory/$id"// isn't set in returned manga + MangasPage(listOf(it), false) + } + } + + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { val url = "$baseUrl".toHttpUrlOrNull()!!.newBuilder() if (query.isNotEmpty()) { @@ -85,7 +140,8 @@ abstract class WPMangaReader( title = element.select("a").attr("title") setUrlWithoutDomain(element.select("a").attr("href")) } - override fun searchMangaNextPageSelector() = "div.pagination .next, div.hpage .r" + + override fun searchMangaNextPageSelector() = "div.pagination .next, div.hpage .r" // manga details override fun mangaDetailsParse(document: Document) = SManga.create().apply { @@ -101,6 +157,7 @@ abstract class WPMangaReader( .text() ) + title = document.selectFirst("h1.entry-title").text() thumbnail_url = document.select(".infomanga > div[itemprop=image] img, .thumb img").attr("abs:src") description = document.select(".desc, .entry-content[itemprop=description]").joinToString("\n") { it.text() } @@ -278,4 +335,8 @@ abstract class WPMangaReader( LabeledValue("Added", "latest"), LabeledValue("Popular", "popular") ) + + companion object { + const val URL_SEARCH_PREFIX = "url:" + } } diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/wpmangareader/WPMangaReaderGenerator.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/wpmangareader/WPMangaReaderGenerator.kt index e96d0bab3..cf50ef6ad 100644 --- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/wpmangareader/WPMangaReaderGenerator.kt +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/wpmangareader/WPMangaReaderGenerator.kt @@ -9,7 +9,7 @@ class WPMangaReaderGenerator : ThemeSourceGenerator { override val themeClass = "WPMangaReader" - override val baseVersionCode: Int = 5 + override val baseVersionCode: Int = 6 override val sources = listOf( diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/wpmangareader/WPMangaReaderUrlActivity.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/wpmangareader/WPMangaReaderUrlActivity.kt new file mode 100644 index 000000000..cf9969c33 --- /dev/null +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/wpmangareader/WPMangaReaderUrlActivity.kt @@ -0,0 +1,36 @@ +package eu.kanade.tachiyomi.multisrc.wpmangareader + +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.Intent +import android.os.Bundle +import android.util.Log +import eu.kanade.tachiyomi.multisrc.wpmangareader.WPMangaReader +import kotlin.system.exitProcess + +class WPMangaReaderUrlActivity : Activity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val pathSegments = intent?.data?.pathSegments + + if (pathSegments != null && pathSegments.size >= 1) { + + val mainIntent = Intent().apply { + action = "eu.kanade.tachiyomi.SEARCH" + putExtra("query","${WPMangaReader.URL_SEARCH_PREFIX}${intent?.data?.toString()}") + putExtra("filter", packageName) + } + try { + startActivity(mainIntent) + } catch (e: ActivityNotFoundException) { + Log.e("WPMangaReaderUrl", e.toString()) + } + } else { + Log.e("WPMangaReaderUrl", "could not parse uri from intent $intent") + } + + finish() + exitProcess(0) + } +} diff --git a/multisrc/src/main/java/generator/ThemeSourceGenerator.kt b/multisrc/src/main/java/generator/ThemeSourceGenerator.kt index ba8b32c13..2f575b7f9 100644 --- a/multisrc/src/main/java/generator/ThemeSourceGenerator.kt +++ b/multisrc/src/main/java/generator/ThemeSourceGenerator.kt @@ -1,5 +1,6 @@ package generator +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import java.io.File import java.nio.file.Files import java.nio.file.StandardCopyOption @@ -54,7 +55,10 @@ interface ThemeSourceGenerator { val defaultAdditionalGradleText = File(defaultAdditionalGradlePath).readTextOrEmptyString() val additionalGradleOverrideText = File(additionalGradleOverridePath).readTextOrEmptyString() - + val placeholders = mapOf( + "SOURCEHOST" to source.baseUrl.toHttpUrlOrNull()?.host, + "SOURCESCHEME" to source.baseUrl.toHttpUrlOrNull()?.scheme + ).filter { it.value != null } gradle.writeText(""" // THIS FILE IS AUTO-GENERATED; DO NOT EDIT apply plugin: 'com.android.application' @@ -72,13 +76,24 @@ interface ThemeSourceGenerator { $defaultAdditionalGradleText $additionalGradleOverrideText apply from: "${'$'}rootDir/common.gradle" + + android { + defaultConfig { + manifestPlaceholders += [ +${placeholders.map { "${" ".repeat(28)}${it.key}: \"${it.value}\""}.joinToString(",\n")} + ] + } + } """.trimIndent()) } - private fun writeAndroidManifest(androidManifestFile: File, manifestOverridesPath: String) { + private fun writeAndroidManifest(androidManifestFile: File, manifestOverridesPath: String, defaultAndroidManifestPath: String) { val androidManifestOverride = File(manifestOverridesPath) + val defaultAndroidManifest = File(defaultAndroidManifestPath) if (androidManifestOverride.exists()) androidManifestOverride.copyTo(androidManifestFile) + else if (defaultAndroidManifest.exists()) + defaultAndroidManifest.copyTo(androidManifestFile) else androidManifestFile.writeText(""" <?xml version="1.0" encoding="utf-8"?> @@ -92,6 +107,7 @@ interface ThemeSourceGenerator { val projectSrcPath = "$projectRootPath/src/eu/kanade/tachiyomi/extension/${pkgNameSuffix(source, "/")}" val overridesPath = "$userDir/multisrc/overrides/$themePkg/${source.pkgName}" // userDir = tachiyomi-extensions project root path val defaultResPath = "$userDir/multisrc/overrides/$themePkg/default/res" + val defaultAndroidManifestPath = "$userDir/multisrc/overrides/$themePkg/default/AndroidManifest.xml" val defaultAdditionalGradlePath = "$userDir/multisrc/overrides/$themePkg/default/additional.gradle.kts" val resOverridePath = "$overridesPath/res" val srcOverridePath = "$overridesPath/src" @@ -108,7 +124,7 @@ interface ThemeSourceGenerator { cleanDirectory(projectRootFile) writeGradle(projectGradleFile, source, themePkg, baseVersionCode, defaultAdditionalGradlePath, additionalGradleOverridePath) - writeAndroidManifest(projectAndroidManifestFile, manifestOverridePath) + writeAndroidManifest(projectAndroidManifestFile, manifestOverridePath, defaultAndroidManifestPath) writeSourceClasses(projectSrcPath, srcOverridePath, source, themePkg, themeClass) copyThemeClasses(userDir, themePkg, projectRootPath)