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:
RePod 2021-01-17 10:23:45 -05:00 committed by GitHub
parent f82dffc51f
commit 3fc1e2fe65
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 233 additions and 11 deletions

View File

@ -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"

View File

@ -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"
}
}

View File

@ -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
)