mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-03 23:58:55 +01:00 
			
		
		
		
	Add 8muses
This commit is contained in:
		@@ -21,11 +21,14 @@ fun Call.asObservableWithAsyncStacktrace(): Observable<Pair<Exception, Response>
 | 
			
		||||
 | 
			
		||||
        // Wrap the call in a helper which handles both unsubscription and backpressure.
 | 
			
		||||
        val requestArbiter = object : AtomicBoolean(), Producer, Subscription {
 | 
			
		||||
            val executed = AtomicBoolean(false)
 | 
			
		||||
 | 
			
		||||
            override fun request(n: Long) {
 | 
			
		||||
                if (n == 0L || !compareAndSet(false, true)) return
 | 
			
		||||
 | 
			
		||||
                try {
 | 
			
		||||
                    val response = call.execute()
 | 
			
		||||
                    executed.set(true)
 | 
			
		||||
                    if (!subscriber.isUnsubscribed) {
 | 
			
		||||
                        subscriber.onNext(asyncStackTrace to response)
 | 
			
		||||
                        subscriber.onCompleted()
 | 
			
		||||
@@ -38,7 +41,8 @@ fun Call.asObservableWithAsyncStacktrace(): Observable<Pair<Exception, Response>
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            override fun unsubscribe() {
 | 
			
		||||
                call.cancel()
 | 
			
		||||
                if(!executed.get())
 | 
			
		||||
                    call.cancel()
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            override fun isUnsubscribed(): Boolean {
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.HttpSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.all.*
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.english.EightMuses
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.english.HentaiCafe
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.english.Pururin
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.english.Tsumino
 | 
			
		||||
@@ -114,6 +115,7 @@ open class SourceManager(private val context: Context) {
 | 
			
		||||
        exSrcs += NHentai(context)
 | 
			
		||||
        exSrcs += Tsumino(context)
 | 
			
		||||
        exSrcs += Hitomi()
 | 
			
		||||
        exSrcs += EightMuses()
 | 
			
		||||
        return exSrcs
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,381 @@
 | 
			
		||||
package eu.kanade.tachiyomi.source.online.english
 | 
			
		||||
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import com.kizitonwose.time.hours
 | 
			
		||||
import com.lvla.rxjava.interopkt.toV1Single
 | 
			
		||||
import eu.kanade.tachiyomi.network.GET
 | 
			
		||||
import eu.kanade.tachiyomi.network.asObservableSuccess
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.*
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.HttpSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.LewdSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
 | 
			
		||||
import eu.kanade.tachiyomi.util.asJsoup
 | 
			
		||||
import exh.EIGHTMUSES_SOURCE_ID
 | 
			
		||||
import exh.metadata.metadata.EightMusesSearchMetadata
 | 
			
		||||
import exh.metadata.metadata.base.RaisedTag
 | 
			
		||||
import exh.util.CachedField
 | 
			
		||||
import exh.util.NakedTrie
 | 
			
		||||
import exh.util.await
 | 
			
		||||
import exh.util.urlImportFetchSearchManga
 | 
			
		||||
import kotlinx.coroutines.*
 | 
			
		||||
import kotlinx.coroutines.rx2.asSingle
 | 
			
		||||
import okhttp3.*
 | 
			
		||||
import org.jsoup.Jsoup
 | 
			
		||||
import org.jsoup.nodes.Document
 | 
			
		||||
import org.jsoup.nodes.Element
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.schedulers.Schedulers
 | 
			
		||||
 | 
			
		||||
typealias SiteMap = NakedTrie<Unit>
 | 
			
		||||
 | 
			
		||||
class EightMuses: HttpSource(),
 | 
			
		||||
        LewdSource<EightMusesSearchMetadata, Document>,
 | 
			
		||||
        UrlImportableSource {
 | 
			
		||||
    override val id = EIGHTMUSES_SOURCE_ID
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Name of the source.
 | 
			
		||||
     */
 | 
			
		||||
    override val name = "8muses"
 | 
			
		||||
    /**
 | 
			
		||||
     * Whether the source has support for latest updates.
 | 
			
		||||
     */
 | 
			
		||||
    override val supportsLatest = true
 | 
			
		||||
    /**
 | 
			
		||||
     * An ISO 639-1 compliant language code (two letters in lower case).
 | 
			
		||||
     */
 | 
			
		||||
    override val lang: String = "en"
 | 
			
		||||
 | 
			
		||||
    override val metaClass = EightMusesSearchMetadata::class
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Base url of the website without the trailing slash, like: http://mysite.com
 | 
			
		||||
     */
 | 
			
		||||
    override val baseUrl = EightMusesSearchMetadata.BASE_URL
 | 
			
		||||
 | 
			
		||||
    private val siteMapCache = CachedField<SiteMap>(1.hours.inMilliseconds.longValue)
 | 
			
		||||
 | 
			
		||||
    override val client: OkHttpClient
 | 
			
		||||
        get() = network.cloudflareClient
 | 
			
		||||
 | 
			
		||||
    private suspend fun obtainSiteMap() = siteMapCache.obtain {
 | 
			
		||||
        withContext(Dispatchers.IO) {
 | 
			
		||||
            val result = client.newCall(eightMusesGet("$baseUrl/sitemap/1.xml"))
 | 
			
		||||
                    .asObservableSuccess()
 | 
			
		||||
                    .toSingle()
 | 
			
		||||
                    .await(Schedulers.io())
 | 
			
		||||
                    .body()!!.string()
 | 
			
		||||
 | 
			
		||||
            val parsed = Jsoup.parse(result)
 | 
			
		||||
 | 
			
		||||
            val seen = NakedTrie<Unit>()
 | 
			
		||||
 | 
			
		||||
            parsed.getElementsByTag("loc").forEach { item ->
 | 
			
		||||
                seen[item.text().substring(22)] = Unit
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            seen
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun headersBuilder(): Headers.Builder {
 | 
			
		||||
        return Headers.Builder()
 | 
			
		||||
                .add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;")
 | 
			
		||||
                .add("Accept-Language", "en-GB,en-US;q=0.9,en;q=0.8")
 | 
			
		||||
                .add("Referer", "https://www.8muses.com")
 | 
			
		||||
                .add("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun eightMusesGet(url: String): Request {
 | 
			
		||||
        return GET(url, headers = headersBuilder().build())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the request for the popular manga given the page.
 | 
			
		||||
     *
 | 
			
		||||
     * @param page the page number to retrieve.
 | 
			
		||||
     */
 | 
			
		||||
    override fun popularMangaRequest(page: Int) = eightMusesGet("$baseUrl/comics/$page")
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parses the response from the site and returns a [MangasPage] object.
 | 
			
		||||
     *
 | 
			
		||||
     * @param response the response from the site.
 | 
			
		||||
     */
 | 
			
		||||
    override fun popularMangaParse(response: Response): MangasPage {
 | 
			
		||||
        throw UnsupportedOperationException("Should not be called!")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the request for the search manga given the page.
 | 
			
		||||
     *
 | 
			
		||||
     * @param page the page number to retrieve.
 | 
			
		||||
     * @param query the search query.
 | 
			
		||||
     * @param filters the list of filters to apply.
 | 
			
		||||
     */
 | 
			
		||||
    override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
 | 
			
		||||
        val urlBuilder = if(!query.isBlank()) {
 | 
			
		||||
            HttpUrl.parse("$baseUrl/search")!!
 | 
			
		||||
                    .newBuilder()
 | 
			
		||||
                    .addQueryParameter("q", query)
 | 
			
		||||
        } else {
 | 
			
		||||
            HttpUrl.parse("$baseUrl/comics")!!
 | 
			
		||||
                    .newBuilder()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        urlBuilder.addQueryParameter("page", page.toString())
 | 
			
		||||
 | 
			
		||||
        filters.filterIsInstance<SortFilter>().map {
 | 
			
		||||
            it.addToUri(urlBuilder)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return eightMusesGet(urlBuilder.toString())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parses the response from the site and returns a [MangasPage] object.
 | 
			
		||||
     *
 | 
			
		||||
     * @param response the response from the site.
 | 
			
		||||
     */
 | 
			
		||||
    override fun searchMangaParse(response: Response): MangasPage {
 | 
			
		||||
        throw UnsupportedOperationException("Should not be called!")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the request for latest manga given the page.
 | 
			
		||||
     *
 | 
			
		||||
     * @param page the page number to retrieve.
 | 
			
		||||
     */
 | 
			
		||||
    override fun latestUpdatesRequest(page: Int) = eightMusesGet("$baseUrl/comics/lastupdate?page=$page")
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parses the response from the site and returns a [MangasPage] object.
 | 
			
		||||
     *
 | 
			
		||||
     * @param response the response from the site.
 | 
			
		||||
     */
 | 
			
		||||
    override fun latestUpdatesParse(response: Response): MangasPage {
 | 
			
		||||
        throw UnsupportedOperationException("Should not be called!")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun fetchLatestUpdates(page: Int)
 | 
			
		||||
            = fetchListing(latestUpdatesRequest(page), false)
 | 
			
		||||
 | 
			
		||||
    override fun fetchPopularManga(page: Int)
 | 
			
		||||
            = fetchListing(popularMangaRequest(page), false) // TODO Dig
 | 
			
		||||
 | 
			
		||||
    override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
 | 
			
		||||
        return urlImportFetchSearchManga(query) {
 | 
			
		||||
            fetchListing(searchMangaRequest(page, query, filters), false)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun fetchListing(request: Request, dig: Boolean): Observable<MangasPage> {
 | 
			
		||||
        return client.newCall(request)
 | 
			
		||||
                .asObservableSuccess()
 | 
			
		||||
                .flatMapSingle { response ->
 | 
			
		||||
                    GlobalScope.async(Dispatchers.IO) {
 | 
			
		||||
                        parseResultsPage(response, dig)
 | 
			
		||||
                    }.asSingle(GlobalScope.coroutineContext).toV1Single()
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private suspend fun parseResultsPage(response: Response, dig: Boolean): MangasPage {
 | 
			
		||||
        val doc = response.asJsoup()
 | 
			
		||||
        val contents = parseSelf(doc)
 | 
			
		||||
 | 
			
		||||
        val onLastPage = doc.selectFirst(".current:nth-last-child(2)") != null
 | 
			
		||||
 | 
			
		||||
        return MangasPage(
 | 
			
		||||
                if(dig) {
 | 
			
		||||
                    contents.albums.flatMap {
 | 
			
		||||
                        val href = it.attr("href")
 | 
			
		||||
                        val splitHref = href.split('/')
 | 
			
		||||
                        obtainSiteMap().subMap(href).filter {
 | 
			
		||||
                            it.key.split('/').size - splitHref.size == 1
 | 
			
		||||
                        }.map { (key, _) ->
 | 
			
		||||
                            SManga.create().apply {
 | 
			
		||||
                                url = key
 | 
			
		||||
 | 
			
		||||
                                title = key.substringAfterLast('/').replace('-', ' ')
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    contents.albums.map {
 | 
			
		||||
                        SManga.create().apply {
 | 
			
		||||
                            url = it.attr("href")
 | 
			
		||||
 | 
			
		||||
                            title = it.select(".title-text").text()
 | 
			
		||||
 | 
			
		||||
                            thumbnail_url = baseUrl + it.select(".lazyload").attr("data-src")
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                !onLastPage
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parses the response from the site and returns the details of a manga.
 | 
			
		||||
     *
 | 
			
		||||
     * @param response the response from the site.
 | 
			
		||||
     */
 | 
			
		||||
    override fun mangaDetailsParse(response: Response): SManga {
 | 
			
		||||
        throw UnsupportedOperationException("Should not be called!")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns an observable with the updated details for a manga. Normally it's not needed to
 | 
			
		||||
     * override this method.
 | 
			
		||||
     *
 | 
			
		||||
     * @param manga the manga to be updated.
 | 
			
		||||
     */
 | 
			
		||||
    override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
 | 
			
		||||
        return client.newCall(mangaDetailsRequest(manga))
 | 
			
		||||
                .asObservableSuccess()
 | 
			
		||||
                .flatMap {
 | 
			
		||||
                    parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga))
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parses the response from the site and returns a list of chapters.
 | 
			
		||||
     *
 | 
			
		||||
     * @param response the response from the site.
 | 
			
		||||
     */
 | 
			
		||||
    override fun chapterListParse(response: Response): List<SChapter> {
 | 
			
		||||
        throw UnsupportedOperationException("Should not be called!")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
 | 
			
		||||
        return GlobalScope.async(Dispatchers.IO) {
 | 
			
		||||
            fetchAndParseChapterList("", manga.url)
 | 
			
		||||
        }.asSingle(GlobalScope.coroutineContext).toV1Single().toObservable()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private suspend fun fetchAndParseChapterList(prefix: String, url: String): List<SChapter> {
 | 
			
		||||
        // Request
 | 
			
		||||
        val req = eightMusesGet(baseUrl + url)
 | 
			
		||||
 | 
			
		||||
        return client.newCall(req).asObservableSuccess().toSingle().await(Schedulers.io()).use { response ->
 | 
			
		||||
            val contents = parseSelf(response.asJsoup())
 | 
			
		||||
 | 
			
		||||
            val out = mutableListOf<SChapter>()
 | 
			
		||||
            if(contents.images.isNotEmpty()) {
 | 
			
		||||
                out += SChapter.create().apply {
 | 
			
		||||
                    this.url = url
 | 
			
		||||
                    this.name = if(prefix.isBlank()) ">" else prefix
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            val builtPrefix = if(prefix.isBlank()) "> " else "$prefix > "
 | 
			
		||||
 | 
			
		||||
            out + contents.albums.flatMap { ele ->
 | 
			
		||||
                fetchAndParseChapterList(builtPrefix + ele.selectFirst(".title-text").text(), ele.attr("href"))
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    data class SelfContents(val albums: List<Element>, val images: List<Element>)
 | 
			
		||||
    private fun parseSelf(doc: Document): SelfContents {
 | 
			
		||||
        // Parse self
 | 
			
		||||
        val gc = doc.select(".gallery .c-tile")
 | 
			
		||||
 | 
			
		||||
        // Check if any in self
 | 
			
		||||
        val selfAlbums = gc.filter { it.attr("href").startsWith("/comics/album") }
 | 
			
		||||
        val selfImages = gc.filter { it.attr("href").startsWith("/comics/picture") }
 | 
			
		||||
 | 
			
		||||
        return SelfContents(selfAlbums, selfImages)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parses the response from the site and returns a list of pages.
 | 
			
		||||
     *
 | 
			
		||||
     * @param response the response from the site.
 | 
			
		||||
     */
 | 
			
		||||
    override fun pageListParse(response: Response): List<Page> {
 | 
			
		||||
        val contents = parseSelf(response.asJsoup())
 | 
			
		||||
        return contents.images.mapIndexed { index, element ->
 | 
			
		||||
            Page(
 | 
			
		||||
                    index,
 | 
			
		||||
                    element.attr("href"),
 | 
			
		||||
                    "$baseUrl/image/fl" + element.select(".lazyload").attr("data-src").substring(9)
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun parseIntoMetadata(metadata: EightMusesSearchMetadata, input: Document) {
 | 
			
		||||
        with(metadata) {
 | 
			
		||||
            path = Uri.parse(input.location()).pathSegments
 | 
			
		||||
 | 
			
		||||
            val breadcrumbs = input.selectFirst(".top-menu-breadcrumb > ol")
 | 
			
		||||
 | 
			
		||||
            title = breadcrumbs.selectFirst("li:nth-last-child(1) > a").text()
 | 
			
		||||
 | 
			
		||||
            thumbnailUrl = parseSelf(input).let { it.albums + it.images }.firstOrNull()
 | 
			
		||||
                    ?.selectFirst(".lazyload")
 | 
			
		||||
                    ?.attr("data-src")?.let {
 | 
			
		||||
                        baseUrl + it
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
            tags.clear()
 | 
			
		||||
            tags += RaisedTag(
 | 
			
		||||
                    EightMusesSearchMetadata.ARTIST_NAMESPACE,
 | 
			
		||||
                    breadcrumbs.selectFirst("li:nth-child(2) > a").text(),
 | 
			
		||||
                    EightMusesSearchMetadata.TAG_TYPE_DEFAULT
 | 
			
		||||
            )
 | 
			
		||||
            tags += input.select(".album-tags a").map {
 | 
			
		||||
                RaisedTag(
 | 
			
		||||
                        EightMusesSearchMetadata.TAGS_NAMESPACE,
 | 
			
		||||
                        it.text(),
 | 
			
		||||
                        EightMusesSearchMetadata.TAG_TYPE_DEFAULT
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    class SortFilter : Filter.Select<String>(
 | 
			
		||||
            "Sort",
 | 
			
		||||
            SORT_OPTIONS.map { it.second }.toTypedArray()
 | 
			
		||||
    ) {
 | 
			
		||||
        fun addToUri(url: HttpUrl.Builder) {
 | 
			
		||||
            url.addQueryParameter("sort", SORT_OPTIONS[state].first)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        companion object {
 | 
			
		||||
            // <Internal, Display>
 | 
			
		||||
            private val SORT_OPTIONS = listOf(
 | 
			
		||||
                    "" to "Views",
 | 
			
		||||
                    "like" to "Likes",
 | 
			
		||||
                    "date" to "Date",
 | 
			
		||||
                    "az" to "A-Z"
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getFilterList() = FilterList(
 | 
			
		||||
            SortFilter()
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parses the response from the site and returns the absolute url to the source image.
 | 
			
		||||
     *
 | 
			
		||||
     * @param response the response from the site.
 | 
			
		||||
     */
 | 
			
		||||
    override fun imageUrlParse(response: Response): String {
 | 
			
		||||
        throw UnsupportedOperationException("Should not be called!")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override val matchingHosts = listOf(
 | 
			
		||||
            "www.8muses.com",
 | 
			
		||||
            "8muses.com"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    override fun mapUrlToMangaUrl(uri: Uri): String? {
 | 
			
		||||
        var path = uri.pathSegments.drop(2)
 | 
			
		||||
        if(uri.pathSegments[1].toLowerCase() == "picture") {
 | 
			
		||||
            path = path.dropLast(1)
 | 
			
		||||
        }
 | 
			
		||||
        return "/comics/album/${path.joinToString("/")}"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -20,6 +20,7 @@ val HENTAI_CAFE_SOURCE_ID = delegatedSourceId<HentaiCafe>()
 | 
			
		||||
val PURURIN_SOURCE_ID = delegatedSourceId<Pururin>()
 | 
			
		||||
const val TSUMINO_SOURCE_ID = LEWD_SOURCE_SERIES + 9
 | 
			
		||||
const val HITOMI_SOURCE_ID = LEWD_SOURCE_SERIES + 10
 | 
			
		||||
const val EIGHTMUSES_SOURCE_ID = LEWD_SOURCE_SERIES + 11
 | 
			
		||||
const val MERGED_SOURCE_ID = LEWD_SOURCE_SERIES + 69
 | 
			
		||||
 | 
			
		||||
private val DELEGATED_LEWD_SOURCES = listOf(
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,50 @@
 | 
			
		||||
package exh.metadata.metadata
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SManga
 | 
			
		||||
import exh.metadata.metadata.base.RaisedSearchMetadata
 | 
			
		||||
import exh.plusAssign
 | 
			
		||||
 | 
			
		||||
class EightMusesSearchMetadata : RaisedSearchMetadata() {
 | 
			
		||||
    var path: List<String> = emptyList()
 | 
			
		||||
 | 
			
		||||
    var title by titleDelegate(TITLE_TYPE_MAIN)
 | 
			
		||||
 | 
			
		||||
    var thumbnailUrl: String? = null
 | 
			
		||||
 | 
			
		||||
    override fun copyTo(manga: SManga) {
 | 
			
		||||
        manga.url = path.joinToString("/", prefix = "/")
 | 
			
		||||
 | 
			
		||||
        title?.let {
 | 
			
		||||
            manga.title = it
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        thumbnailUrl?.let {
 | 
			
		||||
            manga.thumbnail_url = it
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        manga.artist = tags.ofNamespace(ARTIST_NAMESPACE).joinToString { it.name }
 | 
			
		||||
 | 
			
		||||
        manga.genre = tagsToGenreString()
 | 
			
		||||
 | 
			
		||||
        val titleDesc = StringBuilder()
 | 
			
		||||
        title?.let { titleDesc += "Title: $it\n" }
 | 
			
		||||
 | 
			
		||||
        val tagsDesc = tagsToDescription()
 | 
			
		||||
 | 
			
		||||
        manga.description = listOf(titleDesc.toString(), tagsDesc.toString())
 | 
			
		||||
                .filter(String::isNotBlank)
 | 
			
		||||
                .joinToString(separator = "\n")
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        private const val TITLE_TYPE_MAIN = 0
 | 
			
		||||
 | 
			
		||||
        const val TAG_TYPE_DEFAULT = 0
 | 
			
		||||
 | 
			
		||||
        const val BASE_URL = "https://www.8muses.com"
 | 
			
		||||
 | 
			
		||||
        const val TAGS_NAMESPACE = "tags"
 | 
			
		||||
        const val ARTIST_NAMESPACE = "artist"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										24
									
								
								app/src/main/java/exh/util/CachedField.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								app/src/main/java/exh/util/CachedField.kt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
			
		||||
package exh.util
 | 
			
		||||
 | 
			
		||||
import kotlinx.coroutines.sync.Mutex
 | 
			
		||||
import kotlinx.coroutines.sync.withLock
 | 
			
		||||
 | 
			
		||||
class CachedField<T>(private val expiresAfterMs: Long) {
 | 
			
		||||
    @Volatile
 | 
			
		||||
    private var initTime: Long = -1
 | 
			
		||||
 | 
			
		||||
    @Volatile
 | 
			
		||||
    private var content: T? = null
 | 
			
		||||
 | 
			
		||||
    private val mutex = Mutex()
 | 
			
		||||
 | 
			
		||||
    suspend fun obtain(producer: suspend () -> T): T {
 | 
			
		||||
        return mutex.withLock {
 | 
			
		||||
            if(initTime < 0 || System.currentTimeMillis() - initTime > expiresAfterMs) {
 | 
			
		||||
                content = producer()
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            content as T
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										174
									
								
								app/src/main/java/exh/util/FakeMutables.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								app/src/main/java/exh/util/FakeMutables.kt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,174 @@
 | 
			
		||||
package exh.util
 | 
			
		||||
 | 
			
		||||
// Zero-allocation-overhead mutable collection shims
 | 
			
		||||
 | 
			
		||||
private inline class CollectionShim<E>(private val coll: Collection<E>) : FakeMutableCollection<E> {
 | 
			
		||||
    override val size: Int get() = coll.size
 | 
			
		||||
 | 
			
		||||
    override fun contains(element: E) = coll.contains(element)
 | 
			
		||||
 | 
			
		||||
    override fun containsAll(elements: Collection<E>) = coll.containsAll(elements)
 | 
			
		||||
 | 
			
		||||
    override fun isEmpty() = coll.isEmpty()
 | 
			
		||||
 | 
			
		||||
    override fun fakeIterator() = coll.iterator()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface FakeMutableCollection<E> : MutableCollection<E>, FakeMutableIterable<E> {
 | 
			
		||||
    override fun add(element: E): Boolean {
 | 
			
		||||
        throw UnsupportedOperationException("This collection is immutable!")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun addAll(elements: Collection<E>): Boolean {
 | 
			
		||||
        throw UnsupportedOperationException("This collection is immutable!")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun clear() {
 | 
			
		||||
        throw UnsupportedOperationException("This collection is immutable!")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun remove(element: E): Boolean {
 | 
			
		||||
        throw UnsupportedOperationException("This collection is immutable!")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun removeAll(elements: Collection<E>): Boolean {
 | 
			
		||||
        throw UnsupportedOperationException("This collection is immutable!")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun retainAll(elements: Collection<E>): Boolean {
 | 
			
		||||
        throw UnsupportedOperationException("This collection is immutable!")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun iterator(): MutableIterator<E> = super.iterator()
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        fun <E> fromCollection(coll: Collection<E>): FakeMutableCollection<E> = CollectionShim(coll)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private inline class SetShim<E>(private val set: Set<E>) : FakeMutableSet<E> {
 | 
			
		||||
    override val size: Int get() = set.size
 | 
			
		||||
 | 
			
		||||
    override fun contains(element: E) = set.contains(element)
 | 
			
		||||
 | 
			
		||||
    override fun containsAll(elements: Collection<E>) = set.containsAll(elements)
 | 
			
		||||
 | 
			
		||||
    override fun isEmpty() = set.isEmpty()
 | 
			
		||||
 | 
			
		||||
    override fun fakeIterator() = set.iterator()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface FakeMutableSet<E> : MutableSet<E>, FakeMutableCollection<E> {
 | 
			
		||||
    /**
 | 
			
		||||
     * Adds the specified element to the set.
 | 
			
		||||
     *
 | 
			
		||||
     * @return `true` if the element has been added, `false` if the element is already contained in the set.
 | 
			
		||||
     */
 | 
			
		||||
    override fun add(element: E): Boolean = super.add(element)
 | 
			
		||||
 | 
			
		||||
    override fun addAll(elements: Collection<E>): Boolean = super.addAll(elements)
 | 
			
		||||
 | 
			
		||||
    override fun clear() = super.clear()
 | 
			
		||||
 | 
			
		||||
    override fun remove(element: E): Boolean = super.remove(element)
 | 
			
		||||
 | 
			
		||||
    override fun removeAll(elements: Collection<E>): Boolean = super.removeAll(elements)
 | 
			
		||||
 | 
			
		||||
    override fun retainAll(elements: Collection<E>): Boolean = super.retainAll(elements)
 | 
			
		||||
 | 
			
		||||
    override fun iterator(): MutableIterator<E> = super.iterator()
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        fun <E> fromSet(set: Set<E>): FakeMutableSet<E> = SetShim(set)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private inline class IterableShim<E>(private val iterable: Iterable<E>) : FakeMutableIterable<E> {
 | 
			
		||||
    override fun fakeIterator() = iterable.iterator()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface FakeMutableIterable<E> : MutableIterable<E> {
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns an iterator over the elements of this sequence that supports removing elements during iteration.
 | 
			
		||||
     */
 | 
			
		||||
    override fun iterator(): MutableIterator<E> = FakeMutableIterator.fromIterator(fakeIterator())
 | 
			
		||||
 | 
			
		||||
    fun fakeIterator(): Iterator<E>
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        fun <E> fromIterable(iterable: Iterable<E>): FakeMutableIterable<E> = IterableShim(iterable)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private inline class IteratorShim<E>(private val iterator: Iterator<E>) : FakeMutableIterator<E> {
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns `true` if the iteration has more elements.
 | 
			
		||||
     */
 | 
			
		||||
    override fun hasNext() = iterator.hasNext()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the next element in the iteration.
 | 
			
		||||
     */
 | 
			
		||||
    override fun next() = iterator.next()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
interface FakeMutableIterator<E> : MutableIterator<E> {
 | 
			
		||||
    /**
 | 
			
		||||
     * Removes from the underlying collection the last element returned by this iterator.
 | 
			
		||||
     */
 | 
			
		||||
    override fun remove() {
 | 
			
		||||
        throw UnsupportedOperationException("This set is immutable!")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        fun <E> fromIterator(iterator: Iterator<E>) : FakeMutableIterator<E> = IteratorShim(iterator)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private inline class EntryShim<K, V>(private val entry: Map.Entry<K, V>) : FakeMutableEntry<K, V> {
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the key of this key/value pair.
 | 
			
		||||
     */
 | 
			
		||||
    override val key: K
 | 
			
		||||
        get() = entry.key
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the value of this key/value pair.
 | 
			
		||||
     */
 | 
			
		||||
    override val value: V
 | 
			
		||||
        get() = entry.value
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private inline class PairShim<K, V>(private val pair: Pair<K, V>) : FakeMutableEntry<K, V> {
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the key of this key/value pair.
 | 
			
		||||
     */
 | 
			
		||||
    override val key: K get() = pair.first
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the value of this key/value pair.
 | 
			
		||||
     */
 | 
			
		||||
    override val value: V get() = pair.second
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface FakeMutableEntry<K, V> : MutableMap.MutableEntry<K, V> {
 | 
			
		||||
    override fun setValue(newValue: V): V {
 | 
			
		||||
        throw UnsupportedOperationException("This entry is immutable!")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        fun <K, V> fromEntry(entry: Map.Entry<K, V>): FakeMutableEntry<K, V> = EntryShim(entry)
 | 
			
		||||
 | 
			
		||||
        fun <K, V> fromPair(pair: Pair<K, V>): FakeMutableEntry<K, V> = PairShim(pair)
 | 
			
		||||
 | 
			
		||||
        fun <K, V> fromPair(key: K, value: V) = object : FakeMutableEntry<K, V> {
 | 
			
		||||
            /**
 | 
			
		||||
             * Returns the key of this key/value pair.
 | 
			
		||||
             */
 | 
			
		||||
            override val key: K = key
 | 
			
		||||
            /**
 | 
			
		||||
             * Returns the value of this key/value pair.
 | 
			
		||||
             */
 | 
			
		||||
            override val value: V = value
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										345
									
								
								app/src/main/java/exh/util/NakedTrie.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										345
									
								
								app/src/main/java/exh/util/NakedTrie.kt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,345 @@
 | 
			
		||||
package exh.util
 | 
			
		||||
 | 
			
		||||
import android.util.SparseArray
 | 
			
		||||
import java.util.*
 | 
			
		||||
 | 
			
		||||
class NakedTrieNode<T>(val key: Int, var parent: NakedTrieNode<T>?) {
 | 
			
		||||
    val children = SparseArray<NakedTrieNode<T>>(1)
 | 
			
		||||
    var hasData: Boolean = false
 | 
			
		||||
    var data: T? = null
 | 
			
		||||
 | 
			
		||||
    // Walks in ascending order
 | 
			
		||||
    // Consumer should return true to continue walking, false to stop walking
 | 
			
		||||
    inline fun walk(prefix: String, consumer: (String, T) -> Boolean, leavesOnly: Boolean) {
 | 
			
		||||
        // Special case root
 | 
			
		||||
        if(hasData && (!leavesOnly || children.size() <= 0)) {
 | 
			
		||||
            if(!consumer(prefix, data as T)) return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val stack = LinkedList<Pair<String, NakedTrieNode<T>>>()
 | 
			
		||||
        SparseArrayValueCollection(children, true).forEach {
 | 
			
		||||
            stack += prefix + it.key.toChar() to it
 | 
			
		||||
        }
 | 
			
		||||
        while(!stack.isEmpty()) {
 | 
			
		||||
            val (key, bottom) = stack.removeLast()
 | 
			
		||||
            SparseArrayValueCollection(bottom.children, true).forEach {
 | 
			
		||||
                stack += key + it.key.toChar() to it
 | 
			
		||||
            }
 | 
			
		||||
            if(bottom.hasData && (!leavesOnly || bottom.children.size() <= 0)) {
 | 
			
		||||
                if(!consumer(key, bottom.data as T)) return
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getAsNode(key: String): NakedTrieNode<T>? {
 | 
			
		||||
        var current = this
 | 
			
		||||
        for(c in key) {
 | 
			
		||||
            current = current.children.get(c.toInt()) ?: return null
 | 
			
		||||
            if(!current.hasData) return null
 | 
			
		||||
        }
 | 
			
		||||
        return current
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Fast, memory efficient and flexible trie implementation with implementation details exposed
 | 
			
		||||
 */
 | 
			
		||||
class NakedTrie<T> : MutableMap<String, T> {
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the number of key/value pairs in the map.
 | 
			
		||||
     */
 | 
			
		||||
    override var size: Int = 0
 | 
			
		||||
        private set
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns `true` if the map is empty (contains no elements), `false` otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    override fun isEmpty() = size <= 0
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Removes all elements from this map.
 | 
			
		||||
     */
 | 
			
		||||
    override fun clear() {
 | 
			
		||||
        root.children.clear()
 | 
			
		||||
        root.hasData = false
 | 
			
		||||
        root.data = null
 | 
			
		||||
        size = 0
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    val root = NakedTrieNode<T>(-1, null)
 | 
			
		||||
    private var version: Long = 0
 | 
			
		||||
 | 
			
		||||
    override fun put(key: String, value: T): T? {
 | 
			
		||||
        // Traverse to node location in tree, making parent nodes if required
 | 
			
		||||
        var current = root
 | 
			
		||||
        for(c in key) {
 | 
			
		||||
            val castedC = c.toInt()
 | 
			
		||||
            var node = current.children.get(castedC)
 | 
			
		||||
            if(node == null) {
 | 
			
		||||
                node = NakedTrieNode(castedC, current)
 | 
			
		||||
                current.children.put(castedC, node)
 | 
			
		||||
            }
 | 
			
		||||
            current = node
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Add data to node or replace existing data
 | 
			
		||||
        val previous = if(current.hasData) {
 | 
			
		||||
            current.data
 | 
			
		||||
        } else {
 | 
			
		||||
            current.hasData = true
 | 
			
		||||
            size++
 | 
			
		||||
            null
 | 
			
		||||
        }
 | 
			
		||||
        current.data = value
 | 
			
		||||
 | 
			
		||||
        version++
 | 
			
		||||
 | 
			
		||||
        return previous
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun get(key: String): T? {
 | 
			
		||||
        val current = getAsNode(key) ?: return null
 | 
			
		||||
        return if(current.hasData) current.data else null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getAsNode(key: String): NakedTrieNode<T>? {
 | 
			
		||||
        return root.getAsNode(key)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun containsKey(key: String): Boolean {
 | 
			
		||||
        var current = root
 | 
			
		||||
        for(c in key) {
 | 
			
		||||
            current = current.children.get(c.toInt()) ?: return false
 | 
			
		||||
            if(!current.hasData) return false
 | 
			
		||||
        }
 | 
			
		||||
        return current.hasData
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Removes the specified key and its corresponding value from this map.
 | 
			
		||||
     *
 | 
			
		||||
     * @return the previous value associated with the key, or `null` if the key was not present in the map.
 | 
			
		||||
     */
 | 
			
		||||
    override fun remove(key: String): T? {
 | 
			
		||||
        // Traverse node tree while keeping track of the nodes we have visited
 | 
			
		||||
        val nodeStack = LinkedList<NakedTrieNode<T>>()
 | 
			
		||||
        for(c in key) {
 | 
			
		||||
            val bottomOfStack = nodeStack.last
 | 
			
		||||
            val current = bottomOfStack.children.get(c.toInt()) ?: return null
 | 
			
		||||
            if(!current.hasData) return null
 | 
			
		||||
            nodeStack.add(bottomOfStack)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Mark node as having no data
 | 
			
		||||
        val bottomOfStack = nodeStack.last
 | 
			
		||||
        bottomOfStack.hasData = false
 | 
			
		||||
        val oldData = bottomOfStack.data
 | 
			
		||||
        bottomOfStack.data = null // Clear data field for GC
 | 
			
		||||
 | 
			
		||||
        // Remove nodes that we visited that are useless
 | 
			
		||||
        for(curBottom in nodeStack.descendingIterator()) {
 | 
			
		||||
            val parent = curBottom.parent ?: break
 | 
			
		||||
            if(!curBottom.hasData && curBottom.children.size() <= 0) {
 | 
			
		||||
                // No data or child nodes, this node is useless, discard
 | 
			
		||||
                parent.children.remove(curBottom.key)
 | 
			
		||||
            } else break
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        version++
 | 
			
		||||
        size--
 | 
			
		||||
 | 
			
		||||
        return oldData
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Updates this map with key/value pairs from the specified map [from].
 | 
			
		||||
     */
 | 
			
		||||
    override fun putAll(from: Map<out String, T>) {
 | 
			
		||||
        // No way to optimize this so yeah...
 | 
			
		||||
        from.forEach { (s, u) ->
 | 
			
		||||
            put(s, u)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Walks in ascending order
 | 
			
		||||
    // Consumer should return true to continue walking, false to stop walking
 | 
			
		||||
    inline fun walk(consumer: (String, T) -> Boolean) {
 | 
			
		||||
        walk(consumer, false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Walks in ascending order
 | 
			
		||||
    // Consumer should return true to continue walking, false to stop walking
 | 
			
		||||
    inline fun walk(consumer: (String, T) -> Boolean, leavesOnly: Boolean) {
 | 
			
		||||
        root.walk("", consumer, leavesOnly)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getOrPut(key: String, producer: () -> T): T {
 | 
			
		||||
        // Traverse to node location in tree, making parent nodes if required
 | 
			
		||||
        var current = root
 | 
			
		||||
        for(c in key) {
 | 
			
		||||
            val castedC = c.toInt()
 | 
			
		||||
            var node = current.children.get(castedC)
 | 
			
		||||
            if(node == null) {
 | 
			
		||||
                node = NakedTrieNode(castedC, current)
 | 
			
		||||
                current.children.put(castedC, node)
 | 
			
		||||
            }
 | 
			
		||||
            current = node
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Add data to node or replace existing data
 | 
			
		||||
        if(!current.hasData) {
 | 
			
		||||
            current.hasData = true
 | 
			
		||||
            current.data = producer()
 | 
			
		||||
            size++
 | 
			
		||||
            version++
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return current.data as T
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Includes root
 | 
			
		||||
    fun subMap(prefix: String, leavesOnly: Boolean = false): Map<String, T> {
 | 
			
		||||
        val node = getAsNode(prefix) ?: return emptyMap()
 | 
			
		||||
 | 
			
		||||
        return object : Map<String, T> {
 | 
			
		||||
            /**
 | 
			
		||||
             * Returns a read-only [Set] of all key/value pairs in this map.
 | 
			
		||||
             */
 | 
			
		||||
            override val entries: Set<Map.Entry<String, T>>
 | 
			
		||||
                get() {
 | 
			
		||||
                    val out = mutableSetOf<Map.Entry<String, T>>()
 | 
			
		||||
                    node.walk("", { k, v ->
 | 
			
		||||
                        out.add(AbstractMap.SimpleImmutableEntry(k, v))
 | 
			
		||||
                        true
 | 
			
		||||
                    }, leavesOnly)
 | 
			
		||||
                    return out
 | 
			
		||||
                }
 | 
			
		||||
            /**
 | 
			
		||||
             * Returns a read-only [Set] of all keys in this map.
 | 
			
		||||
             */
 | 
			
		||||
            override val keys: Set<String>
 | 
			
		||||
                get() {
 | 
			
		||||
                    val out = mutableSetOf<String>()
 | 
			
		||||
                    node.walk("", { k, _ ->
 | 
			
		||||
                        out.add(k)
 | 
			
		||||
                        true
 | 
			
		||||
                    }, leavesOnly)
 | 
			
		||||
                    return out
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            /**
 | 
			
		||||
             * Returns the number of key/value pairs in the map.
 | 
			
		||||
             */
 | 
			
		||||
            override val size: Int get() {
 | 
			
		||||
                var s = 0
 | 
			
		||||
                node.walk("", { _, _ -> s++; true }, leavesOnly)
 | 
			
		||||
                return s
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            /**
 | 
			
		||||
             * Returns a read-only [Collection] of all values in this map. Note that this collection may contain duplicate values.
 | 
			
		||||
             */
 | 
			
		||||
            override val values: Collection<T>
 | 
			
		||||
                get() {
 | 
			
		||||
                    val out = mutableSetOf<T>()
 | 
			
		||||
                    node.walk("", { _, v ->
 | 
			
		||||
                        out.add(v)
 | 
			
		||||
                        true
 | 
			
		||||
                    }, leavesOnly)
 | 
			
		||||
                    return out
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            /**
 | 
			
		||||
             * Returns `true` if the map contains the specified [key].
 | 
			
		||||
             */
 | 
			
		||||
            override fun containsKey(key: String): Boolean {
 | 
			
		||||
                if(!key.startsWith(prefix)) return false
 | 
			
		||||
 | 
			
		||||
                val childNode = node.getAsNode(key.removePrefix(prefix)) ?: return false
 | 
			
		||||
                return childNode.hasData && (!leavesOnly || childNode.children.size() <= 0)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            /**
 | 
			
		||||
             * Returns `true` if the map maps one or more keys to the specified [value].
 | 
			
		||||
             */
 | 
			
		||||
            override fun containsValue(value: T): Boolean {
 | 
			
		||||
                node.walk("", { _, v ->
 | 
			
		||||
                    if(v == value) return true
 | 
			
		||||
                    true
 | 
			
		||||
                }, leavesOnly)
 | 
			
		||||
                return false
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            /**
 | 
			
		||||
             * Returns the value corresponding to the given [key], or `null` if such a key is not present in the map.
 | 
			
		||||
             */
 | 
			
		||||
            override fun get(key: String): T? {
 | 
			
		||||
                if(!key.startsWith(prefix)) return null
 | 
			
		||||
 | 
			
		||||
                val childNode = node.getAsNode(key.removePrefix(prefix)) ?: return null
 | 
			
		||||
                if(!childNode.hasData || (leavesOnly && childNode.children.size() > 0)) return null
 | 
			
		||||
                return childNode.data
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            /**
 | 
			
		||||
             * Returns `true` if the map is empty (contains no elements), `false` otherwise.
 | 
			
		||||
             */
 | 
			
		||||
            override fun isEmpty(): Boolean {
 | 
			
		||||
                if(node.children.size() <= 0 && !root.hasData) return true
 | 
			
		||||
                if(!leavesOnly) return false
 | 
			
		||||
                node.walk("", { _, _ -> return false }, leavesOnly)
 | 
			
		||||
                return true
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Slow methods below
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns `true` if the map maps one or more keys to the specified [value].
 | 
			
		||||
     */
 | 
			
		||||
    override fun containsValue(value: T): Boolean {
 | 
			
		||||
        walk { _, t ->
 | 
			
		||||
            if(t == value) {
 | 
			
		||||
                return true
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            true
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns a [MutableSet] of all key/value pairs in this map.
 | 
			
		||||
     */
 | 
			
		||||
    override val entries: MutableSet<MutableMap.MutableEntry<String, T>>
 | 
			
		||||
        get() = FakeMutableSet.fromSet(mutableSetOf<MutableMap.MutableEntry<String, T>>().apply {
 | 
			
		||||
            walk { k, v ->
 | 
			
		||||
                this += FakeMutableEntry.fromPair(k, v)
 | 
			
		||||
                true
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns a [MutableSet] of all keys in this map.
 | 
			
		||||
     */
 | 
			
		||||
    override val keys: MutableSet<String>
 | 
			
		||||
        get() = FakeMutableSet.fromSet(mutableSetOf<String>().apply {
 | 
			
		||||
            walk { k, _ ->
 | 
			
		||||
                this += k
 | 
			
		||||
                true
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns a [MutableCollection] of all values in this map. Note that this collection may contain duplicate values.
 | 
			
		||||
     */
 | 
			
		||||
    override val values: MutableCollection<T>
 | 
			
		||||
        get() = FakeMutableCollection.fromCollection(mutableListOf<T>().apply {
 | 
			
		||||
            walk { _, v ->
 | 
			
		||||
                this += v
 | 
			
		||||
                true
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										73
									
								
								app/src/main/java/exh/util/SparseArrayCollection.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								app/src/main/java/exh/util/SparseArrayCollection.kt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,73 @@
 | 
			
		||||
package exh.util
 | 
			
		||||
 | 
			
		||||
import android.util.SparseArray
 | 
			
		||||
import java.util.AbstractMap
 | 
			
		||||
 | 
			
		||||
class SparseArrayKeyCollection(val sparseArray: SparseArray<out Any?>, var reverse: Boolean = false): AbstractCollection<Int>() {
 | 
			
		||||
    override val size get() = sparseArray.size()
 | 
			
		||||
 | 
			
		||||
    override fun iterator() = object : Iterator<Int> {
 | 
			
		||||
        private var index: Int = 0
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Returns `true` if the iteration has more elements.
 | 
			
		||||
         */
 | 
			
		||||
        override fun hasNext() = index < sparseArray.size()
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Returns the next element in the iteration.
 | 
			
		||||
         */
 | 
			
		||||
        override fun next(): Int {
 | 
			
		||||
            var idx = index++
 | 
			
		||||
            if(reverse) idx = sparseArray.size() - 1 - idx
 | 
			
		||||
            return sparseArray.keyAt(idx)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class SparseArrayValueCollection<E>(val sparseArray: SparseArray<E>, var reverse: Boolean = false): AbstractCollection<E>() {
 | 
			
		||||
    override val size get() = sparseArray.size()
 | 
			
		||||
 | 
			
		||||
    override fun iterator() = object : Iterator<E> {
 | 
			
		||||
        private var index: Int = 0
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Returns `true` if the iteration has more elements.
 | 
			
		||||
         */
 | 
			
		||||
        override fun hasNext() = index < sparseArray.size()
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Returns the next element in the iteration.
 | 
			
		||||
         */
 | 
			
		||||
        override fun next(): E {
 | 
			
		||||
            var idx = index++
 | 
			
		||||
            if(reverse) idx = sparseArray.size() - 1 - idx
 | 
			
		||||
            return sparseArray.valueAt(idx)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class SparseArrayCollection<E>(val sparseArray: SparseArray<E>, var reverse: Boolean = false): AbstractCollection<Map.Entry<Int, E>>() {
 | 
			
		||||
    override val size get() = sparseArray.size()
 | 
			
		||||
 | 
			
		||||
    override fun iterator() = object : Iterator<Map.Entry<Int, E>> {
 | 
			
		||||
        private var index: Int = 0
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Returns `true` if the iteration has more elements.
 | 
			
		||||
         */
 | 
			
		||||
        override fun hasNext() = index < sparseArray.size()
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Returns the next element in the iteration.
 | 
			
		||||
         */
 | 
			
		||||
        override fun next(): Map.Entry<Int, E> {
 | 
			
		||||
            var idx = index++
 | 
			
		||||
            if(reverse) idx = sparseArray.size() - 1 - idx
 | 
			
		||||
            return AbstractMap.SimpleImmutableEntry(
 | 
			
		||||
                    sparseArray.keyAt(idx),
 | 
			
		||||
                    sparseArray.valueAt(idx)
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user