diff --git a/src/all/toomics/build.gradle b/src/all/toomics/build.gradle new file mode 100644 index 000000000..d39945805 --- /dev/null +++ b/src/all/toomics/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + appName = 'Tachiyomi: Toomics' + pkgNameSuffix = 'all.toomics' + extClass = '.ToomicsFactory' + extVersionCode = 1 + libVersion = '1.2' +} + +apply from: "$rootDir/common.gradle" diff --git a/src/all/toomics/res/mipmap-hdpi/ic_launcher.png b/src/all/toomics/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..3999f54bd Binary files /dev/null and b/src/all/toomics/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/all/toomics/res/mipmap-mdpi/ic_launcher.png b/src/all/toomics/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..8a0a435ec Binary files /dev/null and b/src/all/toomics/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/all/toomics/res/mipmap-xhdpi/ic_launcher.png b/src/all/toomics/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..5bffc34fd Binary files /dev/null and b/src/all/toomics/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/all/toomics/res/mipmap-xxhdpi/ic_launcher.png b/src/all/toomics/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..371fc8a42 Binary files /dev/null and b/src/all/toomics/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/all/toomics/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/toomics/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..ecb96fa07 Binary files /dev/null and b/src/all/toomics/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/all/toomics/res/web_hi_res_512.png b/src/all/toomics/res/web_hi_res_512.png new file mode 100644 index 000000000..320869867 Binary files /dev/null and b/src/all/toomics/res/web_hi_res_512.png differ diff --git a/src/all/toomics/src/eu/kanade/tachiyomi/extension/all/toomics/ToomicsFactory.kt b/src/all/toomics/src/eu/kanade/tachiyomi/extension/all/toomics/ToomicsFactory.kt new file mode 100644 index 000000000..2f1482383 --- /dev/null +++ b/src/all/toomics/src/eu/kanade/tachiyomi/extension/all/toomics/ToomicsFactory.kt @@ -0,0 +1,30 @@ +package eu.kanade.tachiyomi.extension.all.toomics + +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.SourceFactory +import java.text.SimpleDateFormat +import java.util.* + +class ToomicsFactory : SourceFactory { + override fun createSources(): List = listOf( + ToomicsEnglish(), + ToomicsSimplifiedChinese(), + ToomicsTraditionalChinese(), + ToomicsSpanishLA(), + ToomicsSpanish(), + ToomicsItalian(), + ToomicsGerman(), + ToomicsFrench(), + ToomicsPortuguese() + ) +} + +class ToomicsEnglish : ToomicsGlobal("en", SimpleDateFormat("MMM dd, yyyy", Locale.ENGLISH)) +class ToomicsSimplifiedChinese : ToomicsGlobal("sc", SimpleDateFormat("yyyy.MM.dd", Locale.SIMPLIFIED_CHINESE), "zh", "简体") +class ToomicsTraditionalChinese : ToomicsGlobal("tc", SimpleDateFormat("yyyy.MM.dd", Locale.TRADITIONAL_CHINESE), "zh", "繁體") +class ToomicsSpanishLA : ToomicsGlobal("mx", SimpleDateFormat("d MMM, yyyy", Locale("es", "419")), "es", "LA") +class ToomicsSpanish : ToomicsGlobal("es", SimpleDateFormat("d MMM, yyyy", Locale("es", "419")), "es") +class ToomicsItalian : ToomicsGlobal("it", SimpleDateFormat("d MMM, yyyy", Locale.ITALIAN)) +class ToomicsGerman : ToomicsGlobal("de", SimpleDateFormat("d. MMM yyyy", Locale.GERMAN)) +class ToomicsFrench : ToomicsGlobal("fr", SimpleDateFormat("dd MMM. yyyy", Locale.ENGLISH)) +class ToomicsPortuguese : ToomicsGlobal("por", SimpleDateFormat("d 'de' MMM 'de' yyyy", Locale("pt", "BR")), "pt") diff --git a/src/all/toomics/src/eu/kanade/tachiyomi/extension/all/toomics/ToomicsGlobal.kt b/src/all/toomics/src/eu/kanade/tachiyomi/extension/all/toomics/ToomicsGlobal.kt new file mode 100644 index 000000000..30bb4d840 --- /dev/null +++ b/src/all/toomics/src/eu/kanade/tachiyomi/extension/all/toomics/ToomicsGlobal.kt @@ -0,0 +1,153 @@ +package eu.kanade.tachiyomi.extension.all.toomics + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.source.model.* +import eu.kanade.tachiyomi.source.online.ParsedHttpSource +import okhttp3.* +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import rx.Observable +import java.net.URLDecoder +import java.text.ParseException +import java.text.SimpleDateFormat + +abstract class ToomicsGlobal(private val siteLang: String, + private val dateFormat: SimpleDateFormat, + override val lang: String = siteLang, + displayName: String = "") : ParsedHttpSource() { + + override val name = "Toomics (Only free chapters)" + (if (displayName.isNotEmpty()) " ($displayName)" else "") + + override val baseUrl = "https://global.toomics.com" + + override val supportsLatest = true + + override fun headersBuilder(): Headers.Builder = Headers.Builder() + .add("Referer", "$baseUrl/$siteLang") + .add("User-Agent", USER_AGENT) + + override fun popularMangaRequest(page: Int): Request { + return GET("$baseUrl/$siteLang/index/set_display/?display=A&return=/$siteLang", headers) + } + + // ToomicsGlobal does not have a popular list, so use recommended instead. + override fun popularMangaSelector(): String = "div.section_most div.list_wrap ul.slick_item li div a" + + override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply { + title = element.select("div.visual div.main_text h4.title").text() + thumbnail_url = element.select("div.visual > p > img").attr("src") + url = element.attr("href") + } + + override fun popularMangaNextPageSelector(): String? = null + + override fun latestUpdatesRequest(page: Int): Request { + return GET("$baseUrl/$siteLang/index/set_display/?display=A&return=/$siteLang", headers) + } + + override fun latestUpdatesSelector(): String = "div#section_todayup div.list_wrap ul.slick_item li div a" + + override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element) + + override fun latestUpdatesNextPageSelector(): String? = null + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val newHeaders = headers.newBuilder() + .set("X-Requested-With", "XMLHttpRequest") + .build() + + val form = FormBody.Builder() + .add("toonData", query) + .build() + + return POST("$baseUrl/$siteLang/webtoon/ajax_search", newHeaders, form) + } + + override fun searchMangaSelector(): String = "div.recently_list ul li a" + + override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply { + title = element.select("div.search_box dl dt span.title").text() + thumbnail_url = element.select("div.search_box p.img img").attr("src") + + // When the family mode is off, the url is encoded and is available in the onclick. + val toonId = element.attr("onclick") + .substringAfter("Base.setDisplay('A', '") + .substringBefore("'") + .let { URLDecoder.decode(it, "UTF-8") } + .substringAfter("?toon=") + .substringBefore("&") + url = "/$siteLang/webtoon/episode/toon/$toonId" + } + + override fun searchMangaNextPageSelector(): String? = null + + override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply { + val header = document.select("#glo_contents header.ep-cover_ch div.title_content") + + title = header.select("h1").text() + author = header.select("p.type_box span.writer").text() + artist = header.select("p.type_box span.writer").text() + genre = header.select("p.type_box span.type").text().replace("/", ",") + description = header.select("h2").text() + thumbnail_url = document.select("head meta[property='og:image']").attr("content") + } + + override fun fetchChapterList(manga: SManga): Observable> { + return super.fetchChapterList(manga) + .map { it.reversed() } + } + + override fun chapterListSelector(): String = "section.ep-body ol.list-ep li.normal_ep a:not([onclick*='login'])" + + override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply { + val num = element.select("div.cell-num span.num").text() + val numText = if (num.isNotEmpty()) "$num - " else "" + + name = numText + element.select("div.cell-title strong.tit").first().ownText() + chapter_number = num.toFloatOrNull() ?: 0f + date_upload = parseChapterDate(element.select("div.cell-time time").text()!!) + scanlator = "Toomics" + url = "/$siteLang" + element.attr("onclick") + .substringAfter("'/$siteLang") + .substringBefore("'") + } + + override fun pageListRequest(chapter: SChapter): Request { + val toonId = chapter.url.substringAfterLast("toon/") + val newHeaders = headers.newBuilder() + .set("Referer", "$baseUrl/$siteLang/webtoon/episode/toon/$toonId") + .build() + + return GET(baseUrl + chapter.url, newHeaders) + } + + override fun pageListParse(document: Document): List { + val url = document.select("head meta[property='og:url']").attr("content") + + return document.select("main.viewer-body div.viewer-imgs div img") + .mapIndexed { i, el -> Page(i, url, el.attr("data-original"))} + } + + override fun imageUrlParse(document: Document): String = "" + + override fun imageRequest(page: Page): Request { + val newHeaders = headers.newBuilder() + .set("Referer", page.url) + .build() + + return GET(page.imageUrl!!, newHeaders) + } + + private fun parseChapterDate(date: String) : Long { + return try { + dateFormat.parse(date).time + } catch (e: ParseException) { + 0L + } + } + + companion object { + private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.92 Safari/537.36" + } +}