xkcd: add Chinese translation (#9772)

This commit is contained in:
ObserverOfTime 2021-11-09 20:18:24 +02:00 committed by GitHub
parent 5bdb0b1e77
commit d95e9e6fcd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 231 additions and 150 deletions

View File

@ -1,11 +1,12 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'xkcd'
pkgNameSuffix = 'en.xkcd'
extClass = '.Xkcd'
extVersionCode = 10
pkgNameSuffix = 'all.xkcd'
extClass = '.XkcdFactory'
extVersionCode = 11
}
apply from: "$rootDir/common.gradle"

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

View File

@ -0,0 +1,151 @@
package eu.kanade.tachiyomi.extension.all.xkcd
import android.net.Uri.encode
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
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.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Response
import rx.Observable
import java.text.SimpleDateFormat
import java.util.Locale
open class Xkcd(
final override val baseUrl: String,
final override val lang: String,
dateFormat: String = "yyyy-MM-dd"
) : HttpSource() {
final override val name = "xkcd"
final override val supportsLatest = false
protected open val archive = "/archive"
protected open val creator = "Randall Munroe"
protected open val synopsis =
"A webcomic of romance, sarcasm, math and language"
protected open val interactiveText =
"To experience the interactive version of this comic," +
"\nopen it in WebView/browser."
protected open val altTextUrl = LATIN_ALT_TEXT_URL
protected open val chapterListSelector = "#middleContainer > a"
protected open val imageSelector = "#comic > img"
private val dateFormat = SimpleDateFormat(dateFormat, Locale.ROOT)
protected fun String.timestamp() = dateFormat.parse(this)?.time ?: 0L
protected fun String.image() = altTextUrl + "&text=" + encode(this)
protected open fun String.numbered(number: Any) = "$number - $this"
final override fun fetchPopularManga(page: Int) =
SManga.create().apply {
title = name
artist = creator
author = creator
description = synopsis
status = SManga.ONGOING
thumbnail_url = THUMBNAIL_URL
setUrlWithoutDomain(archive)
}.let { Observable.just(MangasPage(listOf(it), false))!! }
final override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
Observable.just(MangasPage(emptyList(), false))!!
final override fun fetchMangaDetails(manga: SManga) =
Observable.just(manga.apply { initialized = true })!!
override fun chapterListParse(response: Response) =
response.asJsoup().select(chapterListSelector).map {
SChapter.create().apply {
url = it.attr("href")
val number = url.removeSurrounding("/")
name = it.text().numbered(number)
chapter_number = number.toFloat()
date_upload = it.attr("title").timestamp()
}
}
override fun pageListParse(response: Response): List<Page> {
// if the img tag is empty or has siblings then it is an interactive comic
val img = response.asJsoup().selectFirst(imageSelector)?.takeIf {
it.nextElementSibling() == null
} ?: return listOf(Page(0, "", interactiveText.image()))
// if an HD image is available it'll be the srcset attribute
val image = when {
!img.hasAttr("srcset") -> img.attr("abs:src")
else -> img.attr("abs:srcset").substringBefore(' ')
}
// create a text image for the alt text
val titleWords = img.attr("alt").split(' ')
val altTextWords = img.attr("title").split(' ')
// TODO: maybe use BreakIterator
val text = buildString {
titleWords.forEachIndexed { i, w ->
if (i != 0 && i % 7 == 0) append("\n")
append(w).append(' ')
}
append("\n\n")
var charCount = 0
altTextWords.forEach { w ->
if (charCount > 25) {
append("\n")
charCount = 0
}
append(w).append(' ')
charCount += w.length + 1
}
}
return listOf(Page(0, "", image), Page(1, "", text.image()))
}
final override fun imageUrlParse(response: Response) =
throw UnsupportedOperationException("Not used")
final override fun latestUpdatesParse(response: Response) =
throw UnsupportedOperationException("Not used")
final override fun latestUpdatesRequest(page: Int) =
throw UnsupportedOperationException("Not used")
final override fun mangaDetailsParse(response: Response) =
throw UnsupportedOperationException("Not used")
final override fun popularMangaParse(response: Response) =
throw UnsupportedOperationException("Not used")
final override fun popularMangaRequest(page: Int) =
throw UnsupportedOperationException("Not used")
final override fun searchMangaParse(response: Response) =
throw UnsupportedOperationException("Not used")
final override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
throw UnsupportedOperationException("Not used")
companion object {
private const val THUMBNAIL_URL =
"https://fakeimg.pl/550x780/ffffff/6e7b91/?font=museo&text=xkcd"
const val LATIN_ALT_TEXT_URL =
"https://fakeimg.pl/1500x2126/ffffff/000000/?font=museo&font_size=42"
const val CJK_ALT_TEXT_URL =
"https://placehold.jp/42/ffffff/000000/1500x2126.png?css=" +
"%7B%22padding%22%3A%22300px%22%2C%22text-align%22%3A%22left%22%7D"
}
}

View File

@ -0,0 +1,11 @@
package eu.kanade.tachiyomi.extension.all.xkcd
import eu.kanade.tachiyomi.extension.all.xkcd.translations.XkcdZH
import eu.kanade.tachiyomi.source.SourceFactory
class XkcdFactory : SourceFactory {
override fun createSources() = listOf(
Xkcd("https://xkcd.com", "en"),
XkcdZH(),
)
}

View File

@ -0,0 +1,65 @@
package eu.kanade.tachiyomi.extension.all.xkcd.translations
import eu.kanade.tachiyomi.extension.all.xkcd.Xkcd
import eu.kanade.tachiyomi.network.GET
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.util.asJsoup
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
class XkcdZH : Xkcd("https://xkcd.tw", "zh", "yyyy-MM-dd HH:mm:ss") {
override val archive = "/api/strips.json"
override val creator = "兰德尔·门罗"
override val synopsis = "這裡翻譯某個關於浪漫、諷刺、數學、以及語言的漫畫"
// Google translated, sorry
override val interactiveText =
"要體驗本漫畫的互動版\n請在WebView/瀏覽器中打開。"
override val altTextUrl = CJK_ALT_TEXT_URL
override val imageSelector = "#content > img:not([id])"
private val json by injectLazy<Json>()
override fun String.numbered(number: Any) = "[$number] $this"
override fun mangaDetailsRequest(manga: SManga) = GET(baseUrl, headers)
override fun chapterListParse(response: Response) =
json.parseToJsonElement(response.body!!.string()).jsonObject.values.map {
val obj = it.jsonObject
val number = obj["id"]!!.jsonPrimitive.content
val title = obj["title"]!!.jsonPrimitive.content
val date = obj["translate_time"]!!.jsonPrimitive.content
SChapter.create().apply {
url = "/$number"
name = title.numbered(number)
chapter_number = number.toFloat()
date_upload = date.timestamp()
}
}
override fun pageListParse(response: Response): List<Page> {
// if img tag is empty then it is an interactive comic
val img = response.asJsoup().selectFirst(imageSelector)
?: return listOf(Page(0, "", interactiveText.image()))
val image = img.attr("abs:src")
// create a text image for the alt text
val text = img.attr("alt") + "\n\n" + img.attr("title")
return listOf(Page(0, "", image), Page(1, "", text.image()))
}
override val chapterListSelector: String
get() = throw UnsupportedOperationException("Not used")
}

View File

@ -1,147 +0,0 @@
package eu.kanade.tachiyomi.extension.en.xkcd
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
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.ParsedHttpSource
import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import java.text.SimpleDateFormat
import java.util.Locale
class Xkcd : ParsedHttpSource() {
override val name = "xkcd"
override val baseUrl = "https://xkcd.com"
override val lang = "en"
override val supportsLatest = false
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
val manga = SManga.create()
manga.setUrlWithoutDomain("/archive")
manga.title = "xkcd"
manga.artist = "Randall Munroe"
manga.author = "Randall Munroe"
manga.status = SManga.ONGOING
manga.description = "A webcomic of romance, sarcasm, math and language"
manga.thumbnail_url = thumbnailUrl
return Observable.just(MangasPage(arrayListOf(manga), false))
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> = Observable.just(MangasPage(emptyList(), false))
override fun fetchMangaDetails(manga: SManga): Observable<SManga> = fetchPopularManga(1)
.map { it.mangas.first().apply { initialized = true } }
override fun chapterListSelector() = "div#middleContainer.box a"
override fun chapterFromElement(element: Element): SChapter {
val chapter = SChapter.create()
chapter.url = element.attr("href")
val number = chapter.url.removeSurrounding("/")
chapter.chapter_number = number.toFloat()
chapter.name = number + " - " + element.text()
chapter.date_upload = element.attr("title").let {
SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(it)?.time ?: 0L
}
return chapter
}
override fun pageListParse(document: Document): List<Page> {
val titleWords: Sequence<String>
val altTextWords: Sequence<String>
val interactiveText = """
|To experience the interactive version of this comic,
|open it in WebView/browser.
""".trimMargin("|")
.replace("\n", "%0A")
.replace(" ", "%20")
// transforming filename from info.0.json isn't guaranteed to work, stick to html
// if an HD image is available it'll be the srcset attribute
// if img tag is empty then it is an interactive comic viewable only in browser
val image = document.select("div#comic img").let {
when {
it == null || it.isEmpty() -> baseAltTextUrl + interactiveText + baseAltTextPostUrl
it.hasAttr("srcset") -> it.attr("abs:srcset").substringBefore(" ")
else -> it.attr("abs:src")
}
}
// create a text image for the alt text
document.select("div#comic img").let {
titleWords = it.attr("alt").splitToSequence(" ")
altTextWords = it.attr("title").splitToSequence(" ")
}
val builder = StringBuilder()
var count = 0
for (i in titleWords) {
if (count != 0 && count.rem(7) == 0) {
builder.append("%0A")
}
builder.append(i).append("+")
count++
}
builder.append("%0A%0A")
var charCount = 0
for (i in altTextWords) {
if (charCount > 25) {
builder.append("%0A")
charCount = 0
}
builder.append(i).append("+")
charCount += i.length + 1
}
return listOf(Page(0, "", image), Page(1, "", baseAltTextUrl + builder.toString() + baseAltTextPostUrl))
}
override fun imageUrlRequest(page: Page) = GET(page.url)
override fun imageUrlParse(document: Document) = throw Exception("Not used")
override fun popularMangaSelector(): String = throw Exception("Not used")
override fun searchMangaFromElement(element: Element): SManga = throw Exception("Not used")
override fun searchMangaNextPageSelector(): String? = throw Exception("Not used")
override fun searchMangaSelector(): String = throw Exception("Not used")
override fun popularMangaRequest(page: Int): Request = throw Exception("Not used")
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = throw Exception("Not used")
override fun popularMangaNextPageSelector(): String? = throw Exception("Not used")
override fun popularMangaFromElement(element: Element): SManga = throw Exception("Not used")
override fun mangaDetailsParse(document: Document): SManga = throw Exception("Not used")
override fun latestUpdatesNextPageSelector(): String? = throw Exception("Not used")
override fun latestUpdatesFromElement(element: Element): SManga = throw Exception("Not used")
override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not used")
override fun latestUpdatesSelector(): String = throw Exception("Not used")
companion object {
const val thumbnailUrl = "https://fakeimg.pl/550x780/ffffff/6E7B91/?text=xkcd&font=museo"
const val baseAltTextUrl = "https://fakeimg.pl/1500x2126/ffffff/000000/?text="
const val baseAltTextPostUrl = "&font_size=42&font=museo"
}
}