diff --git a/src/all/akuma/build.gradle b/src/all/akuma/build.gradle index b9b0304b1..4ffbdfe15 100644 --- a/src/all/akuma/build.gradle +++ b/src/all/akuma/build.gradle @@ -1,7 +1,7 @@ ext { extName = 'Akuma' - extClass = '.Akuma' - extVersionCode = 1 + extClass = '.AkumaFactory' + extVersionCode = 2 isNsfw = true } diff --git a/src/all/akuma/src/eu/kanade/tachiyomi/extension/all/akuma/Akuma.kt b/src/all/akuma/src/eu/kanade/tachiyomi/extension/all/akuma/Akuma.kt index 71ab2bf1f..c5a357061 100644 --- a/src/all/akuma/src/eu/kanade/tachiyomi/extension/all/akuma/Akuma.kt +++ b/src/all/akuma/src/eu/kanade/tachiyomi/extension/all/akuma/Akuma.kt @@ -1,8 +1,14 @@ package eu.kanade.tachiyomi.extension.all.akuma +import android.app.Application +import android.content.SharedPreferences +import androidx.preference.PreferenceScreen +import androidx.preference.SwitchPreferenceCompat import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.interceptor.rateLimit +import eu.kanade.tachiyomi.source.ConfigurableSource +import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.Page @@ -20,16 +26,19 @@ import okhttp3.Response import org.jsoup.nodes.Document import org.jsoup.nodes.Element import rx.Observable +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import java.io.IOException -class Akuma : ParsedHttpSource() { +class Akuma( + override val lang: String, + private val akumaLang: String, +) : ConfigurableSource, ParsedHttpSource() { override val name = "Akuma" override val baseUrl = "https://akuma.moe" - override val lang = "all" - override val supportsLatest = false private var nextHash: String? = null @@ -38,6 +47,12 @@ class Akuma : ParsedHttpSource() { private val ddosGuardIntercept = DDosGuardInterceptor(network.client) + private val preferences: SharedPreferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } + + private var iconified = preferences.getBoolean(PREF_TAG_GENDER_ICON, false) + override val client: OkHttpClient = network.client.newBuilder() .addInterceptor(ddosGuardIntercept) .addInterceptor(::tokenInterceptor) @@ -102,12 +117,19 @@ class Akuma : ParsedHttpSource() { .add("view", "3") .build() - return if (page == 1) { + val url = baseUrl.toHttpUrlOrNull()!!.newBuilder() + + if (page == 1) { nextHash = null - POST(baseUrl, headers, payload) } else { - POST("$baseUrl/?cursor=$nextHash", headers, payload) + url.addQueryParameter("cursor", nextHash) } + if (lang != "all") { + // append like `q=language:english$` + url.addQueryParameter("q", "language:$akumaLang$") + } + + return POST(url.toString(), headers, payload) } override fun popularMangaSelector() = ".post-loop li" @@ -154,8 +176,39 @@ class Akuma : ParsedHttpSource() { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { val request = popularMangaRequest(page) + val finalQuery = buildString { + append(query) + if (lang != "all") { + append(" language:", akumaLang, "$") + } + filters.filterIsInstance().forEach { filter -> + if (filter.state.isBlank()) return@forEach + filter.state.split(",").forEach { + // append like `a:"eye-covering bang"$` + if (it.startsWith("-")) { + append(" -", filter.identifier, ":", it.trim().substring(1)) + } else { + append(" ", filter.identifier, ":", it.trim()) + } + } + } + filters.filterIsInstance().firstOrNull()?.let { + val filter = options[it.state].second + if (filter.isNotBlank()) { + append(" opt:", filter) + } + } + filters.filterIsInstance().firstOrNull()?.state?.forEach { + if (it.isIncluded()) { + append(" category:\"", it.name, "\"$") + } else if (it.isExcluded()) { + append(" -category:\"", it.name, "\"$") + } + } + } + val url = request.url.newBuilder() - .addQueryParameter("q", query.trim()) + .setQueryParameter("q", finalQuery) .build() return request.newBuilder() @@ -168,14 +221,45 @@ class Akuma : ParsedHttpSource() { override fun searchMangaParse(response: Response) = popularMangaParse(response) override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element) - override fun mangaDetailsParse(document: Document) = SManga.create().apply { - title = document.select(".entry-title").text() - thumbnail_url = document.select(".img-thumbnail").attr("abs:src") - author = document.select("li.meta-data > span.artist + span.value").text() - genre = document.select(".info-list a").joinToString { it.text() } - description = document.select(".pages span.value").text() + " Pages" - update_strategy = UpdateStrategy.ONLY_FETCH_ONCE - status = SManga.COMPLETED + override fun mangaDetailsParse(document: Document) = with(document) { + SManga.create().apply { + title = select(".entry-title").text() + thumbnail_url = select(".img-thumbnail").attr("abs:src") + + author = select(".group~.value").eachText().joinToString() + artist = select(".artist~.value").eachText().joinToString() + + val characters = select(".character~.value").eachText() + val parodies = select(".parody~.value").eachText() + val males = select(".male~.value") + .map { it.text() + if (iconified) " ♂" else " (male)" } + val females = select(".female~.value") + .map { it.text() + if (iconified) " ♀" else " (female)" } + // show all in tags for quickly searching + + genre = (characters + parodies + males + females).joinToString() + description = buildString { + append( + "Full English and Japanese title: \n", + select(".entry-title").text(), + "\n", + select(".entry-title+span").text(), + "\n\n", + ) + + // translated should show up in the description + append("Language: ", select(".language~.value").text(), "\n") + append("Pages: ", select(".pages .value").text(), "\n") + append("Upload Date: ", select(".date .value>time").text(), "\n") + append("Categories: ", selectFirst(".info-list .value")?.text() ?: "Unknown", "\n\n") + + // show followings for easy to reference + append("Parodies: ", parodies.joinToString(), "\n") + append("Characters: ", characters.joinToString(), "\n") + } + update_strategy = UpdateStrategy.ONLY_FETCH_ONCE + status = SManga.UNKNOWN + } } override fun fetchChapterList(manga: SManga): Observable> { @@ -208,8 +292,66 @@ class Akuma : ParsedHttpSource() { return document.select(".entry-content img").attr("abs:src") } + override fun getFilterList(): FilterList = FilterList( + Filter.Header("Separate tags with commas (,)"), + Filter.Header("Prepend with dash (-) to exclude"), + TextFilter("Female Tags", "female"), + TextFilter("Male Tags", "male"), + CategoryFilter(), + TextFilter("Groups", "group"), + TextFilter("Artists", "artist"), + TextFilter("Parody", "parody"), + TextFilter("Characters", "character"), + Filter.Header("Filter by pages, for example: (>20)"), + TextFilter("Pages", "pages"), + Filter.Header("Search in favorites, read, or commented"), + OptionFilter(), + ) + + private class CategoryFilter : Filter.Group("Categories", values()) { + class TagTriState(name: String) : TriState(name) + private companion object { + fun values() = listOf( + TagTriState("doujinshi"), + TagTriState("manga"), + TagTriState("artist cg"), + TagTriState("game cg"), + TagTriState("west"), + TagTriState("non-h"), + TagTriState("gallery"), + TagTriState("cosplay"), + TagTriState("asian pron"), + TagTriState("misc"), + ) + } + } + private class TextFilter(placeholder: String, val identifier: String) : Filter.Text(placeholder) + private class OptionFilter : + Filter.Select("Options", options.map { it.first }.toTypedArray()) + + override fun setupPreferenceScreen(screen: PreferenceScreen) { + SwitchPreferenceCompat(screen.context).apply { + key = PREF_TAG_GENDER_ICON + title = "Show gender as text or icon in tags (requires refresh)" + summaryOff = "Show gender as text" + summaryOn = "Show gender as icon" + + setOnPreferenceChangeListener { _, newValue -> + iconified = newValue == true + true + } + }.also(screen::addPreference) + } + companion object { const val PREFIX_ID = "id:" + private const val PREF_TAG_GENDER_ICON = "pref_tag_gender_icon" + private val options = listOf( + "None" to "", + "Favorited only" to "favorited", + "Read only" to "read", + "Commented only" to "commented", + ) } override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException() diff --git a/src/all/akuma/src/eu/kanade/tachiyomi/extension/all/akuma/AkumaFactory.kt b/src/all/akuma/src/eu/kanade/tachiyomi/extension/all/akuma/AkumaFactory.kt new file mode 100644 index 000000000..45ae32d52 --- /dev/null +++ b/src/all/akuma/src/eu/kanade/tachiyomi/extension/all/akuma/AkumaFactory.kt @@ -0,0 +1,35 @@ +package eu.kanade.tachiyomi.extension.all.akuma + +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.SourceFactory + +class AkumaFactory : SourceFactory { + override fun createSources(): List = listOf( + Akuma("all", "all"), + Akuma("en", "english"), + Akuma("id", "indonesian"), + Akuma("jv", "javanese"), + Akuma("ca", "catalan"), + Akuma("ceb", "cebuano"), + Akuma("cs", "czech"), + Akuma("da", "danish"), + Akuma("de", "german"), + Akuma("et", "estonian"), + Akuma("es", "spanish"), + Akuma("eo", "esperanto"), + Akuma("fr", "french"), + Akuma("it", "italian"), + Akuma("hi", "hindi"), + Akuma("hu", "hungarian"), + Akuma("pl", "polish"), + Akuma("pt", "portuguese"), + Akuma("vi", "vietnamese"), + Akuma("tr", "turkish"), + Akuma("ru", "russian"), + Akuma("uk", "ukrainian"), + Akuma("ar", "arabic"), + Akuma("ko", "korean"), + Akuma("zh", "chinese"), + Akuma("ja", "japanese"), + ) +}