diff --git a/src/en/xkcd/AndroidManifest.xml b/src/all/xkcd/AndroidManifest.xml similarity index 100% rename from src/en/xkcd/AndroidManifest.xml rename to src/all/xkcd/AndroidManifest.xml diff --git a/src/en/xkcd/build.gradle b/src/all/xkcd/build.gradle similarity index 53% rename from src/en/xkcd/build.gradle rename to src/all/xkcd/build.gradle index 76fb1542a..c21f2a43f 100644 --- a/src/en/xkcd/build.gradle +++ b/src/all/xkcd/build.gradle @@ -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" diff --git a/src/en/xkcd/res/mipmap-hdpi/ic_launcher.png b/src/all/xkcd/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from src/en/xkcd/res/mipmap-hdpi/ic_launcher.png rename to src/all/xkcd/res/mipmap-hdpi/ic_launcher.png diff --git a/src/en/xkcd/res/mipmap-mdpi/ic_launcher.png b/src/all/xkcd/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from src/en/xkcd/res/mipmap-mdpi/ic_launcher.png rename to src/all/xkcd/res/mipmap-mdpi/ic_launcher.png diff --git a/src/en/xkcd/res/mipmap-xhdpi/ic_launcher.png b/src/all/xkcd/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from src/en/xkcd/res/mipmap-xhdpi/ic_launcher.png rename to src/all/xkcd/res/mipmap-xhdpi/ic_launcher.png diff --git a/src/en/xkcd/res/mipmap-xxhdpi/ic_launcher.png b/src/all/xkcd/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from src/en/xkcd/res/mipmap-xxhdpi/ic_launcher.png rename to src/all/xkcd/res/mipmap-xxhdpi/ic_launcher.png diff --git a/src/en/xkcd/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/xkcd/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from src/en/xkcd/res/mipmap-xxxhdpi/ic_launcher.png rename to src/all/xkcd/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/src/en/xkcd/res/web_hi_res_512.png b/src/all/xkcd/res/web_hi_res_512.png similarity index 100% rename from src/en/xkcd/res/web_hi_res_512.png rename to src/all/xkcd/res/web_hi_res_512.png diff --git a/src/all/xkcd/src/eu/kanade/tachiyomi/extension/all/xkcd/Xkcd.kt b/src/all/xkcd/src/eu/kanade/tachiyomi/extension/all/xkcd/Xkcd.kt new file mode 100644 index 000000000..2a8220526 --- /dev/null +++ b/src/all/xkcd/src/eu/kanade/tachiyomi/extension/all/xkcd/Xkcd.kt @@ -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 { + // 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" + } +} diff --git a/src/all/xkcd/src/eu/kanade/tachiyomi/extension/all/xkcd/XkcdFactory.kt b/src/all/xkcd/src/eu/kanade/tachiyomi/extension/all/xkcd/XkcdFactory.kt new file mode 100644 index 000000000..395d10cef --- /dev/null +++ b/src/all/xkcd/src/eu/kanade/tachiyomi/extension/all/xkcd/XkcdFactory.kt @@ -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(), + ) +} diff --git a/src/all/xkcd/src/eu/kanade/tachiyomi/extension/all/xkcd/translations/XkcdZH.kt b/src/all/xkcd/src/eu/kanade/tachiyomi/extension/all/xkcd/translations/XkcdZH.kt new file mode 100644 index 000000000..9ed173179 --- /dev/null +++ b/src/all/xkcd/src/eu/kanade/tachiyomi/extension/all/xkcd/translations/XkcdZH.kt @@ -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() + + 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 { + // 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") +} diff --git a/src/en/xkcd/src/eu/kanade/tachiyomi/extension/en/xkcd/Xkcd.kt b/src/en/xkcd/src/eu/kanade/tachiyomi/extension/en/xkcd/Xkcd.kt deleted file mode 100644 index eeae3f4ce..000000000 --- a/src/en/xkcd/src/eu/kanade/tachiyomi/extension/en/xkcd/Xkcd.kt +++ /dev/null @@ -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 { - 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 = Observable.just(MangasPage(emptyList(), false)) - - override fun fetchMangaDetails(manga: SManga): Observable = 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 { - val titleWords: Sequence - val altTextWords: Sequence - 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" - } -}