diff --git a/src/ja/twi4/AndroidManifest.xml b/src/ja/twi4/AndroidManifest.xml new file mode 100644 index 000000000..30deb7f79 --- /dev/null +++ b/src/ja/twi4/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/src/ja/twi4/build.gradle b/src/ja/twi4/build.gradle new file mode 100644 index 000000000..912782397 --- /dev/null +++ b/src/ja/twi4/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlinx-serialization' + +ext { + extName = 'Twi4' + pkgNameSuffix = 'ja.twi4' + extClass = '.Twi4' + extVersionCode = 1 +} + +apply from: "$rootDir/common.gradle" diff --git a/src/ja/twi4/res/mipmap-hdpi/ic_launcher.png b/src/ja/twi4/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..d524e436b Binary files /dev/null and b/src/ja/twi4/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/ja/twi4/res/mipmap-mdpi/ic_launcher.png b/src/ja/twi4/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..a249fe14e Binary files /dev/null and b/src/ja/twi4/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/ja/twi4/res/mipmap-xhdpi/ic_launcher.png b/src/ja/twi4/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..d47078eff Binary files /dev/null and b/src/ja/twi4/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/ja/twi4/res/mipmap-xxhdpi/ic_launcher.png b/src/ja/twi4/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..96537a943 Binary files /dev/null and b/src/ja/twi4/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/ja/twi4/res/mipmap-xxxhdpi/ic_launcher.png b/src/ja/twi4/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..944cbe873 Binary files /dev/null and b/src/ja/twi4/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/ja/twi4/res/web_hi_res_512.png b/src/ja/twi4/res/web_hi_res_512.png new file mode 100644 index 000000000..1f3654ee2 Binary files /dev/null and b/src/ja/twi4/res/web_hi_res_512.png differ diff --git a/src/ja/twi4/src/eu/kanade/tachiyomi/extension/ja/twi4/Twi4.kt b/src/ja/twi4/src/eu/kanade/tachiyomi/extension/ja/twi4/Twi4.kt new file mode 100644 index 000000000..3dcf1637c --- /dev/null +++ b/src/ja/twi4/src/eu/kanade/tachiyomi/extension/ja/twi4/Twi4.kt @@ -0,0 +1,240 @@ +package eu.kanade.tachiyomi.extension.ja.twi4 + +import android.app.Application +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.asObservableSuccess +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 kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import okhttp3.Headers +import okhttp3.Request +import okhttp3.Response +import org.jsoup.Jsoup +import rx.Observable +import uy.kohesive.injekt.injectLazy + +class Twi4 : HttpSource() { + // The domain sai-zen-sen.jp directs to their main site rather than Twi4. It has to be /comics/twi4 + override val baseUrl: String = "https://sai-zen-sen.jp/comics/twi4/" + override val lang: String = "ja" + override val name: String = "Twi4" + override val supportsLatest: Boolean = false + private val validPageTest: Regex = Regex("/comics/twi4/[a-z]+/works/\\d{4}\\.[0-9a-f]{32}\\.jpg") + private val application: Application by injectLazy() + + private fun getUrlDomain(): String = baseUrl.substring(0, 22) + + private fun getChromeHeaders(): Headers = headersBuilder().add( + "User-Agent", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36" + ).build() + + // Popular manga == All manga in the site + override fun fetchPopularManga(page: Int): Observable { + return client.newCall(popularMangaRequest(page)) + .asObservableSuccess() + .map { response -> + parsePopularMangaRequest(response, page < 2) + } + } + + private fun parsePopularMangaRequest(response: Response, hasNextPage: Boolean): MangasPage { + val doc = Jsoup.parse(response.body?.string()) + val ret = mutableListOf() + // One of the manga is a link to Twi4's zadankai, which is a platform for anyone to post oneshot 4-koma with judges to comment + // It has a completely different page layout and it is pretty much its own "manga site". + // Therefore, for simplicity sake. This extension (or at least this source) will not include that as a "Manga" + val mangas = doc.select("section:not(.zadankai):not([id])") + for (manga in mangas) { + ret.add( + SManga.create().apply { + thumbnail_url = + getUrlDomain() + manga.select("header > div.figgroup > figure > a > img") + .attr("src") + setUrlWithoutDomain( + getUrlDomain() + manga.select("header > div.hgroup > h3 > a").attr("href") + ) + title = manga.select("header > div.hgroup > h3 > a > strong").text() + } + ) + } + return MangasPage(ret, hasNextPage) + } + + // We have to fetch all manga from two different pages + // One from the homepage (which contains all ongoing manga), one from the completed manga page + // The menu at the top relies on JS which JSoup doesn't load + override fun popularMangaRequest(page: Int): Request { + return if (page == 1) { + GET(baseUrl, getChromeHeaders()) + } else { + GET(baseUrl + "completed.html", getChromeHeaders()) + } + } + + override fun mangaDetailsRequest(manga: SManga): Request = + GET(getUrlDomain() + manga.url, getChromeHeaders()) + + override fun mangaDetailsParse(response: Response): SManga { + val document = Jsoup.parse(response.body?.string()) + return SManga.create().apply { + description = + document.select("#introduction > div > div > p").text() + // Determine who are the authors and artists + // 作者, 原作 -> Author (Also the artist) / Original author (Such as light novel adaptation) + // 漫画 -> Artist only + // 提供, etc, etc -> Sponsors, irrelevant stuff + val staffs = document.select("#introduction > div > section > header > div > h3") + for (staff in staffs) { + val role = staff.select("small") + if (role.isEmpty()) + continue + when (role.text().replace(":", "").trim()) { + "作者" -> { + author = staff.select("span").text() + artist = staff.select("span").text() + } + // If 作者 and 原作 appear at the same time, 原作 will overwrite the author field + "原作" -> { + author = staff.select("span").text() + } + "漫画" -> { + artist = staff.select("span").text() + } + } + } + status = SManga.UNKNOWN + } + } + + override fun chapterListRequest(manga: SManga): Request = + GET(getUrlDomain() + manga.url, getChromeHeaders()) + + // They have a