New Lib: Text Interceptor (#13859)
* move TextInterceptor to `lib` to be used for author notes, image alt texts and what not * refactor Tapastic to use `lib:extensions-lib` * Refactor Webtoons to use `:lib-textinterceptor` this turned out to be more complicated than I thought it'd be TextInterceptor was used for Author Notes which, looking at previous messages, was only intended to be added to Webtoons Source and not the entire webtoons multisrc (i.e. WebtoonsTranslate and DongmanManhua don't seem to be making use of the Show Author's Notes setting). This was in my favor, since having to deal with additional.gradle to add dependencies to multisrc files doesn't seem to work... I'll ask previous contributors just in case * Fix `json` access missed this while copying over code from `generated-src` to `multisrc/{overrides,src}` * remove unused import * make HOST name more clear couldve used a better schema but that's something for some other time also put the HOST in the lib itself so that one doesn't lose track of it in the extensions * use android provided methods instead of hardcoding based on https://github.com/tachiyomiorg/tachiyomi-extensions/pull/13859/files#r996276738 that suggested the following SO answer: https://github.com/tachiyomiorg/tachiyomi-extensions/pull/13859/files * remove unused import * move url generation to helper function * fix error oops sorry for that happened when I was copy pasting back and forth between two sources and one generated source Co-authored-by: Navaneeth M Nambiar <nmnambiar@hornbill>
This commit is contained in:
parent
2f4658b3b3
commit
951ca60b56
|
@ -0,0 +1,22 @@
|
||||||
|
plugins {
|
||||||
|
id("com.android.library")
|
||||||
|
kotlin("android")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdk = AndroidConfig.compileSdk
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = AndroidConfig.minSdk
|
||||||
|
targetSdk = AndroidConfig.targetSdk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly(libs.kotlin.stdlib)
|
||||||
|
compileOnly(libs.okhttp)
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="eu.kanade.tachiyomi.lib.textinterceptor" />
|
|
@ -0,0 +1,121 @@
|
||||||
|
package eu.kanade.tachiyomi.lib.textinterceptor
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.text.Html
|
||||||
|
import android.text.Layout
|
||||||
|
import android.text.StaticLayout
|
||||||
|
import android.text.TextPaint
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.Protocol
|
||||||
|
import okhttp3.Response
|
||||||
|
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
|
||||||
|
class TextInterceptor : Interceptor {
|
||||||
|
// With help from:
|
||||||
|
// https://github.com/tachiyomiorg/tachiyomi-extensions/pull/13304#issuecomment-1234532897
|
||||||
|
// https://medium.com/over-engineering/drawing-multiline-text-to-canvas-on-android-9b98f0bfa16a
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// Designer values:
|
||||||
|
private const val WIDTH: Int = 1000
|
||||||
|
private const val X_PADDING: Float = 50f
|
||||||
|
private const val Y_PADDING: Float = 25f
|
||||||
|
private const val HEADING_FONT_SIZE: Float = 36f
|
||||||
|
private const val BODY_FONT_SIZE: Float = 30f
|
||||||
|
private const val SPACING_MULT: Float = 1.1f
|
||||||
|
private const val SPACING_ADD: Float = 2f
|
||||||
|
|
||||||
|
// No need to touch this one:
|
||||||
|
private const val HOST = TextInterceptorHelper.HOST
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val request = chain.request()
|
||||||
|
val url = request.url
|
||||||
|
if (url.host != HOST) return chain.proceed(request)
|
||||||
|
|
||||||
|
val creator = textFixer("Author's Notes from ${url.pathSegments[0]}")
|
||||||
|
val story = textFixer(url.pathSegments[1])
|
||||||
|
|
||||||
|
// Heading
|
||||||
|
val paintHeading = TextPaint().apply {
|
||||||
|
color = Color.BLACK
|
||||||
|
textSize = HEADING_FONT_SIZE
|
||||||
|
typeface = Typeface.DEFAULT_BOLD
|
||||||
|
isAntiAlias = true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
val heading: StaticLayout = StaticLayout(
|
||||||
|
creator, paintHeading, (WIDTH - 2 * X_PADDING).toInt(),
|
||||||
|
Layout.Alignment.ALIGN_NORMAL, SPACING_MULT, SPACING_ADD, true
|
||||||
|
)
|
||||||
|
|
||||||
|
// Body
|
||||||
|
val paintBody = TextPaint().apply {
|
||||||
|
color = Color.BLACK
|
||||||
|
textSize = BODY_FONT_SIZE
|
||||||
|
typeface = Typeface.DEFAULT
|
||||||
|
isAntiAlias = true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
val body: StaticLayout = StaticLayout(
|
||||||
|
story, paintBody, (WIDTH - 2 * X_PADDING).toInt(),
|
||||||
|
Layout.Alignment.ALIGN_NORMAL, SPACING_MULT, SPACING_ADD, true
|
||||||
|
)
|
||||||
|
|
||||||
|
// Image building
|
||||||
|
val imgHeight: Int = (heading.height + body.height + 2 * Y_PADDING).toInt()
|
||||||
|
val bitmap: Bitmap = Bitmap.createBitmap(WIDTH, imgHeight, Bitmap.Config.ARGB_8888)
|
||||||
|
val canvas: Canvas = Canvas(bitmap)
|
||||||
|
|
||||||
|
// Image drawing
|
||||||
|
canvas.drawColor(Color.WHITE)
|
||||||
|
heading.draw(canvas, X_PADDING, Y_PADDING)
|
||||||
|
body.draw(canvas, X_PADDING, Y_PADDING + heading.height.toFloat())
|
||||||
|
|
||||||
|
// Image converting & returning
|
||||||
|
val stream = ByteArrayOutputStream()
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.PNG, 0, stream)
|
||||||
|
val responseBody = stream.toByteArray().toResponseBody("image/png".toMediaType())
|
||||||
|
return Response.Builder()
|
||||||
|
.request(request)
|
||||||
|
.protocol(Protocol.HTTP_1_1)
|
||||||
|
.code(200)
|
||||||
|
.message("OK")
|
||||||
|
.body(responseBody)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun textFixer(htmlString: String): String {
|
||||||
|
return if (Build.VERSION.SDK_INT >= 24) {
|
||||||
|
Html.fromHtml(htmlString , Html.FROM_HTML_MODE_LEGACY).toString()
|
||||||
|
} else {
|
||||||
|
Html.fromHtml(htmlString).toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun StaticLayout.draw(canvas: Canvas, x: Float, y: Float) {
|
||||||
|
canvas.save()
|
||||||
|
canvas.translate(x, y)
|
||||||
|
this.draw(canvas)
|
||||||
|
canvas.restore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object TextInterceptorHelper {
|
||||||
|
|
||||||
|
const val HOST = "tachiyomi-lib-textinterceptor"
|
||||||
|
|
||||||
|
fun createUrl(creator: String, text: String): String {
|
||||||
|
return "http://$HOST/" + Uri.encode(creator) + "/" + Uri.encode(text)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
dependencies {
|
||||||
|
implementation(project(':lib-textinterceptor'))
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.webtoons
|
package eu.kanade.tachiyomi.extension.all.webtoons
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.webtoons.Webtoons
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
import eu.kanade.tachiyomi.source.SourceFactory
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
@ -18,8 +17,8 @@ class WebtoonsFactory : SourceFactory {
|
||||||
WebtoonsDE(),
|
WebtoonsDE(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
class WebtoonsEN : Webtoons("Webtoons.com", "https://www.webtoons.com", "en")
|
class WebtoonsEN : WebtoonsSrc("Webtoons.com", "https://www.webtoons.com", "en")
|
||||||
class WebtoonsID : Webtoons("Webtoons.com", "https://www.webtoons.com", "id") {
|
class WebtoonsID : WebtoonsSrc("Webtoons.com", "https://www.webtoons.com", "id") {
|
||||||
// Override ID as part of the name was removed to be more consiten with other enteries
|
// Override ID as part of the name was removed to be more consiten with other enteries
|
||||||
override val id: Long = 8749627068478740298
|
override val id: Long = 8749627068478740298
|
||||||
|
|
||||||
|
@ -36,8 +35,8 @@ class WebtoonsID : Webtoons("Webtoons.com", "https://www.webtoons.com", "id") {
|
||||||
return GregorianCalendar(year.toInt(), monthIndex, day.toInt()).time.time
|
return GregorianCalendar(year.toInt(), monthIndex, day.toInt()).time.time
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class WebtoonsTH : Webtoons("Webtoons.com", "https://www.webtoons.com", "th", dateFormat = SimpleDateFormat("d MMM yyyy", Locale("th")))
|
class WebtoonsTH : WebtoonsSrc("Webtoons.com", "https://www.webtoons.com", "th", dateFormat = SimpleDateFormat("d MMM yyyy", Locale("th")))
|
||||||
class WebtoonsES : Webtoons("Webtoons.com", "https://www.webtoons.com", "es") {
|
class WebtoonsES : WebtoonsSrc("Webtoons.com", "https://www.webtoons.com", "es") {
|
||||||
// Android seems to be unable to parse es dates like Indonesian; we'll use a short hard-coded table instead.
|
// Android seems to be unable to parse es dates like Indonesian; we'll use a short hard-coded table instead.
|
||||||
private val dateMap: Array<String> = arrayOf(
|
private val dateMap: Array<String> = arrayOf(
|
||||||
"ene", "feb", "mar", "abr", "may", "jun", "jul", "ago", "sep", "oct", "nov", "dic"
|
"ene", "feb", "mar", "abr", "may", "jun", "jul", "ago", "sep", "oct", "nov", "dic"
|
||||||
|
@ -50,9 +49,9 @@ class WebtoonsES : Webtoons("Webtoons.com", "https://www.webtoons.com", "es") {
|
||||||
return GregorianCalendar(year.toInt(), monthIndex, day.toInt()).time.time
|
return GregorianCalendar(year.toInt(), monthIndex, day.toInt()).time.time
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class WebtoonsFR : Webtoons("Webtoons.com", "https://www.webtoons.com", "fr", dateFormat = SimpleDateFormat("d MMM yyyy", Locale.FRENCH))
|
class WebtoonsFR : WebtoonsSrc("Webtoons.com", "https://www.webtoons.com", "fr", dateFormat = SimpleDateFormat("d MMM yyyy", Locale.FRENCH))
|
||||||
class WebtoonsZH : Webtoons("Webtoons.com", "https://www.webtoons.com", "zh-Hant", "zh-hant", "zh_TW", SimpleDateFormat("yyyy/MM/dd", Locale.TRADITIONAL_CHINESE)) {
|
class WebtoonsZH : WebtoonsSrc("Webtoons.com", "https://www.webtoons.com", "zh-Hant", "zh-hant", "zh_TW", SimpleDateFormat("yyyy/MM/dd", Locale.TRADITIONAL_CHINESE)) {
|
||||||
// Due to lang code getting more specific
|
// Due to lang code getting more specific
|
||||||
override val id: Long = 2959982438613576472
|
override val id: Long = 2959982438613576472
|
||||||
}
|
}
|
||||||
class WebtoonsDE : Webtoons("Webtoons.com", "https://www.webtoons.com", "de", dateFormat = SimpleDateFormat("dd.MM.yyyy", Locale.GERMAN))
|
class WebtoonsDE : WebtoonsSrc("Webtoons.com", "https://www.webtoons.com", "de", dateFormat = SimpleDateFormat("dd.MM.yyyy", Locale.GERMAN))
|
||||||
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.all.webtoons
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.preference.PreferenceScreen
|
||||||
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
|
import eu.kanade.tachiyomi.lib.textinterceptor.TextInterceptor
|
||||||
|
import eu.kanade.tachiyomi.lib.textinterceptor.TextInterceptorHelper
|
||||||
|
import eu.kanade.tachiyomi.multisrc.webtoons.Webtoons
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
open class WebtoonsSrc(
|
||||||
|
override val name: String,
|
||||||
|
override val baseUrl: String,
|
||||||
|
override val lang: String,
|
||||||
|
langCode: String = lang,
|
||||||
|
override val localeForCookie: String = lang,
|
||||||
|
dateFormat: SimpleDateFormat = SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH)
|
||||||
|
) : ConfigurableSource, Webtoons(name, baseUrl, lang, langCode, localeForCookie, dateFormat) {
|
||||||
|
|
||||||
|
override val client: OkHttpClient = super.client.newBuilder()
|
||||||
|
.addInterceptor(TextInterceptor())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private val preferences: SharedPreferences by lazy {
|
||||||
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
|
val authorsNotesPref = SwitchPreferenceCompat(screen.context).apply {
|
||||||
|
key = SHOW_AUTHORS_NOTES_KEY
|
||||||
|
title = "Show author's notes"
|
||||||
|
summary = "Enable to see the author's notes at the end of chapters (if they're there)."
|
||||||
|
setDefaultValue(false)
|
||||||
|
|
||||||
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
val checkValue = newValue as Boolean
|
||||||
|
preferences.edit().putBoolean(SHOW_AUTHORS_NOTES_KEY, checkValue).commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
screen.addPreference(authorsNotesPref)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showAuthorsNotesPref() = preferences.getBoolean(SHOW_AUTHORS_NOTES_KEY, false)
|
||||||
|
|
||||||
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
|
var pages = document.select("div#_imageList > img").mapIndexed { i, element -> Page(i, "", element.attr("data-url")) }
|
||||||
|
|
||||||
|
if (showAuthorsNotesPref()) {
|
||||||
|
val note = document.select("div.comment_area div.info_area p").text()
|
||||||
|
|
||||||
|
if (note.isNotEmpty()) {
|
||||||
|
|
||||||
|
val creator = document.select("div.creator_note span.author a").text().trim()
|
||||||
|
|
||||||
|
pages = pages + Page(
|
||||||
|
pages.size, "",
|
||||||
|
TextInterceptorHelper.createUrl(creator, note)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pages.isNotEmpty()) { return pages }
|
||||||
|
|
||||||
|
val docString = document.toString()
|
||||||
|
|
||||||
|
val docUrlRegex = Regex("documentURL:.*?'(.*?)'")
|
||||||
|
val motiontoonPathRegex = Regex("jpg:.*?'(.*?)\\{")
|
||||||
|
|
||||||
|
val docUrl = docUrlRegex.find(docString)!!.destructured.toList()[0]
|
||||||
|
val motiontoonPath = motiontoonPathRegex.find(docString)!!.destructured.toList()[0]
|
||||||
|
val motiontoonResponse = client.newCall(GET(docUrl, headers)).execute()
|
||||||
|
|
||||||
|
val motiontoonJson = json.parseToJsonElement(motiontoonResponse.body!!.string()).jsonObject
|
||||||
|
val motiontoonImages = motiontoonJson["assets"]!!.jsonObject["image"]!!.jsonObject
|
||||||
|
|
||||||
|
return motiontoonImages.entries
|
||||||
|
.filter { it.key.contains("layer") }
|
||||||
|
.mapIndexed { i, entry ->
|
||||||
|
Page(i, "", motiontoonPath + entry.value.jsonPrimitive.content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val SHOW_AUTHORS_NOTES_KEY = "showAuthorsNotes"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,19 +1,6 @@
|
||||||
package eu.kanade.tachiyomi.multisrc.webtoons
|
package eu.kanade.tachiyomi.multisrc.webtoons
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.graphics.Typeface
|
|
||||||
import android.net.Uri
|
|
||||||
import android.text.Layout
|
|
||||||
import android.text.StaticLayout
|
|
||||||
import android.text.TextPaint
|
|
||||||
import androidx.preference.PreferenceScreen
|
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter.Header
|
import eu.kanade.tachiyomi.source.model.Filter.Header
|
||||||
import eu.kanade.tachiyomi.source.model.Filter.Select
|
import eu.kanade.tachiyomi.source.model.Filter.Select
|
||||||
import eu.kanade.tachiyomi.source.model.Filter.Separator
|
import eu.kanade.tachiyomi.source.model.Filter.Separator
|
||||||
|
@ -33,19 +20,13 @@ import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Protocol
|
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
|
||||||
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.Observable
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.net.SocketException
|
import java.net.SocketException
|
||||||
import java.text.ParseException
|
import java.text.ParseException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
@ -59,7 +40,7 @@ open class Webtoons(
|
||||||
open val langCode: String = lang,
|
open val langCode: String = lang,
|
||||||
open val localeForCookie: String = lang,
|
open val localeForCookie: String = lang,
|
||||||
private val dateFormat: SimpleDateFormat = SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH)
|
private val dateFormat: SimpleDateFormat = SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH)
|
||||||
) : ConfigurableSource, ParsedHttpSource() {
|
) : ParsedHttpSource() {
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
@ -84,7 +65,6 @@ open class Webtoons(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.addInterceptor(::sslRetryInterceptor)
|
.addInterceptor(::sslRetryInterceptor)
|
||||||
.addInterceptor(TextInterceptor)
|
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
// m.webtoons.com throws an SSL error that can be solved by a simple retry
|
// m.webtoons.com throws an SSL error that can be solved by a simple retry
|
||||||
|
@ -112,16 +92,12 @@ open class Webtoons(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
val json: Json by injectLazy()
|
||||||
|
|
||||||
override fun popularMangaSelector() = "not using"
|
override fun popularMangaSelector() = "not using"
|
||||||
|
|
||||||
override fun latestUpdatesSelector() = "div#dailyList > $day li > a"
|
override fun latestUpdatesSelector() = "div#dailyList > $day li > a"
|
||||||
|
|
||||||
private val preferences: SharedPreferences by lazy {
|
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
|
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
|
||||||
.add("Referer", "https://www.webtoons.com/$langCode/")
|
.add("Referer", "https://www.webtoons.com/$langCode/")
|
||||||
|
|
||||||
|
@ -129,23 +105,6 @@ open class Webtoons(
|
||||||
.add("Referer", "https://m.webtoons.com")
|
.add("Referer", "https://m.webtoons.com")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
|
||||||
val authorsNotesPref = SwitchPreferenceCompat(screen.context).apply {
|
|
||||||
key = SHOW_AUTHORS_NOTES_KEY
|
|
||||||
title = "Show author's notes"
|
|
||||||
summary = "Enable to see the author's notes at the end of chapters (if they're there)."
|
|
||||||
setDefaultValue(false)
|
|
||||||
|
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
val checkValue = newValue as Boolean
|
|
||||||
preferences.edit().putBoolean(SHOW_AUTHORS_NOTES_KEY, checkValue).commit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
screen.addPreference(authorsNotesPref)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showAuthorsNotesPref() = preferences.getBoolean(SHOW_AUTHORS_NOTES_KEY, false)
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/$langCode/dailySchedule", headers)
|
override fun popularMangaRequest(page: Int) = GET("$baseUrl/$langCode/dailySchedule", headers)
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage {
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
|
@ -317,20 +276,6 @@ open class Webtoons(
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
var pages = document.select("div#_imageList > img").mapIndexed { i, element -> Page(i, "", element.attr("data-url")) }
|
var pages = document.select("div#_imageList > img").mapIndexed { i, element -> Page(i, "", element.attr("data-url")) }
|
||||||
|
|
||||||
if (showAuthorsNotesPref()) {
|
|
||||||
val note = document.select("div.comment_area div.info_area p").text()
|
|
||||||
|
|
||||||
if (note.isNotEmpty()) {
|
|
||||||
|
|
||||||
val creator = document.select("div.creator_note span.author a").text().trim()
|
|
||||||
|
|
||||||
pages = pages + Page(
|
|
||||||
pages.size, "",
|
|
||||||
"http://note/" + Uri.encode(creator) + "/" + Uri.encode(note)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pages.isNotEmpty()) { return pages }
|
if (pages.isNotEmpty()) { return pages }
|
||||||
|
|
||||||
val docString = document.toString()
|
val docString = document.toString()
|
||||||
|
@ -354,102 +299,5 @@ open class Webtoons(
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val URL_SEARCH_PREFIX = "url:"
|
const val URL_SEARCH_PREFIX = "url:"
|
||||||
private const val SHOW_AUTHORS_NOTES_KEY = "showAuthorsNotes"
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Split off into library file or something, because Webtoons is using the exact same TextInterceptor
|
|
||||||
// src/en/tapastic/src/eu/kanade/tachiyomi/extension/en/tapastic/Tapastic.kt
|
|
||||||
object TextInterceptor : Interceptor {
|
|
||||||
// With help from:
|
|
||||||
// https://github.com/tachiyomiorg/tachiyomi-extensions/pull/13304#issuecomment-1234532897
|
|
||||||
// https://medium.com/over-engineering/drawing-multiline-text-to-canvas-on-android-9b98f0bfa16a
|
|
||||||
|
|
||||||
// Designer values:
|
|
||||||
private const val WIDTH: Int = 1000
|
|
||||||
private const val X_PADDING: Float = 50f
|
|
||||||
private const val Y_PADDING: Float = 25f
|
|
||||||
private const val HEADING_FONT_SIZE: Float = 36f
|
|
||||||
private const val BODY_FONT_SIZE: Float = 30f
|
|
||||||
private const val SPACING_MULT: Float = 1.1f
|
|
||||||
private const val SPACING_ADD: Float = 2f
|
|
||||||
|
|
||||||
// No need to touch this one:
|
|
||||||
private const val HOST = "note"
|
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
|
||||||
val request = chain.request()
|
|
||||||
val url = request.url
|
|
||||||
if (url.host != HOST) return chain.proceed(request)
|
|
||||||
|
|
||||||
val creator = textFixer("Author's Notes from ${url.pathSegments[0]}")
|
|
||||||
val story = textFixer(url.pathSegments[1])
|
|
||||||
|
|
||||||
// Heading
|
|
||||||
val paintHeading = TextPaint().apply {
|
|
||||||
color = Color.BLACK
|
|
||||||
textSize = HEADING_FONT_SIZE
|
|
||||||
typeface = Typeface.DEFAULT_BOLD
|
|
||||||
isAntiAlias = true
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
val heading: StaticLayout = StaticLayout(
|
|
||||||
creator, paintHeading, (WIDTH - 2 * X_PADDING).toInt(),
|
|
||||||
Layout.Alignment.ALIGN_NORMAL, SPACING_MULT, SPACING_ADD, true
|
|
||||||
)
|
|
||||||
|
|
||||||
// Body
|
|
||||||
val paintBody = TextPaint().apply {
|
|
||||||
color = Color.BLACK
|
|
||||||
textSize = BODY_FONT_SIZE
|
|
||||||
typeface = Typeface.DEFAULT
|
|
||||||
isAntiAlias = true
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
val body: StaticLayout = StaticLayout(
|
|
||||||
story, paintBody, (WIDTH - 2 * X_PADDING).toInt(),
|
|
||||||
Layout.Alignment.ALIGN_NORMAL, SPACING_MULT, SPACING_ADD, true
|
|
||||||
)
|
|
||||||
|
|
||||||
// Image building
|
|
||||||
val imgHeight: Int = (heading.height + body.height + 2 * Y_PADDING).toInt()
|
|
||||||
val bitmap: Bitmap = Bitmap.createBitmap(WIDTH, imgHeight, Bitmap.Config.ARGB_8888)
|
|
||||||
val canvas: Canvas = Canvas(bitmap)
|
|
||||||
|
|
||||||
// Image drawing
|
|
||||||
canvas.drawColor(Color.WHITE)
|
|
||||||
heading.draw(canvas, X_PADDING, Y_PADDING)
|
|
||||||
body.draw(canvas, X_PADDING, Y_PADDING + heading.height.toFloat())
|
|
||||||
|
|
||||||
// Image converting & returning
|
|
||||||
val stream: ByteArrayOutputStream = ByteArrayOutputStream()
|
|
||||||
bitmap.compress(Bitmap.CompressFormat.PNG, 0, stream)
|
|
||||||
val responseBody = stream.toByteArray().toResponseBody("image/png".toMediaType())
|
|
||||||
return Response.Builder()
|
|
||||||
.request(request)
|
|
||||||
.protocol(Protocol.HTTP_1_1)
|
|
||||||
.code(200)
|
|
||||||
.message("OK")
|
|
||||||
.body(responseBody)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun textFixer(t: String): String {
|
|
||||||
return t
|
|
||||||
.replace("&", "&")
|
|
||||||
.replace("'", "'")
|
|
||||||
.replace(""", "\"")
|
|
||||||
.replace("<", "<")
|
|
||||||
.replace(">", ">")
|
|
||||||
.replace("\\s*<br>\\s*".toRegex(), "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun StaticLayout.draw(canvas: Canvas, x: Float, y: Float) {
|
|
||||||
canvas.save()
|
|
||||||
canvas.translate(x, y)
|
|
||||||
this.draw(canvas)
|
|
||||||
canvas.restore()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
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 kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
import kotlinx.serialization.json.boolean
|
import kotlinx.serialization.json.boolean
|
||||||
import kotlinx.serialization.json.contentOrNull
|
import kotlinx.serialization.json.contentOrNull
|
||||||
|
@ -24,7 +23,6 @@ import okhttp3.Response
|
||||||
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.Observable
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
open class WebtoonsTranslate(
|
open class WebtoonsTranslate(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
|
@ -42,8 +40,6 @@ open class WebtoonsTranslate(
|
||||||
|
|
||||||
private val pageSize = 24
|
private val pageSize = 24
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
|
||||||
|
|
||||||
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
|
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
|
||||||
.removeAll("Referer")
|
.removeAll("Referer")
|
||||||
.add("Referer", mobileBaseUrl.toString())
|
.add("Referer", mobileBaseUrl.toString())
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
include(":core")
|
include(":core")
|
||||||
|
|
||||||
listOf("dataimage", "unpacker", "cryptoaes").forEach {
|
listOf("dataimage", "unpacker", "cryptoaes", "textinterceptor").forEach {
|
||||||
include(":lib-$it")
|
include(":lib-$it")
|
||||||
project(":lib-$it").projectDir = File("lib/$it")
|
project(":lib-$it").projectDir = File("lib/$it")
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,3 +11,7 @@ ext {
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(':lib-textinterceptor'))
|
||||||
|
}
|
|
@ -2,18 +2,12 @@ package eu.kanade.tachiyomi.extension.en.tapastic
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.Bitmap.CompressFormat
|
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.graphics.Typeface
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.text.Layout
|
|
||||||
import android.text.StaticLayout
|
|
||||||
import android.text.TextPaint
|
|
||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
|
import eu.kanade.tachiyomi.lib.textinterceptor.TextInterceptor
|
||||||
|
import eu.kanade.tachiyomi.lib.textinterceptor.TextInterceptorHelper
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
@ -33,20 +27,15 @@ import okhttp3.CookieJar
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
import okhttp3.Interceptor
|
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Protocol
|
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
|
@ -108,7 +97,7 @@ class Tapastic : ConfigurableSource, ParsedHttpSource() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.addInterceptor(TextInterceptor)
|
.addInterceptor(TextInterceptor())
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
private val preferences: SharedPreferences by lazy {
|
private val preferences: SharedPreferences by lazy {
|
||||||
|
@ -372,7 +361,7 @@ class Tapastic : ConfigurableSource, ParsedHttpSource() {
|
||||||
|
|
||||||
pages = pages + Page(
|
pages = pages + Page(
|
||||||
pages.size, "",
|
pages.size, "",
|
||||||
"http://note/" + Uri.encode(creator) + "/" + Uri.encode(episodeStory)
|
TextInterceptorHelper.createUrl(creator, episodeStory)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -528,100 +517,4 @@ class Tapastic : ConfigurableSource, ParsedHttpSource() {
|
||||||
private const val SHOW_AUTHORS_NOTES_KEY = "showAuthorsNotes"
|
private const val SHOW_AUTHORS_NOTES_KEY = "showAuthorsNotes"
|
||||||
private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0"
|
private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Split off into library file or something, because Webtoons is using the exact same TextInterceptor
|
|
||||||
// multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/webtoons/Webtoons.kt
|
|
||||||
object TextInterceptor : Interceptor {
|
|
||||||
// With help from:
|
|
||||||
// https://github.com/tachiyomiorg/tachiyomi-extensions/pull/13304#issuecomment-1234532897
|
|
||||||
// https://medium.com/over-engineering/drawing-multiline-text-to-canvas-on-android-9b98f0bfa16a
|
|
||||||
|
|
||||||
// Designer values:
|
|
||||||
private const val WIDTH: Int = 1000
|
|
||||||
private const val X_PADDING: Float = 50f
|
|
||||||
private const val Y_PADDING: Float = 25f
|
|
||||||
private const val HEADING_FONT_SIZE: Float = 36f
|
|
||||||
private const val BODY_FONT_SIZE: Float = 30f
|
|
||||||
private const val SPACING_MULT: Float = 1.1f
|
|
||||||
private const val SPACING_ADD: Float = 2f
|
|
||||||
|
|
||||||
// No need to touch this one:
|
|
||||||
private const val HOST = "note"
|
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
|
||||||
val request = chain.request()
|
|
||||||
val url = request.url
|
|
||||||
if (url.host != HOST) return chain.proceed(request)
|
|
||||||
|
|
||||||
val creator = textFixer("Author's Notes from ${url.pathSegments[0]}")
|
|
||||||
val story = textFixer(url.pathSegments[1])
|
|
||||||
|
|
||||||
// Heading
|
|
||||||
val paintHeading = TextPaint().apply {
|
|
||||||
color = Color.BLACK
|
|
||||||
textSize = HEADING_FONT_SIZE
|
|
||||||
typeface = Typeface.DEFAULT_BOLD
|
|
||||||
isAntiAlias = true
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
val heading: StaticLayout = StaticLayout(
|
|
||||||
creator, paintHeading, (WIDTH - 2 * X_PADDING).toInt(),
|
|
||||||
Layout.Alignment.ALIGN_NORMAL, SPACING_MULT, SPACING_ADD, true
|
|
||||||
)
|
|
||||||
|
|
||||||
// Body
|
|
||||||
val paintBody = TextPaint().apply {
|
|
||||||
color = Color.BLACK
|
|
||||||
textSize = BODY_FONT_SIZE
|
|
||||||
typeface = Typeface.DEFAULT
|
|
||||||
isAntiAlias = true
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
val body: StaticLayout = StaticLayout(
|
|
||||||
story, paintBody, (WIDTH - 2 * X_PADDING).toInt(),
|
|
||||||
Layout.Alignment.ALIGN_NORMAL, SPACING_MULT, SPACING_ADD, true
|
|
||||||
)
|
|
||||||
|
|
||||||
// Image building
|
|
||||||
val imgHeight: Int = (heading.height + body.height + 2 * Y_PADDING).toInt()
|
|
||||||
val bitmap: Bitmap = Bitmap.createBitmap(WIDTH, imgHeight, Bitmap.Config.ARGB_8888)
|
|
||||||
val canvas: Canvas = Canvas(bitmap)
|
|
||||||
|
|
||||||
// Image drawing
|
|
||||||
canvas.drawColor(Color.WHITE)
|
|
||||||
heading.draw(canvas, X_PADDING, Y_PADDING)
|
|
||||||
body.draw(canvas, X_PADDING, Y_PADDING + heading.height.toFloat())
|
|
||||||
|
|
||||||
// Image converting & returning
|
|
||||||
val stream: ByteArrayOutputStream = ByteArrayOutputStream()
|
|
||||||
bitmap.compress(CompressFormat.PNG, 0, stream)
|
|
||||||
val responseBody = stream.toByteArray().toResponseBody("image/png".toMediaType())
|
|
||||||
return Response.Builder()
|
|
||||||
.request(request)
|
|
||||||
.protocol(Protocol.HTTP_1_1)
|
|
||||||
.code(200)
|
|
||||||
.message("OK")
|
|
||||||
.body(responseBody)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun textFixer(t: String): String {
|
|
||||||
return t
|
|
||||||
.replace("&", "&")
|
|
||||||
.replace("'", "'")
|
|
||||||
.replace(""", "\"")
|
|
||||||
.replace("<", "<")
|
|
||||||
.replace(">", ">")
|
|
||||||
.replace("\\s*<br>\\s*".toRegex(), "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun StaticLayout.draw(canvas: Canvas, x: Float, y: Float) {
|
|
||||||
canvas.save()
|
|
||||||
canvas.translate(x, y)
|
|
||||||
this.draw(canvas)
|
|
||||||
canvas.restore()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue