LANraragi: Filters, Latest, misc API. (#5462)
* LANraragi: Handful of filters. - Descending/new/untagged toggles - Starting page override - Namespace sorting - Categories * LANraragi: Upgrade chapterListRequest to API. Use the metadata endpoint. Preserve WebView for i.e. management. Pull out the date_added tag for date_upload. * LANraragi: Verify if a category is selected. * LANraragi: Hook up Latest. With some new preferences. * LANraragi: Switch Category filter from CBs to Dropdown Soon: LRR has multi-category drifting. Pins are indicated in the dropdown. Correct types in Category. I'm blind. * LANraragi: Change date_upload null check to let. * LANraragi: Empty string compare to isNotBlank * LANraragi: Preferences tidying. * LANraragi: Cleanup Starting Page filter. RIP the dream of indicating the number of items returned by search without pressing the Reset button. * LANraragi: More preferences tidying. * LANraragi: Pagination offset improvements Previously going back pages from the end would result in the wrong size. Include stats in the filter option when available to help with manual pagination.
This commit is contained in:
parent
f82dffc51f
commit
3fc1e2fe65
|
@ -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"
|
||||
|
|
|
@ -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<SChapter> {
|
||||
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<SChapter> {
|
||||
val archive = gson.fromJson<Archive>(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<Filter<*>>()
|
||||
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<Pair<String?, String>>) : UriPartFilter("Category", categories)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
CategorySelect(getCategoryPairs(categories)),
|
||||
Filter.Separator(),
|
||||
DescendingOrder(),
|
||||
NewArchivesOnly(),
|
||||
UntaggedArchivesOnly(),
|
||||
StartingPage(startingPageStats()),
|
||||
SortByNamespace()
|
||||
)
|
||||
|
||||
private var categories = emptyList<Category>()
|
||||
|
||||
// Preferences
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().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<Pair<String?, String>>) :
|
||||
Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].first
|
||||
}
|
||||
|
||||
private fun getCategoryPairs(categories: List<Category>): Array<Pair<String?, String>> {
|
||||
// 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<Category> { 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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
Loading…
Reference in New Issue