diff --git a/src/all/photos18/AndroidManifest.xml b/src/all/photos18/AndroidManifest.xml new file mode 100644 index 000000000..30deb7f79 --- /dev/null +++ b/src/all/photos18/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/src/all/photos18/build.gradle b/src/all/photos18/build.gradle new file mode 100644 index 000000000..d66e10bf9 --- /dev/null +++ b/src/all/photos18/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + extName = 'Photos18' + pkgNameSuffix = 'all.photos18' + extClass = '.Photos18' + extVersionCode = 1 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/all/photos18/res/mipmap-hdpi/ic_launcher.png b/src/all/photos18/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..5ddf3f3bf Binary files /dev/null and b/src/all/photos18/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/all/photos18/res/mipmap-mdpi/ic_launcher.png b/src/all/photos18/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..07a7f7649 Binary files /dev/null and b/src/all/photos18/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/all/photos18/res/mipmap-xhdpi/ic_launcher.png b/src/all/photos18/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..7b5c25ab2 Binary files /dev/null and b/src/all/photos18/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/all/photos18/res/mipmap-xxhdpi/ic_launcher.png b/src/all/photos18/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..7fd596086 Binary files /dev/null and b/src/all/photos18/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/all/photos18/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/photos18/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..4c3b2696a Binary files /dev/null and b/src/all/photos18/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/all/photos18/res/web_hi_res_512.png b/src/all/photos18/res/web_hi_res_512.png new file mode 100644 index 000000000..7811d64ff Binary files /dev/null and b/src/all/photos18/res/web_hi_res_512.png differ diff --git a/src/all/photos18/src/eu/kanade/tachiyomi/extension/all/photos18/Photos18.kt b/src/all/photos18/src/eu/kanade/tachiyomi/extension/all/photos18/Photos18.kt new file mode 100644 index 000000000..5319390bf --- /dev/null +++ b/src/all/photos18/src/eu/kanade/tachiyomi/extension/all/photos18/Photos18.kt @@ -0,0 +1,166 @@ +package eu.kanade.tachiyomi.extension.all.photos18 + +import android.app.Application +import androidx.preference.PreferenceScreen +import androidx.preference.SwitchPreferenceCompat +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 org.jsoup.nodes.Document +import org.jsoup.select.Evaluator +import rx.Observable +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class Photos18 : HttpSource(), ConfigurableSource { + override val name = "Photos18" + override val lang = "all" + override val supportsLatest = true + + override val baseUrl = "https://www.photos18.com" + + private val baseUrlWithLang get() = if (useTrad) baseUrl else "$baseUrl/zh-hans" + private fun String.stripLang() = removePrefix("/zh-hans") + + override val client = network.client.newBuilder().followRedirects(false).build() + + override fun popularMangaRequest(page: Int) = GET("$baseUrlWithLang/sort/views?page=$page", headers) + + override fun popularMangaParse(response: Response): MangasPage { + val document = response.asJsoup() + parseCategories(document) + val mangas = document.selectFirst(Evaluator.Id("videos")).children().map { + val cardBody = it.selectFirst(Evaluator.Class("card-body")) + val link = cardBody.selectFirst(Evaluator.Tag("a")) + SManga.create().apply { + url = link.attr("href").stripLang() + title = link.ownText() + thumbnail_url = baseUrl + it.selectFirst(Evaluator.Tag("img")).attr("data-src") + genre = cardBody.selectFirst(Evaluator.Tag("label")).ownText() + status = SManga.COMPLETED + initialized = true + } + } + val isLastPage = document.selectFirst(Evaluator.Class("next")).run { + this == null || hasClass("disabled") + } + return MangasPage(mangas, !isLastPage) + } + + override fun latestUpdatesRequest(page: Int) = GET("$baseUrlWithLang/?page=$page", headers) + + override fun latestUpdatesParse(response: Response) = popularMangaParse(response) + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = baseUrlWithLang.toHttpUrl().newBuilder() + .addQueryParameter("q", query) + .addQueryParameter("page", page.toString()) + + for (filter in filters) { + if (filter is QueryFilter) filter.addQueryTo(url) + } + + return GET(url.toString(), headers) + } + + override fun searchMangaParse(response: Response) = popularMangaParse(response) + + override fun fetchMangaDetails(manga: SManga): Observable = Observable.just(manga) + + override fun mangaDetailsParse(response: Response) = throw UnsupportedOperationException("Not used.") + + override fun fetchChapterList(manga: SManga): Observable> { + val chapter = SChapter.create().apply { + url = manga.url + name = manga.title + chapter_number = -2f + } + return Observable.just(listOf(chapter)) + } + + override fun chapterListParse(response: Response) = throw UnsupportedOperationException("Not used.") + + override fun pageListParse(response: Response): List { + val document = response.asJsoup() + val images = document.selectFirst(Evaluator.Id("content")).select(Evaluator.Tag("img")) + return images.mapIndexed { index, image -> + Page(index, imageUrl = image.attr("data-src")) + } + } + + override fun imageUrlParse(response: Response) = throw UnsupportedOperationException("Not used.") + + override fun getFilterList() = FilterList( + SortFilter(), + if (categories.isEmpty()) + Filter.Header("Tap 'Reset' to load categories") + else + CategoryFilter(categories) + ) + + private open class QueryFilter( + name: String, + values: Array, + private val queryName: String, + private val queryValues: Array, + state: Int = 0 + ) : Filter.Select(name, values, state) { + fun addQueryTo(builder: HttpUrl.Builder) = + builder.addQueryParameter(queryName, queryValues[state]) + } + + private class SortFilter : QueryFilter( + "Sort by", + arrayOf("Latest", "Popular", "Trend", "Recommended", "Best"), + "sort", + arrayOf("created", "hits", "views", "score", "likes"), + state = 2 + ) + + private class CategoryFilter(categories: List>) : QueryFilter( + "Category", + categories.map { it.first }.toTypedArray(), + "category_id", + categories.map { it.second }.toTypedArray() + ) + + private var categories: List> = emptyList() + + private fun parseCategories(document: Document) { + if (categories.isNotEmpty()) return + val items = document.selectFirst(Evaluator.Id("w3")).children() + categories = buildList(items.size + 1) { + add(Pair("All", "")) + items.mapTo(this) { + val value = it.text().substringBefore(" (") + val queryValue = it.selectFirst(Evaluator.Tag("a")).attr("href").substringAfterLast('/') + Pair(value, queryValue) + } + } + } + + private val preferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000)!! + } + + private val useTrad get() = preferences.getBoolean("ZH_HANT", false) + + override fun setupPreferenceScreen(screen: PreferenceScreen) { + SwitchPreferenceCompat(screen.context).apply { + key = "ZH_HANT" + title = "Use Traditional Chinese" + setDefaultValue(false) + }.let(screen::addPreference) + } +}