diff --git a/src/all/lanraragi/build.gradle b/src/all/lanraragi/build.gradle index 97da0d560..dc5a8bdca 100644 --- a/src/all/lanraragi/build.gradle +++ b/src/all/lanraragi/build.gradle @@ -5,8 +5,13 @@ ext { extName = 'LANraragi' pkgNameSuffix = 'all.lanraragi' extClass = '.LANraragi' - extVersionCode = 2 + extVersionCode = 3 libVersion = '1.2' } +dependencies { + implementation 'io.reactivex:rxandroid:1.2.1' + implementation 'io.reactivex:rxjava:1.3.6' +} + apply from: "$rootDir/common.gradle" diff --git a/src/all/lanraragi/src/eu/kanade/tachiyomi/extension/all/lanraragi/LANraragi.kt b/src/all/lanraragi/src/eu/kanade/tachiyomi/extension/all/lanraragi/LANraragi.kt index 8a9b9709e..6b05409af 100644 --- a/src/all/lanraragi/src/eu/kanade/tachiyomi/extension/all/lanraragi/LANraragi.kt +++ b/src/all/lanraragi/src/eu/kanade/tachiyomi/extension/all/lanraragi/LANraragi.kt @@ -3,25 +3,34 @@ package eu.kanade.tachiyomi.extension.all.lanraragi import android.app.Application import android.content.SharedPreferences import android.net.Uri +import android.support.v7.preference.CheckBoxPreference import android.support.v7.preference.EditTextPreference import android.support.v7.preference.PreferenceScreen import android.util.Base64 import com.github.salomonbrys.kotson.fromJson import com.google.gson.Gson +import eu.kanade.tachiyomi.extension.all.lanraragi.model.Archive import eu.kanade.tachiyomi.extension.all.lanraragi.model.ArchivePage import eu.kanade.tachiyomi.extension.all.lanraragi.model.ArchiveSearchResult +import eu.kanade.tachiyomi.extension.all.lanraragi.model.Category import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.POST 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 okhttp3.CacheControl import okhttp3.Headers +import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response +import rx.Single +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -39,6 +48,9 @@ open class LANraragi : ConfigurableSource, HttpSource() { private val apiKey: String get() = preferences.getString("apiKey", "")!! + private val latestNamespacePref: String + get() = preferences.getString("latestNamespacePref", DEFAULT_SORT_BY_NS)!! + private val gson: Gson = Gson() override fun mangaDetailsParse(response: Response): SManga { @@ -49,10 +61,19 @@ open class LANraragi : ConfigurableSource, HttpSource() { } } - override fun chapterListParse(response: Response): List { - val id = getId(response) + override fun chapterListRequest(manga: SManga): Request { + // Upgrade the LRR reader URL to the API metadata endpoint + // without breaking WebView (i.e. for management). - val uri = getApiUriBuilder("/api/archives/$id/extract") + val id = manga.url.split('=').last() + val uri = getApiUriBuilder("/api/archives/$id/metadata").build() + + return GET(uri.toString(), headers) + } + + override fun chapterListParse(response: Response): List { + val archive = gson.fromJson(response.body()!!.string()) + val uri = getApiUriBuilder("/api/archives/${archive.arcid}/extract") return listOf( SChapter.create().apply { @@ -61,6 +82,10 @@ open class LANraragi : ConfigurableSource, HttpSource() { url = uriBuild.toString() chapter_number = 1F name = "Chapter" + + getDateAdded(archive.tags).toLongOrNull()?.let { + date_upload = it + } } ) } @@ -89,7 +114,17 @@ open class LANraragi : ConfigurableSource, HttpSource() { } override fun latestUpdatesRequest(page: Int): Request { - return searchMangaRequest(page, "", FilterList()) + val filters = mutableListOf>() + val prefNewOnly = preferences.getBoolean("latestNewOnly", false) + + if (prefNewOnly) filters.add(NewArchivesOnly(true)) + + if (latestNamespacePref.isNotBlank()) { + filters.add(SortByNamespace(latestNamespacePref)) + filters.add(DescendingOrder(true)) + } + + return searchMangaRequest(page, "", FilterList(filters)) } override fun latestUpdatesParse(response: Response): MangasPage { @@ -97,16 +132,39 @@ open class LANraragi : ConfigurableSource, HttpSource() { } private var lastResultCount: Int = 100 + private var lastRecordsFiltered: Int = 0 + private var maxResultCount: Int = 0 + private var totalRecords: Int = 0 override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { val uri = getApiUriBuilder("/api/search") - uri.appendQueryParameter("start", ((page - 1) * lastResultCount).toString()) + var startPageOffset = 0 + + filters.forEach { filter -> + when (filter) { + is StartingPage -> { + startPageOffset = filter.state.toIntOrNull() ?: 1 + + // Exception for API wrapping around and user input of 0 + if (startPageOffset > 0) { + startPageOffset -= 1 + } + } + is NewArchivesOnly -> if (filter.state) uri.appendQueryParameter("newonly", "true") + is UntaggedArchivesOnly -> if (filter.state) uri.appendQueryParameter("untaggedonly", "true") + is DescendingOrder -> if (filter.state) uri.appendQueryParameter("order", "desc") + is SortByNamespace -> if (filter.state.isNotEmpty()) uri.appendQueryParameter("sortby", filter.state.trim()) + is CategorySelect -> if (filter.state > 0) uri.appendQueryParameter("category", filter.toUriPart()) + } + } + + uri.appendQueryParameter("start", ((page - 1 + startPageOffset) * maxResultCount).toString()) if (query.isNotEmpty()) { uri.appendQueryParameter("filter", query) } - return GET(uri.toString(), headers) + return GET(uri.toString(), headers, CacheControl.FORCE_NETWORK) } override fun searchMangaParse(response: Response): MangasPage { @@ -114,6 +172,9 @@ open class LANraragi : ConfigurableSource, HttpSource() { val currentStart = getStart(response) lastResultCount = jsonResult.data.size + maxResultCount = if (lastResultCount >= maxResultCount) lastResultCount else maxResultCount + lastRecordsFiltered = jsonResult.recordsFiltered + totalRecords = jsonResult.recordsTotal return MangasPage( jsonResult.data.map { @@ -137,6 +198,25 @@ open class LANraragi : ConfigurableSource, HttpSource() { } } + private class DescendingOrder(overrideState: Boolean = false) : Filter.CheckBox("Descending Order", overrideState) + private class NewArchivesOnly(overrideState: Boolean = false) : Filter.CheckBox("New Archives Only", overrideState) + private class UntaggedArchivesOnly : Filter.CheckBox("Untagged Archives Only", false) + private class StartingPage(stats: String) : Filter.Text("Starting Page$stats", "") + private class SortByNamespace(defaultText: String = "") : Filter.Text("Sort by (namespace)", defaultText) + private class CategorySelect(categories: Array>) : UriPartFilter("Category", categories) + + override fun getFilterList() = FilterList( + CategorySelect(getCategoryPairs(categories)), + Filter.Separator(), + DescendingOrder(), + NewArchivesOnly(), + UntaggedArchivesOnly(), + StartingPage(startingPageStats()), + SortByNamespace() + ) + + private var categories = emptyList() + // Preferences private val preferences: SharedPreferences by lazy { Injekt.get().getSharedPreferences("source_$id", 0x0000) @@ -169,7 +249,7 @@ open class LANraragi : ConfigurableSource, HttpSource() { key = "API Key" title = "API Key" text = apiKey - summary = apiKey + summary = "Required if No-Fun Mode is enabled." dialogTitle = "API Key" setOnPreferenceChangeListener { _, newValue -> @@ -177,15 +257,47 @@ open class LANraragi : ConfigurableSource, HttpSource() { this.apply { text = apiKey - summary = apiKey + summary = "Required if No-Fun Mode is enabled." } preferences.edit().putString("apiKey", newValue).commit() } } + val latestNewOnlyPref = CheckBoxPreference(screen.context).apply { + key = "latestNewOnly" + title = "Latest - New Only" + setDefaultValue(true) + + setOnPreferenceChangeListener { _, newValue -> + val checkValue = newValue as Boolean + preferences.edit().putBoolean("latestNewOnly", checkValue).commit() + } + } + + val latestNamespacePref = EditTextPreference(screen.context).apply { + key = "latestNamespacePref" + title = "Latest - Sort by Namespace" + text = latestNamespacePref + summary = "Sort by the given namespace for Latest, such as date_added." + dialogTitle = "Latest - Sort by Namespace" + setDefaultValue(DEFAULT_SORT_BY_NS) + + setOnPreferenceChangeListener { _, newValue -> + val latestNamespacePref = newValue as String + + this.apply { + text = latestNamespacePref + } + + preferences.edit().putString("latestNamespacePref", newValue).commit() + } + } + screen.addPreference(hostnamePref) screen.addPreference(apiKeyPref) + screen.addPreference(latestNewOnlyPref) + screen.addPreference(latestNamespacePref) } override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) { @@ -215,7 +327,7 @@ open class LANraragi : ConfigurableSource, HttpSource() { key = "API Key" title = "API Key" text = apiKey - summary = apiKey + summary = "Required if No-Fun Mode is enabled." dialogTitle = "API Key" setOnPreferenceChangeListener { _, newValue -> @@ -223,18 +335,77 @@ open class LANraragi : ConfigurableSource, HttpSource() { this.apply { text = apiKey - summary = apiKey + summary = "Required if No-Fun Mode is enabled." } preferences.edit().putString("apiKey", newValue).commit() } } + val latestNewOnlyPref = androidx.preference.CheckBoxPreference(screen.context).apply { + key = "latestNewOnly" + title = "Latest - New Only" + setDefaultValue(true) + + setOnPreferenceChangeListener { _, newValue -> + val checkValue = newValue as Boolean + preferences.edit().putBoolean("latestNewOnly", checkValue).commit() + } + } + + val latestNamespacePref = androidx.preference.EditTextPreference(screen.context).apply { + key = "latestNamespacePref" + title = "Latest - Sort by Namespace" + text = latestNamespacePref + summary = "Sort by the given namespace for Latest, such as date_added." + dialogTitle = "Latest - Sort by Namespace" + setDefaultValue(DEFAULT_SORT_BY_NS) + + setOnPreferenceChangeListener { _, newValue -> + val latestNamespacePref = newValue as String + + this.apply { + text = latestNamespacePref + } + + preferences.edit().putString("latestNamespacePref", newValue).commit() + } + } + screen.addPreference(hostnamePref) screen.addPreference(apiKeyPref) + screen.addPreference(latestNewOnlyPref) + screen.addPreference(latestNamespacePref) } // Helper + protected open class UriPartFilter(displayName: String, val vals: Array>) : + Filter.Select(displayName, vals.map { it.second }.toTypedArray()) { + fun toUriPart() = vals[state].first + } + + private fun getCategoryPairs(categories: List): Array> { + // Empty pair to disable. Sort by pinned status then name for convenience. + // Web client sort is pinned > last_used but reflects between page changes. + + val pin = "\uD83D\uDCCC " + + return listOf(Pair("", "")) + .plus( + categories + .sortedWith(compareByDescending { it.pinned }.thenBy { it.name }) + .map { + val pinned = if (it.pinned == "1") pin else "" + Pair(it.id, "$pinned${it.name}") + } + ) + .toTypedArray() + } + + private fun startingPageStats(): String { + return if (maxResultCount > 0 && totalRecords > 0) " ($maxResultCount / $lastRecordsFiltered items)" else "" + } + private fun getApiUriBuilder(path: String): Uri.Builder { val uri = Uri.parse("$baseUrl$path").buildUpon() @@ -270,4 +441,42 @@ open class LANraragi : ConfigurableSource, HttpSource() { return "N/A" } + + private fun getDateAdded(tags: String): String { + tags.split(',').forEach { + if (it.contains(':')) { + val temp = it.trim().split(':') + + // Pad Date Added LRR plugin (or user specified namespace) to milliseconds + if (temp[0].equals(latestNamespacePref, true)) return temp[1].padEnd(13, '0') + } + } + + return "" + } + + // Headers (currently auth) are done in headersBuilder + override val client: OkHttpClient = network.client.newBuilder().build() + + init { + Single.fromCallable { + client.newCall(GET("$baseUrl/api/categories", headers)).execute() + } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { response -> + categories = try { + gson.fromJson(response.body()?.charStream()!!) + } catch (e: Exception) { + emptyList() + } + }, + {} + ) + } + + companion object { + private const val DEFAULT_SORT_BY_NS = "date_added" + } } diff --git a/src/all/lanraragi/src/eu/kanade/tachiyomi/extension/all/lanraragi/model/Category.kt b/src/all/lanraragi/src/eu/kanade/tachiyomi/extension/all/lanraragi/model/Category.kt new file mode 100644 index 000000000..8cafd247c --- /dev/null +++ b/src/all/lanraragi/src/eu/kanade/tachiyomi/extension/all/lanraragi/model/Category.kt @@ -0,0 +1,8 @@ +package eu.kanade.tachiyomi.extension.all.lanraragi.model + +data class Category( + val id: String, + val last_used: String, + val name: String, + val pinned: String +)