NewToki/ManaToki: update to v1.3.24 (#13546)
* NewToki/ManaToki: update to v1.3.24 * add Korean translation by moonji * simplify chapter regex * update Korean translation by manatails * remove manga title from chapter name * decrement fallback domain number by 1 * use parsed title in chapter list parse Co-authored-by: moon <jamiejakie@gmail.com> Co-authored-by: Pierre Kim <admin@manateeshome.com>
This commit is contained in:
		
							parent
							
								
									229dd1c9be
								
							
						
					
					
						commit
						55db729814
					
				@ -4,8 +4,9 @@ apply plugin: 'kotlin-android'
 | 
				
			|||||||
ext {
 | 
					ext {
 | 
				
			||||||
    extName = 'NewToki / ManaToki'
 | 
					    extName = 'NewToki / ManaToki'
 | 
				
			||||||
    pkgNameSuffix = 'ko.newtoki'
 | 
					    pkgNameSuffix = 'ko.newtoki'
 | 
				
			||||||
    extClass = '.NewTokiFactory'
 | 
					    extClass = '.TokiFactory'
 | 
				
			||||||
    extVersionCode = 23
 | 
					    extVersionCode = 24
 | 
				
			||||||
 | 
					    isNsfw = true
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
apply from: "$rootDir/common.gradle"
 | 
					apply from: "$rootDir/common.gradle"
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										56
									
								
								src/ko/newtoki/domain_log.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								src/ko/newtoki/domain_log.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,56 @@
 | 
				
			|||||||
 | 
					1. Visit https://t.me/s/newtoki3 and load all messages.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					2. Run
 | 
				
			||||||
 | 
					   ```js
 | 
				
			||||||
 | 
					   $$(".tgme_widget_message_service_date").map(e => e.innerText)
 | 
				
			||||||
 | 
					   ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					3. Paste the dates into a spreadsheet.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					4. Remove duplicates.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The number is changed to 154 on 2022-09-21.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					|       Date | Days | Average |
 | 
				
			||||||
 | 
					| ---------: | ---: | ------: |
 | 
				
			||||||
 | 
					| 2022-09-21 |   12 |    12.0 |
 | 
				
			||||||
 | 
					| 2022-09-09 |    7 |     9.5 |
 | 
				
			||||||
 | 
					| 2022-09-02 |    8 |     9.0 |
 | 
				
			||||||
 | 
					| 2022-08-25 |    6 |     8.3 |
 | 
				
			||||||
 | 
					| 2022-08-19 |   11 |     8.8 |
 | 
				
			||||||
 | 
					| 2022-08-08 |   10 |     9.0 |
 | 
				
			||||||
 | 
					| 2022-07-29 |    8 |     8.9 |
 | 
				
			||||||
 | 
					| 2022-07-21 |    9 |     8.9 |
 | 
				
			||||||
 | 
					| 2022-07-12 |    7 |     8.7 |
 | 
				
			||||||
 | 
					| 2022-07-05 |   11 |     8.9 |
 | 
				
			||||||
 | 
					| 2022-06-24 |    6 |     8.6 |
 | 
				
			||||||
 | 
					| 2022-06-18 |    8 |     8.6 |
 | 
				
			||||||
 | 
					| 2022-06-10 |    3 |     8.2 |
 | 
				
			||||||
 | 
					| 2022-06-07 |   11 |     8.4 |
 | 
				
			||||||
 | 
					| 2022-05-27 |    6 |     8.2 |
 | 
				
			||||||
 | 
					| 2022-05-21 |   11 |     8.4 |
 | 
				
			||||||
 | 
					| 2022-05-10 |   11 |     8.5 |
 | 
				
			||||||
 | 
					| 2022-04-29 |    6 |     8.4 |
 | 
				
			||||||
 | 
					| 2022-04-23 |    8 |     8.4 |
 | 
				
			||||||
 | 
					| 2022-04-15 |    6 |     8.3 |
 | 
				
			||||||
 | 
					| 2022-04-09 |   10 |     8.3 |
 | 
				
			||||||
 | 
					| 2022-03-30 |   11 |     8.5 |
 | 
				
			||||||
 | 
					| 2022-03-19 |    9 |     8.5 |
 | 
				
			||||||
 | 
					| 2022-03-10 |   14 |     8.7 |
 | 
				
			||||||
 | 
					| 2022-02-24 |   12 |     8.8 |
 | 
				
			||||||
 | 
					| 2022-02-12 |   15 |     9.1 |
 | 
				
			||||||
 | 
					| 2022-01-28 |    9 |     9.1 |
 | 
				
			||||||
 | 
					| 2022-01-19 |    9 |     9.1 |
 | 
				
			||||||
 | 
					| 2022-01-10 |   17 |     9.3 |
 | 
				
			||||||
 | 
					| 2021-12-24 |    7 |     9.3 |
 | 
				
			||||||
 | 
					| 2021-12-17 |    6 |     9.2 |
 | 
				
			||||||
 | 
					| 2021-12-11 |    8 |     9.1 |
 | 
				
			||||||
 | 
					| 2021-12-03 |    9 |     9.1 |
 | 
				
			||||||
 | 
					| 2021-11-24 |    8 |     9.1 |
 | 
				
			||||||
 | 
					| 2021-11-16 |   11 |     9.1 |
 | 
				
			||||||
 | 
					| 2021-11-05 |    7 |     9.1 |
 | 
				
			||||||
 | 
					| 2021-10-29 |    9 |     9.1 |
 | 
				
			||||||
 | 
					| 2021-10-20 |   12 |     9.2 |
 | 
				
			||||||
 | 
					| 2021-10-08 |    8 |     9.1 |
 | 
				
			||||||
 | 
					| 2021-09-30 |   15 |     9.3 |
 | 
				
			||||||
 | 
					| 2021-09-15 |      |         |
 | 
				
			||||||
@ -0,0 +1,89 @@
 | 
				
			|||||||
 | 
					package eu.kanade.tachiyomi.extension.ko.newtoki
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.util.Log
 | 
				
			||||||
 | 
					import eu.kanade.tachiyomi.network.GET
 | 
				
			||||||
 | 
					import eu.kanade.tachiyomi.util.asJsoup
 | 
				
			||||||
 | 
					import okhttp3.Interceptor
 | 
				
			||||||
 | 
					import okhttp3.Response
 | 
				
			||||||
 | 
					import java.io.IOException
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Source changes domain names every few days (e.g. newtoki31.net to newtoki32.net)
 | 
				
			||||||
 | 
					 * The domain name was newtoki32 on 2019-11-14, this attempts to match the rate at which the domain changes
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * Since 2020-09-20, They changed manga side to Manatoki.
 | 
				
			||||||
 | 
					 * It was merged after shutdown of ManaMoa.
 | 
				
			||||||
 | 
					 * This is by the head of Manamoa, as they decided to move to Newtoki.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * Updated on 2022-09-21, see `domain_log.md`.
 | 
				
			||||||
 | 
					 * To avoid going too fast and to utilize redirections,
 | 
				
			||||||
 | 
					 * the number is decremented by 1 initially,
 | 
				
			||||||
 | 
					 * and increments every 9.2 days which is a bit slower than the average.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					val fallbackDomainNumber get() = (154 - 1) + ((System.currentTimeMillis() - 1663723150_000) / 794880_000).toInt()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var domainNumber = ""
 | 
				
			||||||
 | 
					    get() {
 | 
				
			||||||
 | 
					        val currentValue = field
 | 
				
			||||||
 | 
					        if (currentValue.isNotEmpty()) return currentValue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val prefValue = newTokiPreferences.domainNumber
 | 
				
			||||||
 | 
					        if (prefValue.isNotEmpty()) {
 | 
				
			||||||
 | 
					            field = prefValue
 | 
				
			||||||
 | 
					            return prefValue
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val fallback = fallbackDomainNumber.toString()
 | 
				
			||||||
 | 
					        domainNumber = fallback
 | 
				
			||||||
 | 
					        return fallback
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    set(value) {
 | 
				
			||||||
 | 
					        for (preference in arrayOf(manaTokiPreferences, newTokiPreferences)) {
 | 
				
			||||||
 | 
					            preference.domainNumber = value
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        field = value
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					object DomainInterceptor : Interceptor {
 | 
				
			||||||
 | 
					    override fun intercept(chain: Interceptor.Chain): Response {
 | 
				
			||||||
 | 
					        val request = chain.request()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val response = try {
 | 
				
			||||||
 | 
					            chain.proceed(request)
 | 
				
			||||||
 | 
					        } catch (e: IOException) {
 | 
				
			||||||
 | 
					            if (chain.call().isCanceled()) throw e
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            val newDomainNumber = try {
 | 
				
			||||||
 | 
					                val document = chain.proceed(GET("https://t.me/s/newtoki3")).asJsoup()
 | 
				
			||||||
 | 
					                val description = document.selectFirst("meta[property=og:description]").attr("content")
 | 
				
			||||||
 | 
					                numberRegex.find(description)!!.value
 | 
				
			||||||
 | 
					            } catch (_: Throwable) {
 | 
				
			||||||
 | 
					                fallbackDomainNumber.toString()
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            domainNumber = newDomainNumber
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            val url = request.url
 | 
				
			||||||
 | 
					            val newHost = numberRegex.replaceFirst(url.host, newDomainNumber)
 | 
				
			||||||
 | 
					            val newUrl = url.newBuilder().host(newHost).build()
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                chain.proceed(request.newBuilder().url(newUrl).build())
 | 
				
			||||||
 | 
					            } catch (e: IOException) {
 | 
				
			||||||
 | 
					                Log.e("NewToki", "failed to fetch $newUrl", e)
 | 
				
			||||||
 | 
					                throw IOException(editDomainNumber(), e)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (response.priorResponse == null) return response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val newUrl = response.request.url
 | 
				
			||||||
 | 
					        if ("captcha" in newUrl.toString()) throw IOException(solveCaptcha())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val newHost = newUrl.host
 | 
				
			||||||
 | 
					        if (newHost.startsWith(MANATOKI_PREFIX) || newHost.startsWith(NEWTOKI_PREFIX)) {
 | 
				
			||||||
 | 
					            numberRegex.find(newHost)?.run { domainNumber = value }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return response
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private val numberRegex by lazy { Regex("""\d+""") }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,97 +1,32 @@
 | 
				
			|||||||
package eu.kanade.tachiyomi.extension.ko.newtoki
 | 
					package eu.kanade.tachiyomi.extension.ko.newtoki
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import eu.kanade.tachiyomi.network.GET
 | 
					import eu.kanade.tachiyomi.network.GET
 | 
				
			||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
 | 
					 | 
				
			||||||
import eu.kanade.tachiyomi.source.model.Filter
 | 
					import eu.kanade.tachiyomi.source.model.Filter
 | 
				
			||||||
import eu.kanade.tachiyomi.source.model.FilterList
 | 
					import eu.kanade.tachiyomi.source.model.FilterList
 | 
				
			||||||
import eu.kanade.tachiyomi.source.model.MangasPage
 | 
					 | 
				
			||||||
import eu.kanade.tachiyomi.source.model.SManga
 | 
					import eu.kanade.tachiyomi.source.model.SManga
 | 
				
			||||||
import eu.kanade.tachiyomi.util.asJsoup
 | 
					 | 
				
			||||||
import okhttp3.CacheControl
 | 
					 | 
				
			||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
 | 
					import okhttp3.HttpUrl.Companion.toHttpUrl
 | 
				
			||||||
import okhttp3.Request
 | 
					import okhttp3.Request
 | 
				
			||||||
import okhttp3.Response
 | 
					 | 
				
			||||||
import org.jsoup.nodes.Element
 | 
					import org.jsoup.nodes.Element
 | 
				
			||||||
import rx.Observable
 | 
					 | 
				
			||||||
import java.util.concurrent.TimeUnit
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
/*
 | 
					/*
 | 
				
			||||||
 * ManaToki is too big to support in a Factory File., So split into separate file.
 | 
					 * ManaToki is too big to support in a Factory File., So split into separate file.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ManaToki(domainNumber: Long) : NewToki("ManaToki", "https://manatoki$domainNumber.net", "comic") {
 | 
					object ManaToki : NewToki("ManaToki", "comic") {
 | 
				
			||||||
    // / ! DO NOT CHANGE THIS !  Only the site name changed from newtoki.
 | 
					    // / ! DO NOT CHANGE THIS !  Only the site name changed from newtoki.
 | 
				
			||||||
    override val id by lazy { generateSourceId("NewToki", lang, versionId) }
 | 
					    override val id = MANATOKI_ID
 | 
				
			||||||
    override val supportsLatest by lazy { getExperimentLatest() }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    override fun latestUpdatesSelector() = ".media.post-list"
 | 
					    override val baseUrl get() = "https://$MANATOKI_PREFIX$domainNumber.net"
 | 
				
			||||||
    override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/page/update?hid=update&page=$page")
 | 
					 | 
				
			||||||
    override fun latestUpdatesNextPageSelector() = "nav.pg_wrap > .pg > strong"
 | 
					 | 
				
			||||||
    override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
 | 
					 | 
				
			||||||
        // if this is true, Handle Only 10 mangas with accurate Details per page. (Real Latest Page has 70 mangas.)
 | 
					 | 
				
			||||||
        // Else, Parse from Latest page. which is incomplete.
 | 
					 | 
				
			||||||
        val isParseWithDetail = getLatestWithDetail()
 | 
					 | 
				
			||||||
        val reqPage = if (isParseWithDetail) ((page - 1) / 7 + 1) else page
 | 
					 | 
				
			||||||
        return rateLimitedClient.newCall(latestUpdatesRequest(reqPage))
 | 
					 | 
				
			||||||
            .asObservableSuccess()
 | 
					 | 
				
			||||||
            .map { response ->
 | 
					 | 
				
			||||||
                if (isParseWithDetail) latestUpdatesParseWithDetailPage(response, page)
 | 
					 | 
				
			||||||
                else latestUpdatesParseWithLatestPage(response)
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private fun latestUpdatesParseWithDetailPage(response: Response, page: Int): MangasPage {
 | 
					    override val preferences = manaTokiPreferences
 | 
				
			||||||
        val document = response.asJsoup()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // given cache time to prevent repeated lots of request in latest.
 | 
					    private val chapterRegex by lazy { Regex(""" [ \d,~.-]+화$""") }
 | 
				
			||||||
        val cacheControl = CacheControl.Builder().maxAge(28, TimeUnit.DAYS).maxStale(28, TimeUnit.DAYS).build()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        val rm = 70 * ((page - 1) / 7)
 | 
					    fun latestUpdatesElementParse(element: Element): SManga {
 | 
				
			||||||
        val min = (page - 1) * 10 - rm
 | 
					 | 
				
			||||||
        val max = page * 10 - rm
 | 
					 | 
				
			||||||
        val elements = document.select("${latestUpdatesSelector()} p > a").slice(min until max)
 | 
					 | 
				
			||||||
        val mangas = elements.map { element ->
 | 
					 | 
				
			||||||
            val url = element.attr("abs:href")
 | 
					 | 
				
			||||||
            val manga = mangaDetailsParse(rateLimitedClient.newCall(GET(url, cache = cacheControl)).execute())
 | 
					 | 
				
			||||||
            manga.url = getUrlPath(url)
 | 
					 | 
				
			||||||
            manga
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        val hasNextPage = try {
 | 
					 | 
				
			||||||
            !document.select(popularMangaNextPageSelector()).text().contains("10")
 | 
					 | 
				
			||||||
        } catch (_: Exception) {
 | 
					 | 
				
			||||||
            false
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return MangasPage(mangas, hasNextPage)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private fun latestUpdatesParseWithLatestPage(response: Response): MangasPage {
 | 
					 | 
				
			||||||
        val document = response.asJsoup()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        val mangas = document.select(latestUpdatesSelector()).map { element ->
 | 
					 | 
				
			||||||
            latestUpdatesElementParse(element)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        val hasNextPage = try {
 | 
					 | 
				
			||||||
            !document.select(popularMangaNextPageSelector()).text().contains("10")
 | 
					 | 
				
			||||||
        } catch (_: Exception) {
 | 
					 | 
				
			||||||
            false
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return MangasPage(mangas, hasNextPage)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private fun latestUpdatesElementParse(element: Element): SManga {
 | 
					 | 
				
			||||||
        val linkElement = element.select("a.btn-primary")
 | 
					        val linkElement = element.select("a.btn-primary")
 | 
				
			||||||
        val rawTitle = element.select(".post-subject > a").first().ownText().trim()
 | 
					        val rawTitle = element.select(".post-subject > a").first().ownText().trim()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // TODO: Make Clear Regex.
 | 
					 | 
				
			||||||
        val chapterRegex = Regex("""((?:\s+)(?:(?:(?:[0-9]+권)?(?:[0-9]+부)?(?:[0-9]*?시즌[0-9]*?)?)?(?:\s*)(?:(?:[0-9]+)(?:[-.](?:[0-9]+))?)?(?:\s*[~,]\s*)?(?:[0-9]+)(?:[-.](?:[0-9]+))?)(?:화))""")
 | 
					 | 
				
			||||||
        val title = rawTitle.trim().replace(chapterRegex, "")
 | 
					        val title = rawTitle.trim().replace(chapterRegex, "")
 | 
				
			||||||
        // val regexSpecialChapter = Regex("(부록|단편|외전|.+편)")
 | 
					 | 
				
			||||||
        // val lastTitleWord = excludeChapterTitle.split(" ").last()
 | 
					 | 
				
			||||||
        // val title = excludeChapterTitle.replace(lastTitleWord, lastTitleWord.replace(regexSpecialChapter, ""))
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        val manga = SManga.create()
 | 
					        val manga = SManga.create()
 | 
				
			||||||
        manga.url = getUrlPath(linkElement.attr("href"))
 | 
					        manga.url = getUrlPath(linkElement.attr("href"))
 | 
				
			||||||
@ -104,8 +39,6 @@ class ManaToki(domainNumber: Long) : NewToki("ManaToki", "https://manatoki$domai
 | 
				
			|||||||
    override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
 | 
					    override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
 | 
				
			||||||
        val url = ("$baseUrl/comic" + (if (page > 1) "/p$page" else "")).toHttpUrl().newBuilder()
 | 
					        val url = ("$baseUrl/comic" + (if (page > 1) "/p$page" else "")).toHttpUrl().newBuilder()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        val genres = mutableListOf<String>()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        filters.forEach { filter ->
 | 
					        filters.forEach { filter ->
 | 
				
			||||||
            when (filter) {
 | 
					            when (filter) {
 | 
				
			||||||
                is SearchPublishTypeList -> {
 | 
					                is SearchPublishTypeList -> {
 | 
				
			||||||
@ -121,20 +54,17 @@ class ManaToki(domainNumber: Long) : NewToki("ManaToki", "https://manatoki$domai
 | 
				
			|||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                is SearchGenreTypeList -> {
 | 
					                is SearchGenreTypeList -> {
 | 
				
			||||||
                    filter.state.forEach {
 | 
					                    val genres = filter.state.filter { it.state }.joinToString(",") { it.id }
 | 
				
			||||||
                        if (it.state) {
 | 
					                    url.addQueryParameter("tag", genres)
 | 
				
			||||||
                            genres.add(it.id)
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                is SearchSortTypeList -> {
 | 
					                is SearchSortTypeList -> {
 | 
				
			||||||
                    url.addQueryParameter("sst", listOf("wr_datetime", "wr_hit", "wr_good", "as_update")[filter.state])
 | 
					                    val state = filter.state ?: return@forEach
 | 
				
			||||||
 | 
					                    url.addQueryParameter("sst", arrayOf("wr_datetime", "wr_hit", "wr_good", "as_update")[state.index])
 | 
				
			||||||
 | 
					                    url.addQueryParameter("sod", if (state.ascending) "asc" else "desc")
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                is SearchOrderTypeList -> {
 | 
					                else -> {}
 | 
				
			||||||
                    url.addQueryParameter("sod", listOf("desc", "asc")[filter.state])
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -142,14 +72,12 @@ class ManaToki(domainNumber: Long) : NewToki("ManaToki", "https://manatoki$domai
 | 
				
			|||||||
            url.addQueryParameter("stx", query)
 | 
					            url.addQueryParameter("stx", query)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // Remove some filter QueryParams that not working with query
 | 
					            // Remove some filter QueryParams that not working with query
 | 
				
			||||||
            url.setQueryParameter("publish", null)
 | 
					            url.removeAllQueryParameters("publish")
 | 
				
			||||||
            url.setQueryParameter("jaum", null)
 | 
					            url.removeAllQueryParameters("jaum")
 | 
				
			||||||
 | 
					            url.removeAllQueryParameters("tag")
 | 
				
			||||||
            return GET(url.toString())
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        url.addQueryParameter("tag", genres.joinToString(","))
 | 
					        return GET(url.toString(), headers)
 | 
				
			||||||
        return GET(url.toString())
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private class SearchCheckBox(name: String, val id: String = name) : Filter.CheckBox(name)
 | 
					    private class SearchCheckBox(name: String, val id: String = name) : Filter.CheckBox(name)
 | 
				
			||||||
@ -227,7 +155,7 @@ class ManaToki(domainNumber: Long) : NewToki("ManaToki", "https://manatoki$domai
 | 
				
			|||||||
        ).map { SearchCheckBox(it) }
 | 
					        ).map { SearchCheckBox(it) }
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private class SearchSortTypeList : Filter.Select<String>(
 | 
					    private class SearchSortTypeList : Filter.Sort(
 | 
				
			||||||
        "Sort",
 | 
					        "Sort",
 | 
				
			||||||
        arrayOf(
 | 
					        arrayOf(
 | 
				
			||||||
            "기본(날짜순)",
 | 
					            "기본(날짜순)",
 | 
				
			||||||
@ -237,20 +165,12 @@ class ManaToki(domainNumber: Long) : NewToki("ManaToki", "https://manatoki$domai
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private class SearchOrderTypeList : Filter.Select<String>(
 | 
					 | 
				
			||||||
        "Order",
 | 
					 | 
				
			||||||
        arrayOf(
 | 
					 | 
				
			||||||
            "Descending",
 | 
					 | 
				
			||||||
            "Ascending"
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    override fun getFilterList() = FilterList(
 | 
					    override fun getFilterList() = FilterList(
 | 
				
			||||||
        Filter.Header("Some filters can't use with query"),
 | 
					        SearchSortTypeList(),
 | 
				
			||||||
 | 
					        Filter.Separator(),
 | 
				
			||||||
 | 
					        Filter.Header(ignoredForTextSearch()),
 | 
				
			||||||
        SearchPublishTypeList(),
 | 
					        SearchPublishTypeList(),
 | 
				
			||||||
        SearchJaumTypeList(),
 | 
					        SearchJaumTypeList(),
 | 
				
			||||||
        SearchSortTypeList(),
 | 
					 | 
				
			||||||
        SearchOrderTypeList(),
 | 
					 | 
				
			||||||
        SearchGenreTypeList()
 | 
					        SearchGenreTypeList()
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,10 +1,7 @@
 | 
				
			|||||||
package eu.kanade.tachiyomi.extension.ko.newtoki
 | 
					package eu.kanade.tachiyomi.extension.ko.newtoki
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import android.annotation.SuppressLint
 | 
					 | 
				
			||||||
import android.app.Application
 | 
					 | 
				
			||||||
import android.content.SharedPreferences
 | 
					import android.content.SharedPreferences
 | 
				
			||||||
import android.widget.Toast
 | 
					import android.util.Log
 | 
				
			||||||
import eu.kanade.tachiyomi.AppInfo
 | 
					 | 
				
			||||||
import eu.kanade.tachiyomi.network.GET
 | 
					import eu.kanade.tachiyomi.network.GET
 | 
				
			||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
 | 
					import eu.kanade.tachiyomi.network.asObservableSuccess
 | 
				
			||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
 | 
					import eu.kanade.tachiyomi.network.interceptor.rateLimit
 | 
				
			||||||
@ -16,41 +13,50 @@ import eu.kanade.tachiyomi.source.model.SChapter
 | 
				
			|||||||
import eu.kanade.tachiyomi.source.model.SManga
 | 
					import eu.kanade.tachiyomi.source.model.SManga
 | 
				
			||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
 | 
					import eu.kanade.tachiyomi.source.online.ParsedHttpSource
 | 
				
			||||||
import eu.kanade.tachiyomi.util.asJsoup
 | 
					import eu.kanade.tachiyomi.util.asJsoup
 | 
				
			||||||
 | 
					import okhttp3.HttpUrl.Companion.toHttpUrl
 | 
				
			||||||
import okhttp3.OkHttpClient
 | 
					import okhttp3.OkHttpClient
 | 
				
			||||||
import okhttp3.Request
 | 
					 | 
				
			||||||
import okhttp3.Response
 | 
					import okhttp3.Response
 | 
				
			||||||
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 rx.Observable
 | 
					import rx.Observable
 | 
				
			||||||
import uy.kohesive.injekt.Injekt
 | 
					 | 
				
			||||||
import uy.kohesive.injekt.api.get
 | 
					 | 
				
			||||||
import java.net.URI
 | 
					 | 
				
			||||||
import java.net.URISyntaxException
 | 
					 | 
				
			||||||
import java.text.SimpleDateFormat
 | 
					import java.text.SimpleDateFormat
 | 
				
			||||||
import java.util.Calendar
 | 
					import java.util.Calendar
 | 
				
			||||||
 | 
					import java.util.Locale
 | 
				
			||||||
 | 
					import java.util.concurrent.TimeUnit
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * NewToki Source
 | 
					 * NewToki Source
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * Based on https://github.com/gnuboard/gnuboard5
 | 
				
			||||||
 **/
 | 
					 **/
 | 
				
			||||||
open class NewToki(override val name: String, private val defaultBaseUrl: String, private val boardName: String) : ConfigurableSource, ParsedHttpSource() {
 | 
					abstract class NewToki(override val name: String, private val boardName: String) : ConfigurableSource, ParsedHttpSource() {
 | 
				
			||||||
    override val baseUrl by lazy { getPrefBaseUrl() }
 | 
					
 | 
				
			||||||
    override val lang: String = "ko"
 | 
					    override val lang: String = "ko"
 | 
				
			||||||
    override val supportsLatest = true
 | 
					    override val supportsLatest = true
 | 
				
			||||||
    override val client: OkHttpClient = network.cloudflareClient
 | 
					
 | 
				
			||||||
    protected val rateLimitedClient: OkHttpClient by lazy {
 | 
					    override val client: OkHttpClient by lazy {
 | 
				
			||||||
        network.cloudflareClient.newBuilder()
 | 
					        buildClient(withRateLimit = false)
 | 
				
			||||||
            .rateLimit(1, getRateLimitPeriod())
 | 
					 | 
				
			||||||
            .build()
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected val rateLimitedClient: OkHttpClient by lazy {
 | 
				
			||||||
 | 
					        buildClient(withRateLimit = true)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private fun buildClient(withRateLimit: Boolean) =
 | 
				
			||||||
 | 
					        network.cloudflareClient.newBuilder()
 | 
				
			||||||
 | 
					            .apply { if (withRateLimit) rateLimit(1, preferences.rateLimitPeriod.toLong()) }
 | 
				
			||||||
 | 
					            .addInterceptor(DomainInterceptor) // not rate-limited
 | 
				
			||||||
 | 
					            .connectTimeout(10, TimeUnit.SECONDS) // fail fast
 | 
				
			||||||
 | 
					            .build()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    override fun popularMangaSelector() = "div#webtoon-list > ul > li"
 | 
					    override fun popularMangaSelector() = "div#webtoon-list > ul > li"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    override fun popularMangaFromElement(element: Element): SManga {
 | 
					    override fun popularMangaFromElement(element: Element): SManga {
 | 
				
			||||||
        val linkElement = element.getElementsByTag("a").first()
 | 
					        val linkElement = element.getElementsByTag("a").first()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        val manga = SManga.create()
 | 
					        val manga = SManga.create()
 | 
				
			||||||
        manga.setUrlWithoutDomain(linkElement.attr("href").substringBefore("?"))
 | 
					        manga.url = getUrlPath(linkElement.attr("href"))
 | 
				
			||||||
        manga.title = element.select("span.title").first().ownText()
 | 
					        manga.title = element.select("span.title").first().ownText()
 | 
				
			||||||
        manga.thumbnail_url = linkElement.getElementsByTag("img").attr("src")
 | 
					        manga.thumbnail_url = linkElement.getElementsByTag("img").attr("src")
 | 
				
			||||||
        return manga
 | 
					        return manga
 | 
				
			||||||
@ -59,17 +65,16 @@ open class NewToki(override val name: String, private val defaultBaseUrl: String
 | 
				
			|||||||
    override fun popularMangaNextPageSelector() = "ul.pagination > li:last-child:not(.disabled)"
 | 
					    override fun popularMangaNextPageSelector() = "ul.pagination > li:last-child:not(.disabled)"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Do not add page parameter if page is 1 to prevent tracking.
 | 
					    // Do not add page parameter if page is 1 to prevent tracking.
 | 
				
			||||||
    override fun popularMangaRequest(page: Int) = GET("$baseUrl/$boardName" + if (page > 1) "/p$page" else "")
 | 
					    override fun popularMangaRequest(page: Int) = GET("$baseUrl/$boardName" + if (page > 1) "/p$page" else "", headers)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    override fun searchMangaSelector() = popularMangaSelector()
 | 
					    override fun searchMangaSelector() = popularMangaSelector()
 | 
				
			||||||
    override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
 | 
					    override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
 | 
				
			||||||
    override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
 | 
					    override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
 | 
				
			||||||
    override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = GET("$baseUrl/$boardName" + (if (page > 1) "/p$page" else "") + "?stx=$query")
 | 
					 | 
				
			||||||
    override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
 | 
					    override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
 | 
				
			||||||
        return if (query.startsWith(PREFIX_ID_SEARCH)) {
 | 
					        return if (query.startsWith(PREFIX_ID_SEARCH)) {
 | 
				
			||||||
            val realQuery = query.removePrefix(PREFIX_ID_SEARCH)
 | 
					            val realQuery = query.removePrefix(PREFIX_ID_SEARCH)
 | 
				
			||||||
            val urlPath = "/$boardName/$realQuery"
 | 
					            val urlPath = "/$boardName/$realQuery"
 | 
				
			||||||
            rateLimitedClient.newCall(GET("$baseUrl$urlPath"))
 | 
					            rateLimitedClient.newCall(GET("$baseUrl$urlPath", headers))
 | 
				
			||||||
                .asObservableSuccess()
 | 
					                .asObservableSuccess()
 | 
				
			||||||
                .map { response ->
 | 
					                .map { response ->
 | 
				
			||||||
                    // the id is matches any of 'post' from their CMS board.
 | 
					                    // the id is matches any of 'post' from their CMS board.
 | 
				
			||||||
@ -95,7 +100,7 @@ open class NewToki(override val name: String, private val defaultBaseUrl: String
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
            fullListButton?.text()?.contains("전체목록") == true -> { // Check this page is chapter page
 | 
					            fullListButton?.text()?.contains("전체목록") == true -> { // Check this page is chapter page
 | 
				
			||||||
                val url = fullListButton.attr("abs:href")
 | 
					                val url = fullListButton.attr("abs:href")
 | 
				
			||||||
                val details = mangaDetailsParse(rateLimitedClient.newCall(GET(url)).execute())
 | 
					                val details = mangaDetailsParse(rateLimitedClient.newCall(GET(url, headers)).execute())
 | 
				
			||||||
                details.url = getUrlPath(url)
 | 
					                details.url = getUrlPath(url)
 | 
				
			||||||
                listOf(details)
 | 
					                listOf(details)
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@ -113,10 +118,11 @@ open class NewToki(override val name: String, private val defaultBaseUrl: String
 | 
				
			|||||||
        val description = descriptionElement.map {
 | 
					        val description = descriptionElement.map {
 | 
				
			||||||
            it.text().trim()
 | 
					            it.text().trim()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        val prefix = if (isCleanPath(document.location())) "" else needMigration()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        val manga = SManga.create()
 | 
					        val manga = SManga.create()
 | 
				
			||||||
        manga.title = title
 | 
					        manga.title = title
 | 
				
			||||||
        manga.description = description.joinToString("\n")
 | 
					        manga.description = description.joinToString("\n", prefix = prefix)
 | 
				
			||||||
        manga.thumbnail_url = thumbnail
 | 
					        manga.thumbnail_url = thumbnail
 | 
				
			||||||
        descriptionElement.forEach {
 | 
					        descriptionElement.forEach {
 | 
				
			||||||
            val text = it.text()
 | 
					            val text = it.text()
 | 
				
			||||||
@ -148,7 +154,7 @@ open class NewToki(override val name: String, private val defaultBaseUrl: String
 | 
				
			|||||||
        val rawName = linkElement.ownText().trim()
 | 
					        val rawName = linkElement.ownText().trim()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        val chapter = SChapter.create()
 | 
					        val chapter = SChapter.create()
 | 
				
			||||||
        chapter.url = getUrlWithoutDomainWithFallback(linkElement.attr("href"))
 | 
					        chapter.setUrlWithoutDomain(linkElement.attr("href"))
 | 
				
			||||||
        chapter.chapter_number = parseChapterNumber(rawName)
 | 
					        chapter.chapter_number = parseChapterNumber(rawName)
 | 
				
			||||||
        chapter.name = rawName
 | 
					        chapter.name = rawName
 | 
				
			||||||
        chapter.date_upload = parseChapterDate(element.select(".wr-date").last().text().trim())
 | 
					        chapter.date_upload = parseChapterDate(element.select(".wr-date").last().text().trim())
 | 
				
			||||||
@ -160,21 +166,30 @@ open class NewToki(override val name: String, private val defaultBaseUrl: String
 | 
				
			|||||||
            if (name.contains("[단편]")) return 1f
 | 
					            if (name.contains("[단편]")) return 1f
 | 
				
			||||||
            // `특별` means `Special`, so It can be buggy. so pad `편`(Chapter) to prevent false return
 | 
					            // `특별` means `Special`, so It can be buggy. so pad `편`(Chapter) to prevent false return
 | 
				
			||||||
            if (name.contains("번외") || name.contains("특별편")) return -2f
 | 
					            if (name.contains("번외") || name.contains("특별편")) return -2f
 | 
				
			||||||
            val regex = Regex("([0-9]+)(?:[-.]([0-9]+))?(?:화)")
 | 
					            val regex = chapterNumberRegex
 | 
				
			||||||
            val (ch_primal, ch_second) = regex.find(name)!!.destructured
 | 
					            val (ch_primal, ch_second) = regex.find(name)!!.destructured
 | 
				
			||||||
            return (ch_primal + if (ch_second.isBlank()) "" else ".$ch_second").toFloatOrNull()
 | 
					            return (ch_primal + if (ch_second.isBlank()) "" else ".$ch_second").toFloatOrNull()
 | 
				
			||||||
                ?: -1f
 | 
					                ?: -1f
 | 
				
			||||||
        } catch (e: Exception) {
 | 
					        } catch (e: Exception) {
 | 
				
			||||||
            e.printStackTrace()
 | 
					            Log.e("NewToki", "failed to parse chapter number '$name'", e)
 | 
				
			||||||
            return -1f
 | 
					            return -1f
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private fun mangaDetailsParseWithTitleCheck(manga: SManga, document: Document) =
 | 
				
			||||||
 | 
					        mangaDetailsParse(document).apply {
 | 
				
			||||||
 | 
					            // TODO: don't throw when there is download folder rename feature
 | 
				
			||||||
 | 
					            if (manga.description.isNullOrEmpty() && manga.title != title) {
 | 
				
			||||||
 | 
					                throw Exception(titleNotMatch(title))
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
 | 
					    override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
 | 
				
			||||||
        return rateLimitedClient.newCall(mangaDetailsRequest(manga))
 | 
					        return rateLimitedClient.newCall(mangaDetailsRequest(manga))
 | 
				
			||||||
            .asObservableSuccess()
 | 
					            .asObservableSuccess()
 | 
				
			||||||
            .map { response ->
 | 
					            .map { response ->
 | 
				
			||||||
                mangaDetailsParse(response).apply { initialized = true }
 | 
					                val document = response.asJsoup()
 | 
				
			||||||
 | 
					                mangaDetailsParseWithTitleCheck(manga, document).apply { initialized = true }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -182,11 +197,19 @@ open class NewToki(override val name: String, private val defaultBaseUrl: String
 | 
				
			|||||||
        return rateLimitedClient.newCall(chapterListRequest(manga))
 | 
					        return rateLimitedClient.newCall(chapterListRequest(manga))
 | 
				
			||||||
            .asObservableSuccess()
 | 
					            .asObservableSuccess()
 | 
				
			||||||
            .map { response ->
 | 
					            .map { response ->
 | 
				
			||||||
                chapterListParse(response)
 | 
					                val document = response.asJsoup()
 | 
				
			||||||
 | 
					                val title = mangaDetailsParseWithTitleCheck(manga, document).title
 | 
				
			||||||
 | 
					                document.select(chapterListSelector()).map {
 | 
				
			||||||
 | 
					                    chapterFromElement(it).apply {
 | 
				
			||||||
 | 
					                        name = name.removePrefix(title).trimStart()
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @SuppressLint("SimpleDateFormat")
 | 
					    // not thread-safe
 | 
				
			||||||
 | 
					    private val dateFormat by lazy { SimpleDateFormat("yyyy.MM.dd", Locale.ENGLISH) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private fun parseChapterDate(date: String): Long {
 | 
					    private fun parseChapterDate(date: String): Long {
 | 
				
			||||||
        return try {
 | 
					        return try {
 | 
				
			||||||
            if (date.contains(":")) {
 | 
					            if (date.contains(":")) {
 | 
				
			||||||
@ -205,16 +228,14 @@ open class NewToki(override val name: String, private val defaultBaseUrl: String
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                calendar.timeInMillis
 | 
					                calendar.timeInMillis
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                SimpleDateFormat("yyyy.MM.dd").parse(date)?.time ?: 0
 | 
					                dateFormat.parse(date)?.time ?: 0
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } catch (e: Exception) {
 | 
					        } catch (e: Exception) {
 | 
				
			||||||
            e.printStackTrace()
 | 
					            Log.e("NewToki", "failed to parse chapter date '$date'", e)
 | 
				
			||||||
            0
 | 
					            0
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private val htmlDataRegex = Regex("""html_data\+='([^']+)'""")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    override fun pageListParse(document: Document): List<Page> {
 | 
					    override fun pageListParse(document: Document): List<Page> {
 | 
				
			||||||
        val script = document.select("script:containsData(html_data)").firstOrNull()?.data()
 | 
					        val script = document.select("script:containsData(html_data)").firstOrNull()?.data()
 | 
				
			||||||
            ?: throw Exception("data script not found")
 | 
					            ?: throw Exception("data script not found")
 | 
				
			||||||
@ -231,177 +252,37 @@ open class NewToki(override val name: String, private val defaultBaseUrl: String
 | 
				
			|||||||
            .mapIndexed { i, img -> Page(i, "", if (img.hasAttr(dataAttr)) img.attr(dataAttr) else img.attr("abs:content")) }
 | 
					            .mapIndexed { i, img -> Page(i, "", if (img.hasAttr(dataAttr)) img.attr(dataAttr) else img.attr("abs:content")) }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    override fun latestUpdatesSelector() = popularMangaSelector()
 | 
					    override fun latestUpdatesSelector() = ".media.post-list"
 | 
				
			||||||
    override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
 | 
					    override fun latestUpdatesFromElement(element: Element) = ManaToki.latestUpdatesElementParse(element)
 | 
				
			||||||
    override fun latestUpdatesRequest(page: Int) = popularMangaRequest(page)
 | 
					    override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/page/update?hid=update&page=$page", headers)
 | 
				
			||||||
    override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
 | 
					    override fun latestUpdatesNextPageSelector() = ".pg_end"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // We are able to get the image URL directly from the page list
 | 
					    // We are able to get the image URL directly from the page list
 | 
				
			||||||
    override fun imageUrlParse(document: Document) = throw UnsupportedOperationException("This method should not be called!")
 | 
					    override fun imageUrlParse(document: Document) = throw UnsupportedOperationException("This method should not be called!")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    override fun getFilterList() = FilterList()
 | 
					    override fun getFilterList() = FilterList()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private val preferences: SharedPreferences by lazy {
 | 
					    abstract val preferences: SharedPreferences
 | 
				
			||||||
        Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) {
 | 
					    override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) {
 | 
				
			||||||
        val baseUrlPref = androidx.preference.EditTextPreference(screen.context).apply {
 | 
					        getPreferencesInternal(screen.context).map(screen::addPreference)
 | 
				
			||||||
            key = BASE_URL_PREF_TITLE
 | 
					 | 
				
			||||||
            title = BASE_URL_PREF_TITLE
 | 
					 | 
				
			||||||
            summary = BASE_URL_PREF_SUMMARY
 | 
					 | 
				
			||||||
            this.setDefaultValue(defaultBaseUrl)
 | 
					 | 
				
			||||||
            dialogTitle = BASE_URL_PREF_TITLE
 | 
					 | 
				
			||||||
            dialogMessage = "Default: $defaultBaseUrl"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            setOnPreferenceChangeListener { _, newValue ->
 | 
					 | 
				
			||||||
                try {
 | 
					 | 
				
			||||||
                    val res = preferences.edit().putString(BASE_URL_PREF, newValue as String).commit()
 | 
					 | 
				
			||||||
                    Toast.makeText(screen.context, RESTART_TACHIYOMI, Toast.LENGTH_LONG).show()
 | 
					 | 
				
			||||||
                    res
 | 
					 | 
				
			||||||
                } catch (e: Exception) {
 | 
					 | 
				
			||||||
                    e.printStackTrace()
 | 
					 | 
				
			||||||
                    false
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        val latestExperimentPref = androidx.preference.CheckBoxPreference(screen.context).apply {
 | 
					 | 
				
			||||||
            key = EXPERIMENTAL_LATEST_PREF_TITLE
 | 
					 | 
				
			||||||
            title = EXPERIMENTAL_LATEST_PREF_TITLE
 | 
					 | 
				
			||||||
            summary = EXPERIMENTAL_LATEST_PREF_SUMMARY
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            setOnPreferenceChangeListener { _, newValue ->
 | 
					 | 
				
			||||||
                try {
 | 
					 | 
				
			||||||
                    val res = preferences.edit().putBoolean(EXPERIMENTAL_LATEST_PREF, newValue as Boolean).commit()
 | 
					 | 
				
			||||||
                    Toast.makeText(screen.context, RESTART_TACHIYOMI, Toast.LENGTH_LONG).show()
 | 
					 | 
				
			||||||
                    res
 | 
					 | 
				
			||||||
                } catch (e: Exception) {
 | 
					 | 
				
			||||||
                    e.printStackTrace()
 | 
					 | 
				
			||||||
                    false
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        val latestWithDetailPref = androidx.preference.CheckBoxPreference(screen.context).apply {
 | 
					 | 
				
			||||||
            key = EXPERIMENTAL_LATEST_WITH_DETAIL_PREF_TITLE
 | 
					 | 
				
			||||||
            title = EXPERIMENTAL_LATEST_WITH_DETAIL_PREF_TITLE
 | 
					 | 
				
			||||||
            summary = EXPERIMENTAL_LATEST_WITH_DETAIL_PREF_SUMMARY
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            setOnPreferenceChangeListener { _, newValue ->
 | 
					 | 
				
			||||||
                try {
 | 
					 | 
				
			||||||
                    val res = preferences.edit().putBoolean(EXPERIMENTAL_LATEST_WITH_DETAIL_PREF, newValue as Boolean).commit()
 | 
					 | 
				
			||||||
                    // Toast.makeText(screen.context, RESTART_TACHIYOMI, Toast.LENGTH_LONG).show()
 | 
					 | 
				
			||||||
                    res
 | 
					 | 
				
			||||||
                } catch (e: Exception) {
 | 
					 | 
				
			||||||
                    e.printStackTrace()
 | 
					 | 
				
			||||||
                    false
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        val rateLimitPeriodPref = androidx.preference.EditTextPreference(screen.context).apply {
 | 
					 | 
				
			||||||
            key = RATE_LIMIT_PERIOD_PREF_TITLE
 | 
					 | 
				
			||||||
            title = RATE_LIMIT_PERIOD_PREF_TITLE
 | 
					 | 
				
			||||||
            summary = RATE_LIMIT_PERIOD_PREF_SUMMARY
 | 
					 | 
				
			||||||
            this.setDefaultValue(defaultRateLimitPeriod.toString())
 | 
					 | 
				
			||||||
            dialogTitle = RATE_LIMIT_PERIOD_PREF_TITLE
 | 
					 | 
				
			||||||
            dialogMessage = "Min 1 to Max 9, Invalid value will treat as $defaultRateLimitPeriod. Only Integer.\nDefault: $defaultRateLimitPeriod"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            setOnPreferenceChangeListener { _, newValue ->
 | 
					 | 
				
			||||||
                try {
 | 
					 | 
				
			||||||
                    // Make sure to validate the value.
 | 
					 | 
				
			||||||
                    val p = (newValue as String).toLongOrNull(10)
 | 
					 | 
				
			||||||
                    var value = p ?: defaultRateLimitPeriod
 | 
					 | 
				
			||||||
                    if (p == null || value !in 1..9) {
 | 
					 | 
				
			||||||
                        Toast.makeText(screen.context, RATE_LIMIT_PERIOD_PREF_WARNING_INVALID_VALUE, Toast.LENGTH_LONG).show()
 | 
					 | 
				
			||||||
                        value = defaultRateLimitPeriod
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    val res = preferences.edit().putLong(RATE_LIMIT_PERIOD_PREF, value).commit()
 | 
					 | 
				
			||||||
                    Toast.makeText(screen.context, RESTART_TACHIYOMI, Toast.LENGTH_LONG).show()
 | 
					 | 
				
			||||||
                    res
 | 
					 | 
				
			||||||
                } catch (e: Exception) {
 | 
					 | 
				
			||||||
                    e.printStackTrace()
 | 
					 | 
				
			||||||
                    false
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        screen.addPreference(baseUrlPref)
 | 
					 | 
				
			||||||
        if (name == "ManaToki") {
 | 
					 | 
				
			||||||
            screen.addPreference(latestExperimentPref)
 | 
					 | 
				
			||||||
            screen.addPreference(latestWithDetailPref)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        screen.addPreference(rateLimitPeriodPref)
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected fun getUrlPath(orig: String): String {
 | 
					    protected fun getUrlPath(orig: String): String {
 | 
				
			||||||
        return try {
 | 
					        val url = baseUrl.toHttpUrl().resolve(orig) ?: return orig
 | 
				
			||||||
            URI(orig).path
 | 
					        val pathSegments = url.pathSegments
 | 
				
			||||||
        } catch (e: URISyntaxException) {
 | 
					        return "/${pathSegments[0]}/${pathSegments[1]}"
 | 
				
			||||||
            orig
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // This is just replicate of original method but with fallback.
 | 
					    private fun isCleanPath(absUrl: String): Boolean {
 | 
				
			||||||
    protected fun getUrlWithoutDomainWithFallback(orig: String): String {
 | 
					        val url = absUrl.toHttpUrl()
 | 
				
			||||||
        return try {
 | 
					        return url.pathSegments.size == 2 && url.querySize == 0 && url.fragment == null
 | 
				
			||||||
            val uri = URI(orig)
 | 
					 | 
				
			||||||
            var out = uri.path
 | 
					 | 
				
			||||||
            if (uri.query != null) {
 | 
					 | 
				
			||||||
                out += "?" + uri.query
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            if (uri.fragment != null) {
 | 
					 | 
				
			||||||
                out += "#" + uri.fragment
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            out
 | 
					 | 
				
			||||||
        } catch (e: URISyntaxException) {
 | 
					 | 
				
			||||||
            // fallback method. may not work.
 | 
					 | 
				
			||||||
            orig.substringAfter(baseUrl)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private fun getPrefBaseUrl(): String = preferences.getString(BASE_URL_PREF, defaultBaseUrl)!!
 | 
					 | 
				
			||||||
    protected fun getExperimentLatest(): Boolean = preferences.getBoolean(EXPERIMENTAL_LATEST_PREF, false)
 | 
					 | 
				
			||||||
    protected fun getLatestWithDetail(): Boolean = preferences.getBoolean(EXPERIMENTAL_LATEST_WITH_DETAIL_PREF, false)
 | 
					 | 
				
			||||||
    private fun getRateLimitPeriod(): Long = try { // Check again as preference is bit weirdly buggy.
 | 
					 | 
				
			||||||
        val v = preferences.getLong(RATE_LIMIT_PERIOD_PREF, defaultRateLimitPeriod)
 | 
					 | 
				
			||||||
        if (v in 1..9) v else defaultRateLimitPeriod
 | 
					 | 
				
			||||||
    } catch (e: Exception) {
 | 
					 | 
				
			||||||
        defaultRateLimitPeriod
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    companion object {
 | 
					    companion object {
 | 
				
			||||||
        private const val RESTART_TACHIYOMI = "Restart Tachiyomi to apply new setting."
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        private const val BASE_URL_PREF_TITLE = "Override BaseUrl"
 | 
					 | 
				
			||||||
        private val BASE_URL_PREF = "overrideBaseUrl_v${AppInfo.getVersionName()}"
 | 
					 | 
				
			||||||
        private const val BASE_URL_PREF_SUMMARY = "For temporary uses. Update extension will erase this setting."
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Setting: Experimental Latest Fetcher
 | 
					 | 
				
			||||||
        private const val EXPERIMENTAL_LATEST_PREF_TITLE = "Enable Latest (Experimental)"
 | 
					 | 
				
			||||||
        private const val EXPERIMENTAL_LATEST_PREF = "fetchLatestExperiment"
 | 
					 | 
				
			||||||
        private const val EXPERIMENTAL_LATEST_PREF_SUMMARY = "Fetch Latest Manga using Latest Chapters. May has duplicates and May DB corruption on certain Tachiyomi builds"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Setting: Experimental Latest Fetcher With Full Details (Optional)
 | 
					 | 
				
			||||||
        private const val EXPERIMENTAL_LATEST_WITH_DETAIL_PREF_TITLE = "Fetch Latest with detail (Optional)"
 | 
					 | 
				
			||||||
        private const val EXPERIMENTAL_LATEST_WITH_DETAIL_PREF = "fetchLatestWithDetail"
 | 
					 | 
				
			||||||
        private const val EXPERIMENTAL_LATEST_WITH_DETAIL_PREF_SUMMARY =
 | 
					 | 
				
			||||||
            "Parse latest manga details with detail pages. This will reduce DB corruption on certain Tachiyomi builds.\n" +
 | 
					 | 
				
			||||||
                "But makes chance of IP Ban, Also makes bunch of requests, For prevent IP ban, rate limit is set. so may slow,\n" +
 | 
					 | 
				
			||||||
                "Still, It's experiment. Required to enable `Enable Latest (Experimental).`"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Settings: Rate Limit Period
 | 
					 | 
				
			||||||
        private const val defaultRateLimitPeriod: Long = 2L
 | 
					 | 
				
			||||||
        private const val RATE_LIMIT_PERIOD_PREF_TITLE = "Rate Limit Request Period Seconds"
 | 
					 | 
				
			||||||
        private const val RATE_LIMIT_PERIOD_PREF = "rateLimitPeriod"
 | 
					 | 
				
			||||||
        private const val RATE_LIMIT_PERIOD_PREF_SUMMARY =
 | 
					 | 
				
			||||||
            "As Source is using Temporary IP ban system to who makes bunch of request, Some of requests are rate limited\n" +
 | 
					 | 
				
			||||||
                "If you want to reduce limit, Use this option.\n" +
 | 
					 | 
				
			||||||
                "Invalid value will treat as default $defaultRateLimitPeriod seconds.\n" +
 | 
					 | 
				
			||||||
                "(Valid: Min 1 to Max 9)"
 | 
					 | 
				
			||||||
        private const val RATE_LIMIT_PERIOD_PREF_WARNING_INVALID_VALUE = "Invalid value detected. Treating as $defaultRateLimitPeriod..."
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const val PREFIX_ID_SEARCH = "id:"
 | 
					        const val PREFIX_ID_SEARCH = "id:"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        private val chapterNumberRegex by lazy { Regex("([0-9]+)(?:[-.]([0-9]+))?화") }
 | 
				
			||||||
 | 
					        private val htmlDataRegex by lazy { Regex("""html_data\+='([^']+)'""") }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,37 +1,18 @@
 | 
				
			|||||||
package eu.kanade.tachiyomi.extension.ko.newtoki
 | 
					package eu.kanade.tachiyomi.extension.ko.newtoki
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import eu.kanade.tachiyomi.network.GET
 | 
					import eu.kanade.tachiyomi.network.GET
 | 
				
			||||||
import eu.kanade.tachiyomi.source.Source
 | 
					 | 
				
			||||||
import eu.kanade.tachiyomi.source.SourceFactory
 | 
					 | 
				
			||||||
import eu.kanade.tachiyomi.source.model.Filter
 | 
					import eu.kanade.tachiyomi.source.model.Filter
 | 
				
			||||||
import eu.kanade.tachiyomi.source.model.FilterList
 | 
					import eu.kanade.tachiyomi.source.model.FilterList
 | 
				
			||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
 | 
					import okhttp3.HttpUrl.Companion.toHttpUrl
 | 
				
			||||||
import okhttp3.Request
 | 
					import okhttp3.Request
 | 
				
			||||||
import java.security.MessageDigest
 | 
					 | 
				
			||||||
import java.text.SimpleDateFormat
 | 
					 | 
				
			||||||
import java.util.Date
 | 
					 | 
				
			||||||
import java.util.Locale
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					object NewTokiWebtoon : NewToki("NewToki", "webtoon") {
 | 
				
			||||||
 * Source changes domain names every few days (e.g. newtoki31.net to newtoki32.net)
 | 
					 | 
				
			||||||
 * The domain name was newtoki32 on 2019-11-14, this attempts to match the rate at which the domain changes
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * Since 2020-09-20, They changed manga side to Manatoki.
 | 
					 | 
				
			||||||
 * It was merged after shutdown of ManaMoa.
 | 
					 | 
				
			||||||
 * This is by the head of Manamoa, as they decided to move to Newtoki.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
private val domainNumber = 32 + ((Date().time - SimpleDateFormat("yyyy-MM-dd", Locale.US).parse("2019-11-14")!!.time) / 595000000)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class NewTokiFactory : SourceFactory {
 | 
					 | 
				
			||||||
    override fun createSources(): List<Source> = listOf(
 | 
					 | 
				
			||||||
        ManaToki(domainNumber),
 | 
					 | 
				
			||||||
        NewTokiWebtoon()
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class NewTokiWebtoon : NewToki("NewToki", "https://newtoki$domainNumber.com", "webtoon") {
 | 
					 | 
				
			||||||
    // / ! DO NOT CHANGE THIS !  Prevent to treating as a new site
 | 
					    // / ! DO NOT CHANGE THIS !  Prevent to treating as a new site
 | 
				
			||||||
    override val id by lazy { generateSourceId("NewToki (Webtoon)", lang, versionId) }
 | 
					    override val id = NEWTOKI_ID
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override val baseUrl get() = "https://$NEWTOKI_PREFIX$domainNumber.com"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override val preferences = newTokiPreferences
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
 | 
					    override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
 | 
				
			||||||
        val url = ("$baseUrl/webtoon" + (if (page > 1) "/p$page" else "")).toHttpUrl().newBuilder()
 | 
					        val url = ("$baseUrl/webtoon" + (if (page > 1) "/p$page" else "")).toHttpUrl().newBuilder()
 | 
				
			||||||
@ -44,12 +25,12 @@ class NewTokiWebtoon : NewToki("NewToki", "https://newtoki$domainNumber.com", "w
 | 
				
			|||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                is SearchSortTypeList -> {
 | 
					                is SearchSortTypeList -> {
 | 
				
			||||||
                    url.addQueryParameter("sst", listOf("as_update", "wr_hit", "wr_good")[filter.state])
 | 
					                    val state = filter.state ?: return@forEach
 | 
				
			||||||
 | 
					                    url.addQueryParameter("sst", arrayOf("as_update", "wr_hit", "wr_good")[state.index])
 | 
				
			||||||
 | 
					                    url.addQueryParameter("sod", if (state.ascending) "asc" else "desc")
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                is SearchOrderTypeList -> {
 | 
					                else -> {}
 | 
				
			||||||
                    url.addQueryParameter("sod", listOf("desc", "asc")[filter.state])
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -76,11 +57,13 @@ class NewTokiWebtoon : NewToki("NewToki", "https://newtoki$domainNumber.com", "w
 | 
				
			|||||||
                            url.addQueryParameter("tag", filter.values[filter.state])
 | 
					                            url.addQueryParameter("tag", filter.values[filter.state])
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    else -> {}
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return GET(url.toString())
 | 
					        return GET(url.toString(), headers)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private class SearchTargetTypeList : Filter.Select<String>("Type", arrayOf("전체", "일반웹툰", "성인웹툰", "BL/GL", "완결웹툰"))
 | 
					    private class SearchTargetTypeList : Filter.Select<String>("Type", arrayOf("전체", "일반웹툰", "성인웹툰", "BL/GL", "완결웹툰"))
 | 
				
			||||||
@ -143,7 +126,7 @@ class NewTokiWebtoon : NewToki("NewToki", "https://newtoki$domainNumber.com", "w
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private class SearchSortTypeList : Filter.Select<String>(
 | 
					    private class SearchSortTypeList : Filter.Sort(
 | 
				
			||||||
        "Sort",
 | 
					        "Sort",
 | 
				
			||||||
        arrayOf(
 | 
					        arrayOf(
 | 
				
			||||||
            "기본(업데이트순)",
 | 
					            "기본(업데이트순)",
 | 
				
			||||||
@ -152,28 +135,13 @@ class NewTokiWebtoon : NewToki("NewToki", "https://newtoki$domainNumber.com", "w
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private class SearchOrderTypeList : Filter.Select<String>(
 | 
					 | 
				
			||||||
        "Order",
 | 
					 | 
				
			||||||
        arrayOf(
 | 
					 | 
				
			||||||
            "Descending",
 | 
					 | 
				
			||||||
            "Ascending"
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    override fun getFilterList() = FilterList(
 | 
					    override fun getFilterList() = FilterList(
 | 
				
			||||||
        SearchTargetTypeList(),
 | 
					        SearchTargetTypeList(),
 | 
				
			||||||
        SearchSortTypeList(),
 | 
					        SearchSortTypeList(),
 | 
				
			||||||
        SearchOrderTypeList(),
 | 
					 | 
				
			||||||
        Filter.Separator(),
 | 
					        Filter.Separator(),
 | 
				
			||||||
        Filter.Header("Under 3 Filters can't use with query"),
 | 
					        Filter.Header(ignoredForTextSearch()),
 | 
				
			||||||
        SearchYoilTypeList(),
 | 
					        SearchYoilTypeList(),
 | 
				
			||||||
        SearchJaumTypeList(),
 | 
					        SearchJaumTypeList(),
 | 
				
			||||||
        SearchGenreTypeList()
 | 
					        SearchGenreTypeList()
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
fun generateSourceId(name: String, lang: String, versionId: Int): Long {
 | 
					 | 
				
			||||||
    val key = "${name.lowercase()}/$lang/$versionId"
 | 
					 | 
				
			||||||
    val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
 | 
					 | 
				
			||||||
    return (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -0,0 +1,74 @@
 | 
				
			|||||||
 | 
					package eu.kanade.tachiyomi.extension.ko.newtoki
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.app.Application
 | 
				
			||||||
 | 
					import android.content.Context
 | 
				
			||||||
 | 
					import android.content.SharedPreferences
 | 
				
			||||||
 | 
					import androidx.preference.EditTextPreference
 | 
				
			||||||
 | 
					import androidx.preference.ListPreference
 | 
				
			||||||
 | 
					import uy.kohesive.injekt.Injekt
 | 
				
			||||||
 | 
					import uy.kohesive.injekt.api.get
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const val MANATOKI_ID = 2526381983439079467L // "NewToki/ko/1"
 | 
				
			||||||
 | 
					const val NEWTOKI_ID = 1977818283770282459L // "NewToki (Webtoon)/ko/1"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const val MANATOKI_PREFIX = "manatoki"
 | 
				
			||||||
 | 
					const val NEWTOKI_PREFIX = "newtoki"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					val manaTokiPreferences = getSharedPreferences(MANATOKI_ID)
 | 
				
			||||||
 | 
					val newTokiPreferences = getSharedPreferences(NEWTOKI_ID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun getPreferencesInternal(context: Context) = arrayOf(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    EditTextPreference(context).apply {
 | 
				
			||||||
 | 
					        key = DOMAIN_NUMBER_PREF
 | 
				
			||||||
 | 
					        title = domainNumberTitle()
 | 
				
			||||||
 | 
					        summary = domainNumberSummary()
 | 
				
			||||||
 | 
					        setOnPreferenceChangeListener { _, newValue ->
 | 
				
			||||||
 | 
					            val value = newValue as String
 | 
				
			||||||
 | 
					            if (value.isEmpty() || value != value.trim()) {
 | 
				
			||||||
 | 
					                false
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                domainNumber = value
 | 
				
			||||||
 | 
					                true
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ListPreference(context).apply {
 | 
				
			||||||
 | 
					        key = RATE_LIMIT_PERIOD_PREF
 | 
				
			||||||
 | 
					        title = rateLimitTitle()
 | 
				
			||||||
 | 
					        summary = "%s\n" + requiresAppRestart()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val values = Array(RATE_LIMIT_PERIOD_MAX) { (it + 1).toString() }
 | 
				
			||||||
 | 
					        entries = Array(RATE_LIMIT_PERIOD_MAX) { rateLimitEntry(values[it]) }
 | 
				
			||||||
 | 
					        entryValues = values
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        setDefaultValue(RATE_LIMIT_PERIOD_DEFAULT)
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var SharedPreferences.domainNumber: String
 | 
				
			||||||
 | 
					    get() = getString(DOMAIN_NUMBER_PREF, "")!!
 | 
				
			||||||
 | 
					    set(value) = edit().putString(DOMAIN_NUMBER_PREF, value).apply()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					val SharedPreferences.rateLimitPeriod: Int
 | 
				
			||||||
 | 
					    get() = getString(RATE_LIMIT_PERIOD_PREF, RATE_LIMIT_PERIOD_DEFAULT)!!.toInt().coerceIn(1, RATE_LIMIT_PERIOD_MAX)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Don't use the following legacy keys:
 | 
				
			||||||
 | 
					 * - "Override BaseUrl"
 | 
				
			||||||
 | 
					 * - "overrideBaseUrl_v${AppInfo.getVersionName()}"
 | 
				
			||||||
 | 
					 * - "Enable Latest (Experimental)"
 | 
				
			||||||
 | 
					 * - "fetchLatestExperiment"
 | 
				
			||||||
 | 
					 * - "Fetch Latest with detail (Optional)"
 | 
				
			||||||
 | 
					 * - "fetchLatestWithDetail"
 | 
				
			||||||
 | 
					 * - "Rate Limit Request Period Seconds"
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					private const val DOMAIN_NUMBER_PREF = "domainNumber"
 | 
				
			||||||
 | 
					private const val RATE_LIMIT_PERIOD_PREF = "rateLimitPeriod"
 | 
				
			||||||
 | 
					private const val RATE_LIMIT_PERIOD_DEFAULT = 2.toString()
 | 
				
			||||||
 | 
					private const val RATE_LIMIT_PERIOD_MAX = 9
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					private fun getSharedPreferences(id: Long): SharedPreferences =
 | 
				
			||||||
 | 
					    Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
 | 
				
			||||||
@ -0,0 +1,76 @@
 | 
				
			|||||||
 | 
					package eu.kanade.tachiyomi.extension.ko.newtoki
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.os.Build
 | 
				
			||||||
 | 
					import android.os.LocaleList
 | 
				
			||||||
 | 
					import java.util.Locale
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					private val useKorean by lazy {
 | 
				
			||||||
 | 
					    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
 | 
				
			||||||
 | 
					        LocaleList.getDefault().getFirstMatch(arrayOf("ko", "en"))?.language == "ko"
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        Locale.getDefault().language == "ko"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// region Prompts
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun solveCaptcha() = when {
 | 
				
			||||||
 | 
					    useKorean -> "WebView에서 캡챠 풀기"
 | 
				
			||||||
 | 
					    else -> "Solve Captcha with WebView"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun titleNotMatch(realTitle: String) = when {
 | 
				
			||||||
 | 
					    useKorean -> "이 만화를 찾으시려면 '$realTitle'으로 검색하세요"
 | 
				
			||||||
 | 
					    else -> "Find this manga by searching '$realTitle'"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun needMigration() = when {
 | 
				
			||||||
 | 
					    useKorean -> "이 항목은 URL 포맷이 틀립니다. 중복된 항목을 피하려면 동일한 소스로 이전하세요.\n\n"
 | 
				
			||||||
 | 
					    else -> "This entry has wrong URL format. Please migrate to the same source to avoid duplicates.\n\n"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// endregion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// region Filters
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun ignoredForTextSearch() = when {
 | 
				
			||||||
 | 
					    useKorean -> "검색에서 다음 필터 항목은 무시됩니다"
 | 
				
			||||||
 | 
					    else -> "The following filters are ignored for text search"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// endregion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// region Preferences
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun domainNumberTitle() = when {
 | 
				
			||||||
 | 
					    useKorean -> "도메인 번호"
 | 
				
			||||||
 | 
					    else -> "Domain number"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun domainNumberSummary() = when {
 | 
				
			||||||
 | 
					    useKorean -> "도메인 번호는 자동으로 갱신됩니다"
 | 
				
			||||||
 | 
					    else -> "This number is updated automatically"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun editDomainNumber() = when {
 | 
				
			||||||
 | 
					    useKorean -> "확장기능 설정에서 도메인 번호를 수정해 주세요"
 | 
				
			||||||
 | 
					    else -> "Please edit domain number in extension settings"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun rateLimitTitle() = when {
 | 
				
			||||||
 | 
					    useKorean -> "요청 제한"
 | 
				
			||||||
 | 
					    else -> "Rate limit"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun rateLimitEntry(period: String) = when {
 | 
				
			||||||
 | 
					    useKorean -> "${period}초마다 요청"
 | 
				
			||||||
 | 
					    else -> "1 request every $period seconds"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// taken from app strings
 | 
				
			||||||
 | 
					fun requiresAppRestart() = when {
 | 
				
			||||||
 | 
					    useKorean -> "설정을 적용하려면 앱을 재시작하세요"
 | 
				
			||||||
 | 
					    else -> "Requires app restart to take effect"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// endregion
 | 
				
			||||||
@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					package eu.kanade.tachiyomi.extension.ko.newtoki
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import eu.kanade.tachiyomi.source.SourceFactory
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TokiFactory : SourceFactory {
 | 
				
			||||||
 | 
					    override fun createSources() = listOf(ManaToki, NewTokiWebtoon)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user