add MangaBall (#11344)

* MangaBall

* remove

* suggested changes and more

* remove this

* MangaBall: Fix Korean language code

* change to Locale.ROOT as the pattern isn't language specific

* only throw if filtered
This commit is contained in:
AwkwardPeak7 2025-11-01 12:57:15 +05:00 committed by Draff
parent 9627718a40
commit 68b70d54d9
Signed by: Draff
GPG Key ID: E8A89F3211677653
12 changed files with 783 additions and 0 deletions

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".all.mangaball.UrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:host="mangaball.net" />
<data android:scheme="https" />
<data android:pathPattern="/title-detail/..*" />
<data android:pathPattern="/chapter-detail/..*" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,8 @@
ext {
extName = 'Manga Ball'
extClass = '.MangaBallFactory'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -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<SearchManga>,
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<ChapterContainer>,
)
@Serializable
class ChapterContainer(
@SerialName("number_float")
val number: Float,
val translations: List<Chapter>,
)
@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<Graph>,
) {
@Serializable
class Graph(
@SerialName("@type")
val type: String,
val url: String? = null,
)
}

View File

@ -0,0 +1,201 @@
package eu.kanade.tachiyomi.extension.all.mangaball
import eu.kanade.tachiyomi.source.model.Filter
abstract class SelectFilter<T>(
name: String,
private val options: List<Pair<String, T>>,
) : Filter.Select<String>(
name,
options.map { it.first }.toTypedArray(),
) {
val selected get() = options[state].second
}
class TriStateFilter<T>(name: String, val value: T) : Filter.TriState(name)
abstract class TriStateGroupFilter<T>(
name: String,
options: List<Pair<String, T>>,
) : Filter.Group<TriStateFilter<T>>(
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<String>(
"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<String>(
"Content",
options = listOf(
"Gore" to "685148d115e8b86aae68e4f3",
"Sexual Violence" to "685146c5f3ed681c80f257e7",
),
)
class FormatFilter : TriStateGroupFilter<String>(
"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<String>(
"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<String>(
"Origin",
options = listOf(
"Comic" to "68ecab8507ec62d87e62780f",
"Manga" to "68ecab1e07ec62d87e627806",
"Manhua" to "68ecab4807ec62d87e62780b",
"Manhwa" to "68ecab3b07ec62d87e627809",
),
)
class ThemeFilter : TriStateGroupFilter<String>(
"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<String>(
"Tag Include Mode",
options = listOf(
"AND" to "and",
"OR" to "or",
),
)
class TagExcludeMode : SelectFilter<String>(
"Tag Exclude Mode",
options = listOf(
"AND" to "and",
"OR" to "or",
),
)
class DemographicFilter : SelectFilter<String>(
"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<String>(
"Publication Status",
options = listOf(
"Any" to "any",
"Ongoing" to "ongoing",
"Completed" to "completed",
"Hiatus" to "hiatus",
"Cancelled" to "cancelled",
),
)

View File

@ -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<SortFilter>().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<MangasPage> {
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<SortFilter>().selected)
add("filters[page]", page.toString())
filters.filterIsInstance<TriStateGroupFilter<String>>().forEach { tags ->
tags.included.forEach { tag ->
add("filters[tag_included_ids][]", tag)
}
}
add("filters[tag_included_mode]", filters.firstInstance<TagIncludeMode>().selected)
filters.filterIsInstance<TriStateGroupFilter<String>>().forEach { tags ->
tags.excluded.forEach { tag ->
add("filters[tag_excluded_ids][]", tag)
}
}
add("filters[tag_excluded_mode]", filters.firstInstance<TagExcludeMode>().selected)
add("filters[contentRating]", "any")
add("filters[demographic]", filters.firstInstance<DemographicFilter>().selected)
add("filters[person]", "any")
add("filters[publicationYear]", "")
add("filters[publicationStatus]", filters.firstInstance<StatusFilter>().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<SearchResponse>()
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<MangasPage> {
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<Yoast>()
.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<SChapter> {
(response.request.body as FormBody).also {
updateViews(it.value(0))
}
val data = response.parseAs<ChapterListResponse>()
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<Page> {
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<List<String>>()
.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"

View File

@ -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"),
)
}

View File

@ -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)
}
}