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
This commit is contained in:
parent
b1dbd87295
commit
eaf36bacd3
|
@ -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>
|
|
@ -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")
|
thumbnail_url = document.select("div.thumb img").attr("abs:src")
|
||||||
description = document.select(".entry-content[itemprop=description]").joinToString("\n") { it.text() }
|
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
|
// add series type(manga/manhwa/manhua/other) thinggy to genre
|
||||||
document.select(seriesTypeSelector).firstOrNull()?.ownText()?.let {
|
document.select(seriesTypeSelector).firstOrNull()?.ownText()?.let {
|
||||||
|
|
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.multisrc.wpmangareader
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import eu.kanade.tachiyomi.network.GET
|
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.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
@ -18,6 +19,8 @@ import okhttp3.Response
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
|
import rx.Observable
|
||||||
|
import rx.Single
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
|
@ -41,7 +44,6 @@ abstract class WPMangaReader(
|
||||||
override fun popularMangaSelector() = throw UnsupportedOperationException("Not used")
|
override fun popularMangaSelector() = throw UnsupportedOperationException("Not used")
|
||||||
override fun popularMangaNextPageSelector() = throw UnsupportedOperationException("Not used")
|
override fun popularMangaNextPageSelector() = throw UnsupportedOperationException("Not used")
|
||||||
|
|
||||||
|
|
||||||
// latest
|
// latest
|
||||||
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", FilterList(OrderByFilter(3)))
|
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", FilterList(OrderByFilter(3)))
|
||||||
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
|
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
|
||||||
|
@ -53,6 +55,59 @@ abstract class WPMangaReader(
|
||||||
// search
|
// search
|
||||||
override fun searchMangaSelector() = ".utao .uta .imgu, .listupd .bs .bsx, .listo .bs .bsx"
|
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 {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val url = "$baseUrl".toHttpUrlOrNull()!!.newBuilder()
|
val url = "$baseUrl".toHttpUrlOrNull()!!.newBuilder()
|
||||||
if (query.isNotEmpty()) {
|
if (query.isNotEmpty()) {
|
||||||
|
@ -85,7 +140,8 @@ abstract class WPMangaReader(
|
||||||
title = element.select("a").attr("title")
|
title = element.select("a").attr("title")
|
||||||
setUrlWithoutDomain(element.select("a").attr("href"))
|
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
|
// manga details
|
||||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||||
|
@ -101,6 +157,7 @@ abstract class WPMangaReader(
|
||||||
.text()
|
.text()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
title = document.selectFirst("h1.entry-title").text()
|
||||||
thumbnail_url = document.select(".infomanga > div[itemprop=image] img, .thumb img").attr("abs:src")
|
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() }
|
description = document.select(".desc, .entry-content[itemprop=description]").joinToString("\n") { it.text() }
|
||||||
|
|
||||||
|
@ -278,4 +335,8 @@ abstract class WPMangaReader(
|
||||||
LabeledValue("Added", "latest"),
|
LabeledValue("Added", "latest"),
|
||||||
LabeledValue("Popular", "popular")
|
LabeledValue("Popular", "popular")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val URL_SEARCH_PREFIX = "url:"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ class WPMangaReaderGenerator : ThemeSourceGenerator {
|
||||||
|
|
||||||
override val themeClass = "WPMangaReader"
|
override val themeClass = "WPMangaReader"
|
||||||
|
|
||||||
override val baseVersionCode: Int = 5
|
override val baseVersionCode: Int = 6
|
||||||
|
|
||||||
override val sources = listOf(
|
override val sources = listOf(
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
package generator
|
package generator
|
||||||
|
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.StandardCopyOption
|
import java.nio.file.StandardCopyOption
|
||||||
|
@ -54,7 +55,10 @@ interface ThemeSourceGenerator {
|
||||||
|
|
||||||
val defaultAdditionalGradleText = File(defaultAdditionalGradlePath).readTextOrEmptyString()
|
val defaultAdditionalGradleText = File(defaultAdditionalGradlePath).readTextOrEmptyString()
|
||||||
val additionalGradleOverrideText = File(additionalGradleOverridePath).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("""
|
gradle.writeText("""
|
||||||
// THIS FILE IS AUTO-GENERATED; DO NOT EDIT
|
// THIS FILE IS AUTO-GENERATED; DO NOT EDIT
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
|
@ -72,13 +76,24 @@ interface ThemeSourceGenerator {
|
||||||
$defaultAdditionalGradleText
|
$defaultAdditionalGradleText
|
||||||
$additionalGradleOverrideText
|
$additionalGradleOverrideText
|
||||||
apply from: "${'$'}rootDir/common.gradle"
|
apply from: "${'$'}rootDir/common.gradle"
|
||||||
|
|
||||||
|
android {
|
||||||
|
defaultConfig {
|
||||||
|
manifestPlaceholders += [
|
||||||
|
${placeholders.map { "${" ".repeat(28)}${it.key}: \"${it.value}\""}.joinToString(",\n")}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
""".trimIndent())
|
""".trimIndent())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun writeAndroidManifest(androidManifestFile: File, manifestOverridesPath: String) {
|
private fun writeAndroidManifest(androidManifestFile: File, manifestOverridesPath: String, defaultAndroidManifestPath: String) {
|
||||||
val androidManifestOverride = File(manifestOverridesPath)
|
val androidManifestOverride = File(manifestOverridesPath)
|
||||||
|
val defaultAndroidManifest = File(defaultAndroidManifestPath)
|
||||||
if (androidManifestOverride.exists())
|
if (androidManifestOverride.exists())
|
||||||
androidManifestOverride.copyTo(androidManifestFile)
|
androidManifestOverride.copyTo(androidManifestFile)
|
||||||
|
else if (defaultAndroidManifest.exists())
|
||||||
|
defaultAndroidManifest.copyTo(androidManifestFile)
|
||||||
else
|
else
|
||||||
androidManifestFile.writeText("""
|
androidManifestFile.writeText("""
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
@ -92,6 +107,7 @@ interface ThemeSourceGenerator {
|
||||||
val projectSrcPath = "$projectRootPath/src/eu/kanade/tachiyomi/extension/${pkgNameSuffix(source, "/")}"
|
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 overridesPath = "$userDir/multisrc/overrides/$themePkg/${source.pkgName}" // userDir = tachiyomi-extensions project root path
|
||||||
val defaultResPath = "$userDir/multisrc/overrides/$themePkg/default/res"
|
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 defaultAdditionalGradlePath = "$userDir/multisrc/overrides/$themePkg/default/additional.gradle.kts"
|
||||||
val resOverridePath = "$overridesPath/res"
|
val resOverridePath = "$overridesPath/res"
|
||||||
val srcOverridePath = "$overridesPath/src"
|
val srcOverridePath = "$overridesPath/src"
|
||||||
|
@ -108,7 +124,7 @@ interface ThemeSourceGenerator {
|
||||||
cleanDirectory(projectRootFile)
|
cleanDirectory(projectRootFile)
|
||||||
|
|
||||||
writeGradle(projectGradleFile, source, themePkg, baseVersionCode, defaultAdditionalGradlePath, additionalGradleOverridePath)
|
writeGradle(projectGradleFile, source, themePkg, baseVersionCode, defaultAdditionalGradlePath, additionalGradleOverridePath)
|
||||||
writeAndroidManifest(projectAndroidManifestFile, manifestOverridePath)
|
writeAndroidManifest(projectAndroidManifestFile, manifestOverridePath, defaultAndroidManifestPath)
|
||||||
|
|
||||||
writeSourceClasses(projectSrcPath, srcOverridePath, source, themePkg, themeClass)
|
writeSourceClasses(projectSrcPath, srcOverridePath, source, themePkg, themeClass)
|
||||||
copyThemeClasses(userDir, themePkg, projectRootPath)
|
copyThemeClasses(userDir, themePkg, projectRootPath)
|
||||||
|
|
Loading…
Reference in New Issue