diff --git a/multisrc/overrides/bakkin/bakkinselfhosted/src/BakkinSelfHosted.kt b/multisrc/overrides/bakkin/bakkinselfhosted/src/BakkinSelfHosted.kt new file mode 100644 index 000000000..df2bd3dc4 --- /dev/null +++ b/multisrc/overrides/bakkin/bakkinselfhosted/src/BakkinSelfHosted.kt @@ -0,0 +1,29 @@ +package eu.kanade.tachiyomi.extension.en.bakkinselfhosted + +import androidx.preference.EditTextPreference +import androidx.preference.PreferenceScreen +import eu.kanade.tachiyomi.multisrc.bakkin.BakkinReaderX + +class BakkinSelfHosted : BakkinReaderX("Bakkin Self-hosted", "", "en") { + override val baseUrl by lazy { + preferences.getString("baseUrl", "http://127.0.0.1/")!! + } + + override fun setupPreferenceScreen(screen: PreferenceScreen) { + super.setupPreferenceScreen(screen) + screen.addPreference( + EditTextPreference(screen.context).apply { + key = "baseUrl" + title = "Custom URL" + summary = "Connect to a self-hosted Bakkin Reader X server" + setDefaultValue("http://127.0.0.1/") + + setOnPreferenceChangeListener { _, newValue -> + // Make sure the URL ends with one slash + val url = (newValue as String).trimEnd('/') + '/' + preferences.edit().putString("baseUrl", url).commit() + } + } + ) + } +} diff --git a/multisrc/overrides/bakkin/default/res/mipmap-hdpi/ic_launcher.png b/multisrc/overrides/bakkin/default/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..b42c408da Binary files /dev/null and b/multisrc/overrides/bakkin/default/res/mipmap-hdpi/ic_launcher.png differ diff --git a/multisrc/overrides/bakkin/default/res/mipmap-mdpi/ic_launcher.png b/multisrc/overrides/bakkin/default/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..2b23e8137 Binary files /dev/null and b/multisrc/overrides/bakkin/default/res/mipmap-mdpi/ic_launcher.png differ diff --git a/multisrc/overrides/bakkin/default/res/mipmap-xhdpi/ic_launcher.png b/multisrc/overrides/bakkin/default/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..fd7c6a79f Binary files /dev/null and b/multisrc/overrides/bakkin/default/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/bakkin/default/res/mipmap-xxhdpi/ic_launcher.png b/multisrc/overrides/bakkin/default/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..f43ff5819 Binary files /dev/null and b/multisrc/overrides/bakkin/default/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/bakkin/default/res/mipmap-xxxhdpi/ic_launcher.png b/multisrc/overrides/bakkin/default/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..436cfe887 Binary files /dev/null and b/multisrc/overrides/bakkin/default/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/bakkin/default/res/web_hi_res_512.png b/multisrc/overrides/bakkin/default/res/web_hi_res_512.png new file mode 100644 index 000000000..2a5a78c7b Binary files /dev/null and b/multisrc/overrides/bakkin/default/res/web_hi_res_512.png differ diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/bakkin/BakkinGenerator.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/bakkin/BakkinGenerator.kt new file mode 100644 index 000000000..a1bd56dcc --- /dev/null +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/bakkin/BakkinGenerator.kt @@ -0,0 +1,21 @@ +package eu.kanade.tachiyomi.multisrc.bakkin + +import generator.ThemeSourceData.SingleLang +import generator.ThemeSourceGenerator + +class BakkinGenerator : ThemeSourceGenerator { + override val themePkg = "bakkin" + + override val themeClass = "BakkinReaderX" + + override val baseVersionCode: Int = 1 + + override val sources = listOf( + SingleLang("Bakkin", "https://bakkin.moe/reader/", "en"), + SingleLang("Bakkin Self-hosted", "", "en", className = "BakkinSelfHosted") + ) + + companion object { + @JvmStatic fun main(args: Array) = BakkinGenerator().createAll() + } +} diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/bakkin/BakkinJSON.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/bakkin/BakkinJSON.kt new file mode 100644 index 000000000..38e3e5c2f --- /dev/null +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/bakkin/BakkinJSON.kt @@ -0,0 +1,38 @@ +package eu.kanade.tachiyomi.multisrc.bakkin + +@kotlinx.serialization.Serializable +internal data class Series( + val dir: String, + val name: String, + val author: String?, + val status: String?, + val thumb: String?, + val volumes: List +) : Iterable { + override fun iterator() = volumes.flatMap { + // Prepend the volume name to the chapter name + it.map { ch -> ch.copy(name = "$it - $ch") } + }.iterator() + + val cover get() = thumb ?: "static/nocover.png" + + override fun toString() = name.ifEmpty { dir } +} + +@kotlinx.serialization.Serializable +internal data class Volume( + val dir: String, + val name: String, + val chapters: List +) : Iterable by chapters { + override fun toString() = name.ifEmpty { dir } +} + +@kotlinx.serialization.Serializable +internal data class Chapter( + val dir: String, + val name: String, + val pages: List +) : Iterable by pages { + override fun toString() = name.ifEmpty { dir } +} diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/bakkin/BakkinReaderX.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/bakkin/BakkinReaderX.kt new file mode 100644 index 000000000..b04b9f3b1 --- /dev/null +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/bakkin/BakkinReaderX.kt @@ -0,0 +1,153 @@ +package eu.kanade.tachiyomi.multisrc.bakkin + +import android.app.Application +import android.os.Build +import androidx.preference.CheckBoxPreference +import androidx.preference.PreferenceScreen +import eu.kanade.tachiyomi.BuildConfig +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.asObservableSuccess +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.json.Json +import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.jsonObject +import okhttp3.Headers +import okhttp3.Request +import okhttp3.Response +import rx.Observable +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +abstract class BakkinReaderX( + override val name: String, + override val baseUrl: String, + override val lang: String +) : ConfigurableSource, HttpSource() { + override val supportsLatest = false + + private val userAgent = "Mozilla/5.0 (" + + "Android ${Build.VERSION.RELEASE}; Mobile) " + + "Tachiyomi/${BuildConfig.VERSION_NAME}" + + protected val preferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000)!! + } + + private val json by lazy { Injekt.get() } + + private val mainUrl + get() = baseUrl + "/main.php" + + if (preferences.getBoolean("fullsize", false)) "?fullsize" else "" + + private var seriesCache = emptyList() + + private fun observableSeries(block: (List) -> R) = + if (seriesCache.isNotEmpty()) Observable.just(block(seriesCache)) + else client.newCall(GET(mainUrl, headers)).asObservableSuccess().map { + seriesCache = json.parseToJsonElement(it.body!!.string()) + .jsonObject.values.map(json::decodeFromJsonElement) + block(seriesCache) + } + + override fun headersBuilder() = Headers.Builder().add("User-Agent", userAgent) + + // Request the actual manga URL for the webview + override fun mangaDetailsRequest(manga: SManga) = GET("$baseUrl#m=${manga.url}", headers) + + override fun fetchPopularManga(page: Int): Observable = + observableSeries { series -> + series.map { + SManga.create().apply { + url = it.dir + title = it.toString() + thumbnail_url = baseUrl + it.cover + } + }.let { MangasPage(it, false) } + } + + override fun fetchMangaDetails(manga: SManga): Observable = + observableSeries { series -> + series.first { it.dir == manga.url }.let { + SManga.create().apply { + url = it.dir + title = it.toString() + thumbnail_url = baseUrl + it.cover + initialized = true + author = it.author + status = when (it.status) { + "Ongoing" -> SManga.ONGOING + "Completed" -> SManga.COMPLETED + else -> SManga.UNKNOWN + } + } + } + } + + override fun fetchChapterList(manga: SManga): Observable> = + observableSeries { series -> + series.first { it.dir == manga.url }.mapIndexed { idx, chapter -> + SChapter.create().apply { + url = chapter.dir + name = chapter.toString() + chapter_number = idx.toFloat() + date_upload = -1L + } + } + } + + override fun fetchPageList(chapter: SChapter): Observable> = + observableSeries { series -> + series.flatten().first { it.dir == chapter.url } + .mapIndexed { idx, page -> Page(idx, "", "$baseUrl$page") } + } + + override fun setupPreferenceScreen(screen: PreferenceScreen) { + screen.addPreference( + CheckBoxPreference(screen.context).apply { + key = "fullsize" + summary = "View fullsize images" + setDefaultValue(false) + + setOnPreferenceChangeListener { _, newValue -> + preferences.edit().putBoolean(key, newValue as Boolean).commit() + } + } + ) + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = + throw UnsupportedOperationException("Search is not supported by this source.") + + override fun popularMangaRequest(page: Int): Request = + throw UnsupportedOperationException("Not used!") + + override fun latestUpdatesRequest(page: Int): Request = + throw UnsupportedOperationException("Not used!") + + override fun searchMangaParse(response: Response): MangasPage = + throw UnsupportedOperationException("Not used!") + + override fun popularMangaParse(response: Response): MangasPage = + throw UnsupportedOperationException("Not used!") + + override fun latestUpdatesParse(response: Response): MangasPage = + throw UnsupportedOperationException("Not used!") + + override fun mangaDetailsParse(response: Response): SManga = + throw UnsupportedOperationException("Not used!") + + override fun chapterListParse(response: Response): List = + throw UnsupportedOperationException("Not used!") + + override fun pageListParse(response: Response): List = + throw UnsupportedOperationException("Not used!") + + override fun imageUrlParse(response: Response): String = + throw UnsupportedOperationException("Not used!") +}