diff --git a/src/all/mangaball/AndroidManifest.xml b/src/all/mangaball/AndroidManifest.xml new file mode 100644 index 000000000..5eff26df2 --- /dev/null +++ b/src/all/mangaball/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + diff --git a/src/all/mangaball/build.gradle b/src/all/mangaball/build.gradle new file mode 100644 index 000000000..1469f3c38 --- /dev/null +++ b/src/all/mangaball/build.gradle @@ -0,0 +1,8 @@ +ext { + extName = 'Manga Ball' + extClass = '.MangaBallFactory' + extVersionCode = 1 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/all/mangaball/res/mipmap-hdpi/ic_launcher.png b/src/all/mangaball/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..03854c11d Binary files /dev/null and b/src/all/mangaball/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/all/mangaball/res/mipmap-mdpi/ic_launcher.png b/src/all/mangaball/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..c1562e10b Binary files /dev/null and b/src/all/mangaball/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/all/mangaball/res/mipmap-xhdpi/ic_launcher.png b/src/all/mangaball/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..c69593789 Binary files /dev/null and b/src/all/mangaball/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/all/mangaball/res/mipmap-xxhdpi/ic_launcher.png b/src/all/mangaball/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..21c53e08a Binary files /dev/null and b/src/all/mangaball/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/all/mangaball/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/mangaball/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..679052f9c Binary files /dev/null and b/src/all/mangaball/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/all/mangaball/src/eu/kanade/tachiyomi/extension/all/mangaball/Dto.kt b/src/all/mangaball/src/eu/kanade/tachiyomi/extension/all/mangaball/Dto.kt new file mode 100644 index 000000000..82b6fbf5e --- /dev/null +++ b/src/all/mangaball/src/eu/kanade/tachiyomi/extension/all/mangaball/Dto.kt @@ -0,0 +1,71 @@ +package eu.kanade.tachiyomi.extension.all.mangaball + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +class SearchResponse( + val data: List, + private val pagination: Pagination, +) { + @Serializable + class Pagination( + @SerialName("current_page") + val currentPage: Int, + @SerialName("last_page") + val lastPage: Int, + ) + + fun hasNextPage() = pagination.currentPage < pagination.lastPage +} + +@Serializable +class SearchManga( + val url: String, + val name: String, + val cover: String, + val isAdult: Boolean, +) + +@Serializable +class ChapterListResponse( + @SerialName("ALL_CHAPTERS") + val chapters: List, +) + +@Serializable +class ChapterContainer( + @SerialName("number_float") + val number: Float, + val translations: List, +) + +@Serializable +class Chapter( + val id: String, + val name: String, + val language: String, + val group: Group, + val date: String, + val volume: Int, +) + +@Serializable +class Group( + @SerialName("_id") + val id: String, + val name: String, +) + +@Serializable +class Yoast( + @SerialName("@graph") + val graph: List, +) { + @Serializable + class Graph( + @SerialName("@type") + val type: String, + val url: String? = null, + ) +} diff --git a/src/all/mangaball/src/eu/kanade/tachiyomi/extension/all/mangaball/Filters.kt b/src/all/mangaball/src/eu/kanade/tachiyomi/extension/all/mangaball/Filters.kt new file mode 100644 index 000000000..c4c646768 --- /dev/null +++ b/src/all/mangaball/src/eu/kanade/tachiyomi/extension/all/mangaball/Filters.kt @@ -0,0 +1,201 @@ +package eu.kanade.tachiyomi.extension.all.mangaball + +import eu.kanade.tachiyomi.source.model.Filter + +abstract class SelectFilter( + name: String, + private val options: List>, +) : Filter.Select( + name, + options.map { it.first }.toTypedArray(), +) { + val selected get() = options[state].second +} + +class TriStateFilter(name: String, val value: T) : Filter.TriState(name) + +abstract class TriStateGroupFilter( + name: String, + options: List>, +) : Filter.Group>( + name, + options.map { TriStateFilter(it.first, it.second) }, +) { + val included get() = state.filter { it.isIncluded() }.map { it.value } + val excluded get() = state.filter { it.isExcluded() }.map { it.value } +} + +class SortFilter : SelectFilter( + "Sort By", + options = listOf( + "Lastest Updated Chapters" to "updated_chapters_desc", + "Oldest Updated Chapters" to "updated_chapters_asc", + "Lastest Created" to "created_at_desc", + "Oldest Created" to "created_at_asc", + "Title A-Z" to "name_asc", + "Title Z-A" to "name_desc", + "Views High to Low" to "views_desc", + "Views Low to High" to "views_asc", + ), +) + +class ContentFilter : TriStateGroupFilter( + "Content", + options = listOf( + "Gore" to "685148d115e8b86aae68e4f3", + "Sexual Violence" to "685146c5f3ed681c80f257e7", + ), +) + +class FormatFilter : TriStateGroupFilter( + "Format", + options = listOf( + "4-Koma" to "685148d115e8b86aae68e4ec", + "Adaptation" to "685148cf15e8b86aae68e4de", + "Anthology" to "685148e915e8b86aae68e558", + "Award Winning" to "685148fe15e8b86aae68e5a7", + "Doujinshi" to "6851490e15e8b86aae68e5da", + "Fan Colored" to "6851498215e8b86aae68e704", + "Full Color" to "685148d615e8b86aae68e502", + "Long Strip" to "685148d915e8b86aae68e517", + "Official Colored" to "6851493515e8b86aae68e64a", + "Oneshot" to "685148eb15e8b86aae68e56c", + "Self-Published" to "6851492e15e8b86aae68e633", + "Web Comic" to "685148d715e8b86aae68e50d", + ), +) + +class GenreFilter : TriStateGroupFilter( + "Genre", + options = listOf( + "Action" to "685146c5f3ed681c80f257e3", + "Adult" to "689371f0a943baf927094f03", + "Adventure" to "685146c5f3ed681c80f257e6", + "Boys' Love" to "685148ef15e8b86aae68e573", + "Comedy" to "685146c5f3ed681c80f257e5", + "Crime" to "685148da15e8b86aae68e51f", + "Drama" to "685148cf15e8b86aae68e4dd", + "Ecchi" to "6892a73ba943baf927094e37", + "Fantasy" to "685146c5f3ed681c80f257ea", + "Girls' Love" to "685148da15e8b86aae68e524", + "Historical" to "685148db15e8b86aae68e527", + "Horror" to "685148da15e8b86aae68e520", + "Isekai" to "685146c5f3ed681c80f257e9", + "Magical Girls" to "6851490d15e8b86aae68e5d4", + "Mature" to "68932d11a943baf927094e7b", + "Mecha" to "6851490c15e8b86aae68e5d2", + "Medical" to "6851494e15e8b86aae68e66e", + "Mystery" to "685148d215e8b86aae68e4f4", + "Philosophical" to "685148e215e8b86aae68e544", + "Psychological" to "685148d715e8b86aae68e507", + "Romance" to "685148cf15e8b86aae68e4db", + "Sci-Fi" to "685148cf15e8b86aae68e4da", + "Shounen Ai" to "689f0ab1f2e66744c6091524", + "Slice of Life" to "685148d015e8b86aae68e4e3", + "Smut" to "689371f2a943baf927094f04", + "Sports" to "685148f515e8b86aae68e588", + "Superhero" to "6851492915e8b86aae68e61c", + "Thriller" to "685148d915e8b86aae68e51e", + "Tragedy" to "685148db15e8b86aae68e529", + "User Created" to "68932c3ea943baf927094e77", + "Wuxia" to "6851490715e8b86aae68e5c3", + "Yaoi" to "68932f68a943baf927094eaa", + "Yuri" to "6896a885a943baf927094f66", + ), +) + +class OriginFilter : TriStateGroupFilter( + "Origin", + options = listOf( + "Comic" to "68ecab8507ec62d87e62780f", + "Manga" to "68ecab1e07ec62d87e627806", + "Manhua" to "68ecab4807ec62d87e62780b", + "Manhwa" to "68ecab3b07ec62d87e627809", + ), +) + +class ThemeFilter : TriStateGroupFilter( + "Theme", + options = listOf( + "Aliens" to "6851490d15e8b86aae68e5d5", + "Animals" to "685148e715e8b86aae68e54b", + "Comics" to "68bf09ff8fdeab0b6a9bc2b7", + "Cooking" to "685148d215e8b86aae68e4f8", + "Crossdressing" to "685148df15e8b86aae68e534", + "Delinquents" to "685148d915e8b86aae68e519", + "Demons" to "685146c5f3ed681c80f257e4", + "Genderswap" to "685148d715e8b86aae68e505", + "Ghosts" to "685148d615e8b86aae68e501", + "Gyaru" to "685148d015e8b86aae68e4e8", + "Harem" to "685146c5f3ed681c80f257e8", + "Hentai" to "68bfceaf4dbc442a26519889", + "Incest" to "685148f215e8b86aae68e584", + "Loli" to "685148d715e8b86aae68e506", + "Mafia" to "685148d915e8b86aae68e518", + "Magic" to "685148d715e8b86aae68e509", + "Manhwa 18+" to "68f5f5ce5f29d3c1863dec3a", + "Martial Arts" to "6851490615e8b86aae68e5c2", + "Military" to "685148e215e8b86aae68e541", + "Monster Girls" to "685148db15e8b86aae68e52c", + "Monsters" to "685146c5f3ed681c80f257e2", + "Music" to "685148d015e8b86aae68e4e4", + "Ninja" to "685148d715e8b86aae68e508", + "Office Workers" to "685148d315e8b86aae68e4fd", + "Police" to "6851498815e8b86aae68e714", + "Post-Apocalyptic" to "685148e215e8b86aae68e540", + "Reincarnation" to "685146c5f3ed681c80f257e1", + "Reverse Harem" to "685148df15e8b86aae68e533", + "Samurai" to "6851490415e8b86aae68e5b9", + "School Life" to "685148d015e8b86aae68e4e7", + "Shota" to "685148d115e8b86aae68e4ed", + "Supernatural" to "685148db15e8b86aae68e528", + "Survival" to "685148cf15e8b86aae68e4dc", + "Time Travel" to "6851490c15e8b86aae68e5d1", + "Traditional Games" to "6851493515e8b86aae68e645", + "Vampires" to "685148f915e8b86aae68e597", + "Video Games" to "685148e115e8b86aae68e53c", + "Villainess" to "6851492115e8b86aae68e602", + "Virtual Reality" to "68514a1115e8b86aae68e83e", + "Zombies" to "6851490c15e8b86aae68e5d3", + ), +) + +class TagIncludeMode : SelectFilter( + "Tag Include Mode", + options = listOf( + "AND" to "and", + "OR" to "or", + ), +) + +class TagExcludeMode : SelectFilter( + "Tag Exclude Mode", + options = listOf( + "AND" to "and", + "OR" to "or", + ), +) + +class DemographicFilter : SelectFilter( + "Magazine Demographic", + options = listOf( + "Any" to "any", + "Shounen" to "shounen", + "Shoujo" to "shoujo", + "Seinen" to "seinen", + "Josei" to "josei", + "Yuri" to "yuri", + "Yaoi" to "yaoi", + ), +) + +class StatusFilter : SelectFilter( + "Publication Status", + options = listOf( + "Any" to "any", + "Ongoing" to "ongoing", + "Completed" to "completed", + "Hiatus" to "hiatus", + "Cancelled" to "cancelled", + ), +) diff --git a/src/all/mangaball/src/eu/kanade/tachiyomi/extension/all/mangaball/MangaBall.kt b/src/all/mangaball/src/eu/kanade/tachiyomi/extension/all/mangaball/MangaBall.kt new file mode 100644 index 000000000..e4bdc0956 --- /dev/null +++ b/src/all/mangaball/src/eu/kanade/tachiyomi/extension/all/mangaball/MangaBall.kt @@ -0,0 +1,403 @@ +package eu.kanade.tachiyomi.extension.all.mangaball + +import android.util.Log +import androidx.preference.PreferenceScreen +import androidx.preference.SwitchPreferenceCompat +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.source.ConfigurableSource +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 keiyoushi.utils.firstInstance +import keiyoushi.utils.getPreferencesLazy +import keiyoushi.utils.parseAs +import keiyoushi.utils.tryParse +import okhttp3.Call +import okhttp3.Callback +import okhttp3.FormBody +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Request +import okhttp3.Response +import okhttp3.internal.closeQuietly +import okio.IOException +import org.jsoup.nodes.Document +import rx.Observable +import java.lang.UnsupportedOperationException +import java.text.SimpleDateFormat +import java.util.Locale + +class MangaBall( + override val lang: String, + private vararg val siteLang: String, +) : HttpSource(), ConfigurableSource { + + override val name = "Manga Ball" + override val baseUrl = "https://mangaball.net" + override val supportsLatest = true + private val preferences by getPreferencesLazy() + + override val client = network.cloudflareClient.newBuilder() + .addInterceptor { chain -> + var request = chain.request() + if (request.url.pathSegments[0] == "api") { + request = request.newBuilder() + .header("X-Requested-With", "XMLHttpRequest") + .header("X-CSRF-TOKEN", getCSRF()) + .build() + + val response = chain.proceed(request) + if (!response.isSuccessful && response.code == 403) { + response.close() + request = request.newBuilder() + .header("X-CSRF-TOKEN", getCSRF(forceReset = true)) + .build() + + chain.proceed(request) + } else { + response + } + } else { + chain.proceed(request) + } + } + .build() + + private var _csrf: String? = null + + @Synchronized + private fun getCSRF(document: Document? = null, forceReset: Boolean = false): String { + if (_csrf == null || document != null || forceReset) { + val doc = document ?: client.newCall( + GET(baseUrl, headers), + ).execute().asJsoup() + + doc.selectFirst("meta[name=csrf-token]") + ?.attr("content") + ?.takeIf { it.isNotBlank() } + ?.also { _csrf = it } + } + + return _csrf ?: throw Exception("CSRF token not found") + } + + override fun headersBuilder() = super.headersBuilder() + .set("Referer", "$baseUrl/") + + override fun popularMangaRequest(page: Int): Request { + val filters = getFilterList().apply { + firstInstance().state = 6 + } + + return searchMangaRequest(page, "", filters) + } + + override fun popularMangaParse(response: Response) = + searchMangaParse(response) + + override fun latestUpdatesRequest(page: Int) = + searchMangaRequest(page, "", getFilterList()) + + override fun latestUpdatesParse(response: Response) = + searchMangaParse(response) + + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { + return if (query.startsWith("https://")) { + deepLink(query) + } else { + super.fetchSearchManga(page, query, filters) + } + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val body = FormBody.Builder().apply { + add("search_input", query.trim()) + add("filters[sort]", filters.firstInstance().selected) + add("filters[page]", page.toString()) + filters.filterIsInstance>().forEach { tags -> + tags.included.forEach { tag -> + add("filters[tag_included_ids][]", tag) + } + } + add("filters[tag_included_mode]", filters.firstInstance().selected) + filters.filterIsInstance>().forEach { tags -> + tags.excluded.forEach { tag -> + add("filters[tag_excluded_ids][]", tag) + } + } + add("filters[tag_excluded_mode]", filters.firstInstance().selected) + add("filters[contentRating]", "any") + add("filters[demographic]", filters.firstInstance().selected) + add("filters[person]", "any") + add("filters[publicationYear]", "") + add("filters[publicationStatus]", filters.firstInstance().selected) + siteLang.forEach { + add("filters[translatedLanguage][]", it) + } + }.build() + + return POST("$baseUrl/api/v1/title/search-advanced/", headers, body) + } + + override fun getFilterList() = FilterList( + SortFilter(), + DemographicFilter(), + StatusFilter(), + ContentFilter(), + FormatFilter(), + GenreFilter(), + OriginFilter(), + ThemeFilter(), + TagIncludeMode(), + TagExcludeMode(), + ) + + override fun searchMangaParse(response: Response): MangasPage { + val data = response.parseAs() + val hideNsfw = hideNsfwPreference() + + val mangas = data.data + .filterNot { + it.isAdult && hideNsfw + } + .map { + SManga.create().apply { + url = it.url.toHttpUrl().pathSegments[1] + title = it.name + thumbnail_url = it.cover + } + } + + if (mangas.isEmpty() && hideNsfw) { + throw Exception("All results filtered out due to nsfw filter") + } + + return MangasPage(mangas, data.hasNextPage()) + } + + private fun deepLink(url: String): Observable { + val httpUrl = url.toHttpUrl() + if ( + httpUrl.host == baseUrl.toHttpUrl().host && + httpUrl.pathSegments.size >= 2 && + httpUrl.pathSegments[0] in listOf("title-detail", "chapter-detail") + ) { + val slug = if (httpUrl.pathSegments[0] == "title-detail") { + httpUrl.pathSegments[1] + } else { + client.newCall(GET(httpUrl, headers)).execute() + .use { response -> + response.asJsoup() + .selectFirst(".yoast-schema-graph")!!.data() + .parseAs() + .graph.first { it.type == "WebPage" } + .url!!.toHttpUrl() + .pathSegments[1] + } + } + + val manga = SManga.create().apply { + this.url = slug + } + + return fetchMangaDetails(manga).map { + MangasPage(listOf(it), false) + } + } + + throw Exception("Unsupported url") + } + + override fun mangaDetailsRequest(manga: SManga): Request { + return GET(getMangaUrl(manga), headers) + } + + override fun getMangaUrl(manga: SManga): String { + return "$baseUrl/title-detail/${manga.url}/" + } + + override fun mangaDetailsParse(response: Response): SManga { + val document = response.asJsoup() + getCSRF(document) + + return SManga.create().apply { + url = document.location().toHttpUrl().pathSegments[1] + title = document.selectFirst("#comicDetail h6")!!.ownText() + thumbnail_url = document.selectFirst("img.featured-cover")?.absUrl("src") + genre = buildList { + document.selectFirst("#featuredComicsCarousel img[src*=/flags/]") + ?.attr("src")?.also { + when { + it.contains("jp") -> add("Manga") + it.contains("kr") -> add("Manhwa") + it.contains("cn") -> add("Manhua") + } + } + document.select("#comicDetail span[data-tag-id]") + .mapTo(this) { it.ownText() } + }.joinToString() + author = document.select("#comicDetail span[data-person-id]") + .eachText().joinToString() + description = buildString { + document.selectFirst("#descriptionContent p") + ?.also { append(it.wholeText()) } + document.selectFirst("#comicDetail span.badge:contains(Published)") + ?.also { append("\n\n", it.text()) } + val titles = document.select("div.alternate-name-container").text().split("/") + if (titles.isNotEmpty()) { + append("\n\nAlternative Names: \n") + titles.forEach { + append("- ", it.trim(), "\n") + } + } + }.trim() + status = when (document.selectFirst("span.badge-status")?.text()) { + "Ongoing" -> SManga.ONGOING + "Completed" -> SManga.COMPLETED + "Hiatus" -> SManga.ON_HIATUS + "Cancelled" -> SManga.CANCELLED + else -> SManga.UNKNOWN + } + } + } + + override fun chapterListRequest(manga: SManga): Request { + val id = manga.url.substringAfterLast("-") + val body = FormBody.Builder() + .add("title_id", id) + .build() + + return POST("$baseUrl/api/v1/chapter/chapter-listing-by-title-id/", headers, body) + } + + override fun chapterListParse(response: Response): List { + (response.request.body as FormBody).also { + updateViews(it.value(0)) + } + + val data = response.parseAs() + + return data.chapters.flatMap { chapter -> + chapter.translations.mapNotNull { translation -> + if (translation.language in siteLang) { + SChapter.create().apply { + url = translation.id + name = buildString { + if (translation.volume > 0) { + append("Vol. ") + append(translation.volume) + append(" ") + } + val number = chapter.number.toString().removeSuffix(".0") + if (translation.name.contains(number)) { + append(translation.name.trim()) + } else { + append("Ch. ") + append(number) + append(" ") + append(translation.name.trim()) + } + } + chapter_number = chapter.number + date_upload = dateFormat.tryParse(translation.date) + scanlator = buildString { + append(translation.group.name) + // id is usually the name of the site the chapter was scraped from + // if not then it is generated id of an active group on the site + if (groupIdRegex.matchEntire(translation.group.id) == null) { + append(" (") + append(translation.group.id) + append(")") + } + } + } + } else { + null + } + } + } + } + + private val groupIdRegex = Regex("""[a-z0-9]{24}""") + + private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ROOT) + + override fun pageListRequest(chapter: SChapter): Request { + return GET(getChapterUrl(chapter), headers) + } + + override fun getChapterUrl(chapter: SChapter): String { + return "$baseUrl/chapter-detail/${chapter.url}/" + } + + override fun pageListParse(response: Response): List { + val document = response.asJsoup() + getCSRF(document) + + document.select("script:containsData(titleId)").joinToString(";") { it.data() }.also { + val titleId = titleIdRegex.find(it) + ?.groupValues?.get(1) + ?: return@also + val chapterId = chapterIdRegex.find(it) + ?.groupValues?.get(1) + ?: return@also + + updateViews(titleId, chapterId) + } + + val script = document.select("script:containsData(chapterImages)").joinToString(";") { it.data() } + val images = imagesRegex.find(script) + ?.groupValues?.get(1) + ?.parseAs>() + .orEmpty() + + return images.mapIndexed { idx, img -> + Page(idx, imageUrl = img) + } + } + + private val imagesRegex = Regex("""const\s+chapterImages\s*=\s*JSON\.parse\(`([^`]+)`\)""") + private val titleIdRegex = Regex("""const\s+titleId\s*=\s*`([^`]+)`;""") + private val chapterIdRegex = Regex("""const\s+chapterId\s*=\s*`([^`]+)`;""") + + private fun updateViews(titleId: String, chapterId: String = "") { + val body = FormBody.Builder() + .add("title_id", titleId) + .add("chapter_id", chapterId) + .build() + + val request = POST("$baseUrl/api/v1/views/update/", headers, body) + + client.newCall(request) + .enqueue( + object : Callback { + override fun onResponse(call: Call, response: Response) { + response.closeQuietly() + } + override fun onFailure(call: Call, e: IOException) { + Log.e(name, "Failed to update views", e) + } + }, + ) + } + + override fun setupPreferenceScreen(screen: PreferenceScreen) { + SwitchPreferenceCompat(screen.context).apply { + key = NSFW_PREF + title = "Hide NSFW content" + setDefaultValue(false) + }.also(screen::addPreference) + } + + private fun hideNsfwPreference() = preferences.getBoolean(NSFW_PREF, false) + + override fun imageUrlParse(response: Response): String { + throw UnsupportedOperationException() + } +} + +private const val NSFW_PREF = "nsfw_pref" diff --git a/src/all/mangaball/src/eu/kanade/tachiyomi/extension/all/mangaball/MangaBallFactory.kt b/src/all/mangaball/src/eu/kanade/tachiyomi/extension/all/mangaball/MangaBallFactory.kt new file mode 100644 index 000000000..e1a4a4ed6 --- /dev/null +++ b/src/all/mangaball/src/eu/kanade/tachiyomi/extension/all/mangaball/MangaBallFactory.kt @@ -0,0 +1,50 @@ +package eu.kanade.tachiyomi.extension.all.mangaball + +import eu.kanade.tachiyomi.source.SourceFactory + +class MangaBallFactory : SourceFactory { + override fun createSources() = listOf( + MangaBall("ar", "ar"), + MangaBall("bg", "bg"), + MangaBall("bn", "bn"), + MangaBall("ca", "ca", "ca-ad", "ca-es", "ca-fr", "ca-it", "ca-pt"), + MangaBall("cs", "cs"), + MangaBall("da", "da"), + MangaBall("de", "de"), + MangaBall("el", "el"), + MangaBall("en", "en"), + MangaBall("es", "es", "es-ar", "es-mx", "es-es", "es-la", "es-419"), + MangaBall("fa", "fa"), + MangaBall("fi", "fi"), + MangaBall("fr", "fr"), + MangaBall("he", "he"), + MangaBall("hi", "hi"), + MangaBall("hu", "hu"), + MangaBall("id", "id"), + MangaBall("it", "it", "it-it"), + MangaBall("is", "ib", "ib-is", "is"), + MangaBall("ja", "jp"), + MangaBall("ko", "kr"), + MangaBall("kn", "kn", "kn-in", "kn-my", "kn-sg", "kn-tw"), + MangaBall("ml", "ml", "ml-in", "ml-my", "ml-sg", "ml-tw"), + MangaBall("ms", "ms"), + MangaBall("ne", "ne"), + MangaBall("nl", "nl", "nl-be"), + MangaBall("no", "no"), + MangaBall("pl", "pl"), + MangaBall("pt-BR", "pt-br", "pt-pt"), + MangaBall("ro", "ro"), + MangaBall("ru", "ru"), + MangaBall("sk", "sk"), + MangaBall("sl", "sl"), + MangaBall("sq", "sq"), + MangaBall("sr", "sr", "sr-cyrl"), + MangaBall("sv", "sv"), + MangaBall("ta", "ta"), + MangaBall("th", "th", "th-hk", "th-kh", "th-la", "th-my", "th-sg"), + MangaBall("tr", "tr"), + MangaBall("uk", "uk"), + MangaBall("vi", "vi"), + MangaBall("zh", "zh", "zh-cn", "zh-hk", "zh-mo", "zh-sg", "zh-tw"), + ) +} diff --git a/src/all/mangaball/src/eu/kanade/tachiyomi/extension/all/mangaball/UrlActivity.kt b/src/all/mangaball/src/eu/kanade/tachiyomi/extension/all/mangaball/UrlActivity.kt new file mode 100644 index 000000000..33e5b18e9 --- /dev/null +++ b/src/all/mangaball/src/eu/kanade/tachiyomi/extension/all/mangaball/UrlActivity.kt @@ -0,0 +1,29 @@ +package eu.kanade.tachiyomi.extension.all.mangaball + +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.Intent +import android.os.Bundle +import android.util.Log +import kotlin.system.exitProcess + +class UrlActivity : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val mainIntent = Intent().apply { + action = "eu.kanade.tachiyomi.SEARCH" + putExtra("query", intent.data.toString()) + putExtra("filter", packageName) + } + + try { + startActivity(mainIntent) + } catch (e: ActivityNotFoundException) { + Log.e("MangaBall", "Unable to launch activity", e) + } + + finish() + exitProcess(0) + } +}