diff --git a/src/all/ninenineninehentai/AndroidManifest.xml b/src/all/ninenineninehentai/AndroidManifest.xml new file mode 100644 index 000000000..733320219 --- /dev/null +++ b/src/all/ninenineninehentai/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/all/ninenineninehentai/build.gradle b/src/all/ninenineninehentai/build.gradle new file mode 100644 index 000000000..5f7f2d5e3 --- /dev/null +++ b/src/all/ninenineninehentai/build.gradle @@ -0,0 +1,13 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlinx-serialization' + +ext { + extName = '999Hentai' + pkgNameSuffix = 'all.ninenineninehentai' + extClass = '.NineNineNineHentaiFactory' + extVersionCode = 1 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" \ No newline at end of file diff --git a/src/all/ninenineninehentai/res/mipmap-hdpi/ic_launcher.png b/src/all/ninenineninehentai/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..f5d448ee4 Binary files /dev/null and b/src/all/ninenineninehentai/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/all/ninenineninehentai/res/mipmap-mdpi/ic_launcher.png b/src/all/ninenineninehentai/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..929615309 Binary files /dev/null and b/src/all/ninenineninehentai/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/all/ninenineninehentai/res/mipmap-xhdpi/ic_launcher.png b/src/all/ninenineninehentai/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..2a76504d5 Binary files /dev/null and b/src/all/ninenineninehentai/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/all/ninenineninehentai/res/mipmap-xxhdpi/ic_launcher.png b/src/all/ninenineninehentai/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..8df4c773f Binary files /dev/null and b/src/all/ninenineninehentai/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/all/ninenineninehentai/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/ninenineninehentai/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..0f3523c57 Binary files /dev/null and b/src/all/ninenineninehentai/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/all/ninenineninehentai/res/web_hi_res_512.png b/src/all/ninenineninehentai/res/web_hi_res_512.png new file mode 100644 index 000000000..7e94372b4 Binary files /dev/null and b/src/all/ninenineninehentai/res/web_hi_res_512.png differ diff --git a/src/all/ninenineninehentai/src/eu/kanade/tachiyomi/extension/all/ninenineninehentai/NineNineHentaiFilters.kt b/src/all/ninenineninehentai/src/eu/kanade/tachiyomi/extension/all/ninenineninehentai/NineNineHentaiFilters.kt new file mode 100644 index 000000000..0526b14ff --- /dev/null +++ b/src/all/ninenineninehentai/src/eu/kanade/tachiyomi/extension/all/ninenineninehentai/NineNineHentaiFilters.kt @@ -0,0 +1,69 @@ +package eu.kanade.tachiyomi.extension.all.ninenineninehentai + +import eu.kanade.tachiyomi.source.model.Filter +import eu.kanade.tachiyomi.source.model.FilterList + +abstract class SelectFilter( + displayName: String, + private val options: Array>, +) : Filter.Select( + displayName, + options.map { it.first }.toTypedArray(), +) { + val selected get() = options[state].second.takeUnless { it.isEmpty() } +} + +abstract class TextFilter(name: String) : Filter.Text(name) + +abstract class TagFilter(name: String) : TextFilter(name) { + val tags get() = state.split(",") + .map { it.trim().lowercase() } + .filter { it.isNotEmpty() } + .takeUnless { it.isEmpty() } +} + +abstract class PageFilter(name: String) : TextFilter(name) { + val value get() = state.trim().toIntOrNull() +} + +class SortFilter : SelectFilter( + "Sort By", + arrayOf( + Pair("Update", ""), + Pair("Popular", "Popular"), + Pair("Top", "Top"), + Pair("Name Ascending", "Name_ASC"), + Pair("Name Descending", "Name_DESC"), + ), +) + +class FormatFilter : SelectFilter( + "Format", + arrayOf( + Pair("", ""), + Pair("Manga", "manga"), + Pair("Doujinshi", "doujinshi"), + Pair("ArtistCG", "artistcg"), + Pair("GameCG", "gamecg"), + ), +) + +class MinPageFilter : PageFilter("Minimum Pages") + +class MaxPageFilter : PageFilter("Maximum Pages") + +class IncludedTagFilter : TagFilter("Include Tags") + +class ExcludedTagFilter : TagFilter("Exclude Tags") + +fun getFilters() = FilterList( + SortFilter(), + FormatFilter(), + Filter.Separator(), + MinPageFilter(), + MaxPageFilter(), + Filter.Separator(), + IncludedTagFilter(), + ExcludedTagFilter(), + Filter.Header("comma (,) separated tag/parody/character/artist/group"), +) diff --git a/src/all/ninenineninehentai/src/eu/kanade/tachiyomi/extension/all/ninenineninehentai/NineNineNineHentai.kt b/src/all/ninenineninehentai/src/eu/kanade/tachiyomi/extension/all/ninenineninehentai/NineNineNineHentai.kt new file mode 100644 index 000000000..230091d6d --- /dev/null +++ b/src/all/ninenineninehentai/src/eu/kanade/tachiyomi/extension/all/ninenineninehentai/NineNineNineHentai.kt @@ -0,0 +1,305 @@ +package eu.kanade.tachiyomi.extension.all.ninenineninehentai + +import android.annotation.SuppressLint +import android.app.Application +import android.content.SharedPreferences +import androidx.preference.ListPreference +import androidx.preference.PreferenceScreen +import eu.kanade.tachiyomi.extension.all.ninenineninehentai.Url.Companion.toAbsUrl +import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.network.asObservableSuccess +import eu.kanade.tachiyomi.network.interceptor.rateLimit +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 kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import okhttp3.Headers +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import rx.Observable +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import uy.kohesive.injekt.injectLazy +import java.text.SimpleDateFormat +import java.util.Locale + +open class NineNineNineHentai( + final override val lang: String, + private val siteLang: String = lang, +) : HttpSource(), ConfigurableSource { + + override val name = "999Hentai" + + override val baseUrl = "https://999hentai.to" + + private val apiUrl = "https://api.999hentai.to/api" + + override val supportsLatest = true + + private val json: Json by injectLazy() + + override val client = network.cloudflareClient.newBuilder() + .rateLimit(1) + .build() + + private val preference by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } + + override fun popularMangaRequest(page: Int): Request { + val payload = GraphQL( + PopularVariables(size, page, 1, siteLang), + POPULAR_QUERY, + ) + + val requestBody = payload.toJsonRequestBody() + + val apiHeaders = headersBuilder().buildApiHeaders(requestBody) + + return POST(apiUrl, apiHeaders, requestBody) + } + + override fun popularMangaParse(response: Response): MangasPage { + val res = response.parseAs() + val mangas = res.data.popular.edges + val dateMap = preference.dateMap + val entries = mangas.map { manga -> + manga.uploadDate?.let { dateMap[manga.id] = it } + manga.toSManga() + } + preference.dateMap = dateMap + val hasNextPage = mangas.size == size + + return MangasPage(entries, hasNextPage) + } + + override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", FilterList()) + + override fun latestUpdatesParse(response: Response) = searchMangaParse(response) + + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { + return if (query.startsWith(SEARCH_PREFIX)) { + val mangaId = query.substringAfter(SEARCH_PREFIX) + client.newCall(mangaFromIDRequest(mangaId)) + .asObservableSuccess() + .map(::searchMangaFromIDParse) + } else { + super.fetchSearchManga(page, query, filters) + } + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val payload = GraphQL( + SearchVariables( + size = size, + page = page, + search = SearchPayload( + query = query.trim().takeUnless { it.isEmpty() }, + language = siteLang, + sortBy = filters.firstInstanceOrNull()?.selected, + format = filters.firstInstanceOrNull()?.selected, + tags = filters.firstInstanceOrNull()?.tags, + excludeTags = filters.firstInstanceOrNull()?.tags, + pagesRangeStart = filters.firstInstanceOrNull()?.value, + pagesRangeEnd = filters.firstInstanceOrNull()?.value, + ), + ), + SEARCH_QUERY, + ) + + val requestBody = payload.toJsonRequestBody() + + val apiHeaders = headersBuilder().buildApiHeaders(requestBody) + + return POST(apiUrl, apiHeaders, requestBody) + } + + override fun searchMangaParse(response: Response): MangasPage { + val res = response.parseAs() + val mangas = res.data.search.edges + val dateMap = preference.dateMap + val entries = mangas.map { manga -> + manga.uploadDate?.let { dateMap[manga.id] = it } + manga.toSManga() + } + preference.dateMap = dateMap + val hasNextPage = mangas.size == size + + return MangasPage(entries, hasNextPage) + } + + override fun getFilterList() = getFilters() + + private fun mangaFromIDRequest(id: String): Request { + val payload = GraphQL( + IdVariables(id), + DETAILS_QUERY, + ) + + val requestBody = payload.toJsonRequestBody() + + val apiHeaders = headersBuilder().buildApiHeaders(requestBody) + + return POST(apiUrl, apiHeaders, requestBody) + } + + private fun searchMangaFromIDParse(response: Response): MangasPage { + val res = response.parseAs() + + val manga = res.data.details + .takeIf { it.language == siteLang || lang == "all" } + ?.let { manga -> + preference.dateMap = preference.dateMap.also { dateMap -> + manga.uploadDate?.let { dateMap[manga.id] = it } + } + manga.toSManga() + } + + return MangasPage(listOfNotNull(manga), false) + } + + override fun mangaDetailsRequest(manga: SManga): Request { + return mangaFromIDRequest(manga.url) + } + + override fun mangaDetailsParse(response: Response): SManga { + val res = response.parseAs() + val manga = res.data.details + + preference.dateMap = preference.dateMap.also { dateMap -> + manga.uploadDate?.let { dateMap[manga.id] = it } + } + + return manga.toSManga() + } + + override fun getMangaUrl(manga: SManga) = "$baseUrl/hchapter/${manga.url}" + + override fun fetchChapterList(manga: SManga): Observable> { + val group = manga.description + ?.substringAfter("Group:", "") + ?.substringBefore("\n") + ?.trim() + ?.takeUnless { it.isEmpty() } + + return Observable.just( + listOf( + SChapter.create().apply { + name = "Chapter" + url = manga.url + date_upload = preference.dateMap[manga.url].parseDate() + scanlator = group + }, + ), + ) + } + + override fun getChapterUrl(chapter: SChapter) = "$baseUrl/hchapter/${chapter.url}" + + override fun pageListRequest(chapter: SChapter): Request { + val payload = GraphQL( + IdVariables(chapter.url), + PAGES_QUERY, + ) + + val requestBody = payload.toJsonRequestBody() + + val apiHeaders = headersBuilder().buildApiHeaders(requestBody) + + return POST(apiUrl, apiHeaders, requestBody) + } + + override fun pageListParse(response: Response): List { + val res = response.parseAs() + + val pages = res.data.chapter.pages?.firstOrNull() + ?: return emptyList() + + val cdn = pages.urlPart.toAbsUrl() + + val selectedImages = when (preference.getString(PREF_IMG_QUALITY_KEY, "original")) { + "medium" -> pages.qualityMedium?.mapIndexed { i, it -> + it ?: pages.qualityOriginal[i] + } + else -> pages.qualityOriginal + } ?: pages.qualityOriginal + + return selectedImages.mapIndexed { index, image -> + Page(index, "", "$cdn/${image.url}") + } + } + + private inline fun String.parseAs(): T = + json.decodeFromString(this) + + private inline fun Response.parseAs(): T = + use { body.string() }.parseAs() + + private inline fun List<*>.firstInstanceOrNull(): T? = + filterIsInstance().firstOrNull() + + private inline fun T.toJsonRequestBody(): RequestBody = + json.encodeToString(this) + .toRequestBody(JSON_MEDIA_TYPE) + + private fun Headers.Builder.buildApiHeaders(requestBody: RequestBody) = this + .add("Content-Length", requestBody.contentLength().toString()) + .add("Content-Type", requestBody.contentType().toString()) + .build() + + private fun String?.parseDate(): Long { + return runCatching { + dateFormat.parse(this!!.trim())!!.time + }.getOrDefault(0L) + } + + override fun setupPreferenceScreen(screen: PreferenceScreen) { + ListPreference(screen.context).apply { + key = PREF_IMG_QUALITY_KEY + title = "Default Image Quality" + entries = arrayOf("Original", "Medium") + entryValues = arrayOf("original", "medium") + setDefaultValue("original") + summary = "%s" + }.also(screen::addPreference) + } + + private var SharedPreferences.dateMap: MutableMap + get() { + val jsonMap = getString(PREF_DATE_MAP_KEY, "{}")!! + val dateMap = runCatching { jsonMap.parseAs>() } + return dateMap.getOrDefault(mutableMapOf()) + } + + @SuppressLint("ApplySharedPref") + set(dateMap) { + edit() + .putString(PREF_DATE_MAP_KEY, json.encodeToString(dateMap)) + .commit() + } + + override fun chapterListParse(response: Response) = throw UnsupportedOperationException("Not Used") + override fun imageUrlParse(response: Response) = throw UnsupportedOperationException("Not Used") + + companion object { + private const val size = 20 + const val SEARCH_PREFIX = "id:" + + private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull() + private val dateFormat by lazy { + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH) + } + + private const val PREF_DATE_MAP_KEY = "pref_date_map" + private const val PREF_IMG_QUALITY_KEY = "pref_image_quality" + } +} diff --git a/src/all/ninenineninehentai/src/eu/kanade/tachiyomi/extension/all/ninenineninehentai/NineNineNineHentaiDto.kt b/src/all/ninenineninehentai/src/eu/kanade/tachiyomi/extension/all/ninenineninehentai/NineNineNineHentaiDto.kt new file mode 100644 index 000000000..48a66577a --- /dev/null +++ b/src/all/ninenineninehentai/src/eu/kanade/tachiyomi/extension/all/ninenineninehentai/NineNineNineHentaiDto.kt @@ -0,0 +1,159 @@ +package eu.kanade.tachiyomi.extension.all.ninenineninehentai + +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.model.UpdateStrategy +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import java.util.Locale + +typealias ApiPopularResponse = Data + +typealias ApiSearchResponse = Data + +typealias ApiDetailsResponse = Data + +typealias ApiPageListResponse = Data + +@Serializable +data class Data(val data: T) + +@Serializable +data class Edges(val edges: List) + +@Serializable +data class PopularResponse( + @SerialName("queryPopularChapters") val popular: Edges, +) + +@Serializable +data class SearchResponse( + @SerialName("queryChapters") val search: Edges, +) + +@Serializable +data class DetailsResponse( + @SerialName("queryChapter") val details: ChapterResponse, +) + +@Serializable +data class ChapterResponse( + @SerialName("_id") val id: String, + val name: String, + val uploadDate: String? = null, + val format: String? = null, + val language: String? = null, + val pages: Int? = null, + @SerialName("firstPics") val cover: List? = emptyList(), + val tags: List? = emptyList(), +) { + fun toSManga() = SManga.create().apply { + url = id + title = name + thumbnail_url = cover?.firstOrNull()?.absUrl + author = this@ChapterResponse.author + artist = author + genre = genres + description = buildString { + if (formatParsed != null) append("Format: ${formatParsed}\n") + if (languageParsed != null) append("Language: $languageParsed\n") + if (group != null) append("Group: $group\n") + if (characters != null) append("Character(s): $characters\n") + if (parody != null) append("Parody: $parody\n") + if (pages != null) append("Pages: $pages\n") + } + status = SManga.COMPLETED + update_strategy = UpdateStrategy.ONLY_FETCH_ONCE + initialized = true + } + + private val formatParsed = when (format) { + "artistcg" -> "ArtistCG" + "gamecg" -> "GameCG" + else -> format?.capitalize() + } + + private val languageParsed = when (language) { + "en" -> "English" + "jp" -> "Japanese" + "cn" -> "Chinese" + "es" -> "Spanish" + else -> language + } + + private val author = tags?.firstOrNull { it.tagType == "artist" }?.tagName?.capitalize() + + private val group = tags?.filter { it.tagType == "group" } + ?.joinToString { it.tagName.capitalize() } + ?.takeUnless { it.isEmpty() } + + private val characters = tags?.filter { it.tagType == "character" } + ?.joinToString { it.tagName.capitalize() } + ?.takeUnless { it.isEmpty() } + + private val parody = tags?.filter { it.tagType == "parody" } + ?.joinToString { it.tagName.capitalize() } + ?.takeUnless { it.isEmpty() } + + private val genres = tags?.filterNot { it.tagType in filterTags } + ?.joinToString { it.tagName.capitalize() } + ?.takeUnless { it.isEmpty() } + + companion object { + private val filterTags = listOf("artist", "group", "character", "parody") + + private fun String.capitalize(): String { + return this.trim().split(" ").joinToString(" ") { word -> + word.replaceFirstChar { + if (it.isLowerCase()) { + it.titlecase( + Locale.getDefault(), + ) + } else { + it.toString() + } + } + } + } + } +} + +@Serializable +data class Url(val url: String) { + val absUrl get() = url.toAbsUrl() + + companion object { + fun String.toAbsUrl(): String { + return if (this.matches(urlRegex)) { + this + } else { + cdnUrl + this + } + } + + private const val cdnUrl = "https://edge.timmm111.online/" + private val urlRegex = Regex("^https?://.*") + } +} + +@Serializable +data class Tag( + val tagName: String, + val tagType: String? = "genre", +) + +@Serializable +data class PageList( + @SerialName("queryChapter") val chapter: PageUrl, +) + +@Serializable +data class PageUrl( + @SerialName("pictureUrls") val pages: List? = emptyList(), +) + +@Serializable +data class Pages( + @SerialName("picCdn") val urlPart: String, + @SerialName("pics") val qualityOriginal: List, + @SerialName("picsM") val qualityMedium: List? = emptyList(), +) diff --git a/src/all/ninenineninehentai/src/eu/kanade/tachiyomi/extension/all/ninenineninehentai/NineNineNineHentaiFactory.kt b/src/all/ninenineninehentai/src/eu/kanade/tachiyomi/extension/all/ninenineninehentai/NineNineNineHentaiFactory.kt new file mode 100644 index 000000000..eb3d6ba7b --- /dev/null +++ b/src/all/ninenineninehentai/src/eu/kanade/tachiyomi/extension/all/ninenineninehentai/NineNineNineHentaiFactory.kt @@ -0,0 +1,13 @@ +package eu.kanade.tachiyomi.extension.all.ninenineninehentai + +import eu.kanade.tachiyomi.source.SourceFactory + +class NineNineNineHentaiFactory : SourceFactory { + override fun createSources() = listOf( + NineNineNineHentai("all"), + NineNineNineHentai("en"), + NineNineNineHentai("ja", "jp"), + NineNineNineHentai("zh", "cn"), + NineNineNineHentai("es"), + ) +} diff --git a/src/all/ninenineninehentai/src/eu/kanade/tachiyomi/extension/all/ninenineninehentai/NineNineNineHentaiPayloadDto.kt b/src/all/ninenineninehentai/src/eu/kanade/tachiyomi/extension/all/ninenineninehentai/NineNineNineHentaiPayloadDto.kt new file mode 100644 index 000000000..e7ebc3b3f --- /dev/null +++ b/src/all/ninenineninehentai/src/eu/kanade/tachiyomi/extension/all/ninenineninehentai/NineNineNineHentaiPayloadDto.kt @@ -0,0 +1,39 @@ +package eu.kanade.tachiyomi.extension.all.ninenineninehentai + +import kotlinx.serialization.Serializable + +@Serializable +data class GraphQL( + val variables: T, + val query: String, +) + +@Serializable +data class PopularVariables( + val size: Int, + val page: Int, + val dateRange: Int, + val language: String, +) + +@Serializable +data class SearchVariables( + val size: Int, + val page: Int, + val search: SearchPayload, +) + +@Serializable +data class SearchPayload( + val query: String?, + val language: String, + val sortBy: String?, + val format: String?, + val tags: List?, + val excludeTags: List?, + val pagesRangeStart: Int?, + val pagesRangeEnd: Int?, +) + +@Serializable +data class IdVariables(val id: String) diff --git a/src/all/ninenineninehentai/src/eu/kanade/tachiyomi/extension/all/ninenineninehentai/NineNineNineHentaiQueries.kt b/src/all/ninenineninehentai/src/eu/kanade/tachiyomi/extension/all/ninenineninehentai/NineNineNineHentaiQueries.kt new file mode 100644 index 000000000..26759bb53 --- /dev/null +++ b/src/all/ninenineninehentai/src/eu/kanade/tachiyomi/extension/all/ninenineninehentai/NineNineNineHentaiQueries.kt @@ -0,0 +1,102 @@ +package eu.kanade.tachiyomi.extension.all.ninenineninehentai + +private fun buildQuery(queryAction: () -> String): String { + return queryAction() + .trimIndent() + .replace("%", "$") +} + +val POPULAR_QUERY: String = buildQuery { + """ + query( + %size: Int + %language: String + %dateRange: Int + %page: Int + ) { + queryPopularChapters( + size: %size + language: %language + dateRange: %dateRange + page: %page + ) { + edges { + _id + name + uploadDate + format + language + pages + firstPics + tags + } + } + } + """ +} + +val SEARCH_QUERY: String = buildQuery { + """ + query( + %search: SearchInput + %size: Int + %page: Int + ) { + queryChapters( + limit: %size + search: %search + page: %page + ) { + edges { + _id + name + uploadDate + format + language + pages + firstPics + tags + } + } + } + """ +} + +val DETAILS_QUERY: String = buildQuery { + """ + query( + %id: String + ) { + queryChapter( + chapterId: %id + ) { + _id + name + uploadDate + format + language + pages + firstPics + tags + } + } + """ +} + +val PAGES_QUERY: String = buildQuery { + """ + query( + %id: String + ) { + queryChapter( + chapterId: %id + ) { + pictureUrls { + picCdn + pics + picsM + } + } + } + """ +} diff --git a/src/all/ninenineninehentai/src/eu/kanade/tachiyomi/extension/all/ninenineninehentai/NineNineNineHentaiUrlActivity.kt b/src/all/ninenineninehentai/src/eu/kanade/tachiyomi/extension/all/ninenineninehentai/NineNineNineHentaiUrlActivity.kt new file mode 100644 index 000000000..d598b5fba --- /dev/null +++ b/src/all/ninenineninehentai/src/eu/kanade/tachiyomi/extension/all/ninenineninehentai/NineNineNineHentaiUrlActivity.kt @@ -0,0 +1,34 @@ +package eu.kanade.tachiyomi.extension.all.ninenineninehentai + +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 NineNineNineHentaiUrlActivity : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val pathSegments = intent?.data?.pathSegments + if (pathSegments != null && pathSegments.size > 1) { + val id = pathSegments[1] + val mainIntent = Intent().apply { + action = "eu.kanade.tachiyomi.SEARCH" + putExtra("query", "${NineNineNineHentai.SEARCH_PREFIX}$id") + putExtra("filter", packageName) + } + + try { + startActivity(mainIntent) + } catch (e: ActivityNotFoundException) { + Log.e("999HentaiUrlActivity", e.toString()) + } + } else { + Log.e("999HentaiUrlActivity", "could not parse uri from intent $intent") + } + + finish() + exitProcess(0) + } +}