diff --git a/src/all/comicfury/AndroidManifest.xml b/src/all/comicfury/AndroidManifest.xml new file mode 100644 index 000000000..30deb7f79 --- /dev/null +++ b/src/all/comicfury/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/src/all/comicfury/build.gradle b/src/all/comicfury/build.gradle new file mode 100644 index 000000000..daf7c52bd --- /dev/null +++ b/src/all/comicfury/build.gradle @@ -0,0 +1,17 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlinx-serialization' + +ext { + extName = 'Comic Fury' + pkgNameSuffix = 'all.comicfury' + extClass = '.ComicFuryFactory' + extVersionCode = 1 + isNsfw = true +} + +dependencies { + implementation(project(':lib-textinterceptor')) +} + +apply from: "$rootDir/common.gradle" diff --git a/src/all/comicfury/res/mipmap-hdpi/ic_launcher.png b/src/all/comicfury/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..865ccdc5f Binary files /dev/null and b/src/all/comicfury/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/all/comicfury/res/mipmap-mdpi/ic_launcher.png b/src/all/comicfury/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..436075be2 Binary files /dev/null and b/src/all/comicfury/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/all/comicfury/res/mipmap-xhdpi/ic_launcher.png b/src/all/comicfury/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..2e7393ab7 Binary files /dev/null and b/src/all/comicfury/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/all/comicfury/res/mipmap-xxhdpi/ic_launcher.png b/src/all/comicfury/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..a5c573216 Binary files /dev/null and b/src/all/comicfury/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/all/comicfury/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/comicfury/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..1188a64c5 Binary files /dev/null and b/src/all/comicfury/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/all/comicfury/res/web_hi_res_512.png b/src/all/comicfury/res/web_hi_res_512.png new file mode 100644 index 000000000..9f721a498 Binary files /dev/null and b/src/all/comicfury/res/web_hi_res_512.png differ diff --git a/src/all/comicfury/src/eu/kanade/tachiyomi/extension/all/comicfury/ComicFury.kt b/src/all/comicfury/src/eu/kanade/tachiyomi/extension/all/comicfury/ComicFury.kt new file mode 100644 index 000000000..dc7b3b82a --- /dev/null +++ b/src/all/comicfury/src/eu/kanade/tachiyomi/extension/all/comicfury/ComicFury.kt @@ -0,0 +1,290 @@ +package eu.kanade.tachiyomi.extension.all.comicfury + +import android.app.Application +import android.content.SharedPreferences +import androidx.preference.PreferenceScreen +import androidx.preference.SwitchPreferenceCompat +import eu.kanade.tachiyomi.lib.textinterceptor.TextInterceptor +import eu.kanade.tachiyomi.lib.textinterceptor.TextInterceptorHelper +import eu.kanade.tachiyomi.network.GET +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 +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.util.asJsoup +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Request +import okhttp3.Response +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.text.SimpleDateFormat +import java.util.Locale + +class ComicFury( + override val lang: String, + private val siteLang: String = lang, // override lang string used in MangaSearch + private val extraName: String = "", +) : HttpSource(), ConfigurableSource { + override val baseUrl: String = "https://comicfury.com" + override val name: String = "Comic Fury$extraName" //Used for No Text + override val supportsLatest: Boolean = true + private val dateFormat = SimpleDateFormat("dd MMM yyyy hh:mm aa", Locale.US) + private val dateFormatSlim = SimpleDateFormat("dd MMM yyyy", Locale.US) + + override val client = super.client.newBuilder().addInterceptor(TextInterceptor()).build() + + /** + * Archive is on a separate page from manga info + */ + override fun chapterListRequest(manga: SManga): Request = + GET("$baseUrl/read/${manga.url.substringAfter("?url=")}/archive") + + /** + * Open Archive Url instead of the details page + * Helps with getting past the nfsw pages + */ + override fun getMangaUrl(manga: SManga): String { + return "$baseUrl/read/" + manga.url.substringAfter("?url=") + "/archive" + } + + /** + * There are two different ways chapters are setup + * First Way if (true) + * Manga -> Chapter -> Comic -> Pages + * The Second Way if (false) + * Manga -> Comic -> Pages + * + * Importantly the Chapter And Comic Pages can be easy distinguished + * by the name of the list elements in this case archive-chapter/archive-comic + * + * For Manga that doesn't have "chapters" skip the loop. Including All Sub-Comics of Chapters + * + * Put the chapter name into scanlator so read can know what chapter it is. + * + * Chapter Number is handled as Chapter dot Comic. Ex. Chapter 6, Comic 4: chapter_number = 6.4 + * + */ + override fun chapterListParse(response: Response): List { + val jsp = response.asJsoup() + if (jsp.selectFirst("div.archive-chapter") != null) { + val chapters: MutableList = arrayListOf() + for (chapter in jsp.select("div.archive-chapter").parents().reversed()) { + val name = chapter.text() + chapters.addAll( + client.newCall( + GET("$baseUrl${chapter.attr("href")}"), + ).execute() + .use { chapterListParse(it) } + .mapIndexed { i, it -> + it.apply { + scanlator = name + chapter_number += i + } + }, + ) + } + return chapters + } else { + return jsp.select("div.archive-comic").mapIndexed { i, it -> + SChapter.create().apply { + url = it.parent()!!.attr("href") + name = it.child(0).ownText() + date_upload = it.child(1).ownText().toDate() + chapter_number = "0.$i".toFloat() + } + }.toList().reversed() + } + } + + override fun pageListParse(response: Response): List { + val jsp = response.asJsoup() + val pages: MutableList = arrayListOf() + val comic = jsp.selectFirst("div.is--comic-page") + for (child in comic!!.select("div.is--image-segment div img")) { + pages.add( + Page( + pages.size, + response.request.url.toString(), + child.attr("src"), + ), + ) + } + if (showAuthorsNotesPref()) { + for (child in comic.select("div.is--author-notes div.is--comment-box").withIndex()) { + pages.add( + Page( + pages.size, + response.request.url.toString(), + TextInterceptorHelper.createUrl( + jsp.selectFirst("a.is--comment-author")?.ownText() + ?: "Error No Author For Comment Found", + jsp.selectFirst("div.is--comment-content")?.html() + ?: "Error No Comment Content Found", + ), + ), + ) + } + } + return pages + } + + /** + * Author name joining maybe redundant. + * + * Manga Status is available but not currently implemented. + */ + override fun mangaDetailsParse(response: Response): SManga { + val jsp = response.asJsoup() + val desDiv = jsp.selectFirst("div.description-tags") + return SManga.create().apply { + setUrlWithoutDomain(response.request.url.toString()) + description = desDiv?.parent()?.ownText() + genre = desDiv?.children()?.eachText()?.joinToString(", ") + author = jsp.select("a.authorname").eachText().joinToString(", ") + initialized = true + } + } + + override fun searchMangaParse(response: Response): MangasPage { + val jsp = response.asJsoup() + val list: MutableList = arrayListOf() + for (result in jsp.select("div.webcomic-result")) { + list.add( + SManga.create().apply { + url = result.selectFirst("div.webcomic-result-avatar a")!!.attr("href") + title = result.selectFirst("div.webcomic-result-title")!!.attr("title") + thumbnail_url = result.selectFirst("div.webcomic-result-avatar a img")!!.absUrl("src") + }, + ) + } + return MangasPage(list, (jsp.selectFirst("div.search-next-page") != null)) + } + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val req: HttpUrl.Builder = "$baseUrl/search.php".toHttpUrl().newBuilder() + req.addQueryParameter("page", page.toString()) + req.addQueryParameter("language", siteLang) + filters.forEach { + when (it) { + is TagsFilter -> req.addEncodedQueryParameter( + "tags", + it.state.replace(", ", ","), + ) + is SortFilter -> req.addQueryParameter("sort", it.state.toString()) + is CompletedComicFilter -> req.addQueryParameter( + "completed", + it.state.toInt().toString(), + ) + is LastUpdatedFilter -> req.addQueryParameter( + "lastupdate", + it.state.toString(), + ) + is ViolenceFilter -> req.addQueryParameter("fv", it.state.toString()) + is NudityFilter -> req.addQueryParameter("fn", it.state.toString()) + is StrongLangFilter -> req.addQueryParameter("fl", it.state.toString()) + is SexualFilter -> req.addQueryParameter("fs", it.state.toString()) + else -> {} + } + } + + return Request.Builder().url(req.build()).build() + } + + // START OF AUTHOR NOTES // + private val preferences: SharedPreferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } + companion object { + private const val SHOW_AUTHORS_NOTES_KEY = "showAuthorsNotes" + } + private fun showAuthorsNotesPref() = + preferences.getBoolean(SHOW_AUTHORS_NOTES_KEY, false) + + override fun setupPreferenceScreen(screen: PreferenceScreen) { + val authorsNotesPref = SwitchPreferenceCompat(screen.context).apply { + key = SHOW_AUTHORS_NOTES_KEY; title = "Show author's notes" + summary = "Enable to see the author's notes at the end of chapters (if they're there)." + setDefaultValue(false) + } + screen.addPreference(authorsNotesPref) + } + // END OF AUTHOR NOTES // + + // START OF FILTERS // + override fun getFilterList(): FilterList = getFilterList(0) + private fun getFilterList(sortIndex: Int): FilterList = FilterList( + TagsFilter(), + Filter.Separator(), + SortFilter(sortIndex), + Filter.Separator(), + LastUpdatedFilter(), + CompletedComicFilter(), + Filter.Separator(), + Filter.Header("Flags"), + ViolenceFilter(), + NudityFilter(), + StrongLangFilter(), + SexualFilter(), + ) + + internal class SortFilter(index: Int) : Filter.Select( + "Sort By", + arrayOf("Relevance", "Popularity", "Last Update"), + index, + ) + internal class CompletedComicFilter : Filter.CheckBox("Comic Completed", false) + internal class LastUpdatedFilter : Filter.Select( + "Last Updated", + arrayOf("All Time", "This Week", "This Month", "This Year", "Completed Only"), + 0, + ) + internal class ViolenceFilter : Filter.Select( + "Violence", + arrayOf("None / Minimal", "Violent Content", "Gore / Graphic"), + 2, + ) + internal class NudityFilter : Filter.Select( + "Frontal Nudity", + arrayOf("None", "Occasional", "Frequent"), + 2, + ) + internal class StrongLangFilter : Filter.Select( + "Strong Language", + arrayOf("None", "Occasional", "Frequent"), + 2, + ) + internal class SexualFilter : Filter.Select( + "Sexual Content", + arrayOf("No Sexual Content", "Sexual Situations", "Strong Sexual Themes"), + 2, + ) + internal class TagsFilter : Filter.Text("Tags") + + // END OF FILTERS // + + override fun popularMangaRequest(page: Int): Request = + searchMangaRequest(page, "", getFilterList(1)) + override fun popularMangaParse(response: Response): MangasPage = searchMangaParse(response) + + override fun latestUpdatesRequest(page: Int): Request = + searchMangaRequest(page, "", getFilterList(2)) + override fun latestUpdatesParse(response: Response): MangasPage = searchMangaParse(response) + + override fun imageUrlParse(response: Response): String = + throw UnsupportedOperationException("Not Used") + + private fun String.toDate(): Long { + val ret = this.replace("st", "") + .replace("nd", "") + .replace("rd", "") + .replace("th", "") + .replace(",", "") + return dateFormat.parse(ret)?.time ?: dateFormatSlim.parse(ret)!!.time + } + + private fun Boolean.toInt(): Int = if (this) { 0 } else { 1 } +} diff --git a/src/all/comicfury/src/eu/kanade/tachiyomi/extension/all/comicfury/ComicFuryFactory.kt b/src/all/comicfury/src/eu/kanade/tachiyomi/extension/all/comicfury/ComicFuryFactory.kt new file mode 100644 index 000000000..cb18b496d --- /dev/null +++ b/src/all/comicfury/src/eu/kanade/tachiyomi/extension/all/comicfury/ComicFuryFactory.kt @@ -0,0 +1,23 @@ +package eu.kanade.tachiyomi.extension.all.comicfury + +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.SourceFactory + +class ComicFuryFactory : SourceFactory { + override fun createSources(): List = listOf( + ComicFury("all"), + ComicFury("en"), + ComicFury("es"), + ComicFury("pt-BR", "pt"), + ComicFury("de"), + ComicFury("fr"), + ComicFury("it"), + ComicFury("pl"), + ComicFury("ja"), + ComicFury("zh"), + ComicFury("ru"), + ComicFury("fi"), + ComicFury("other"), + ComicFury("other", "notext", " (No Text)"), + ) +}