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)