parent
cdc6d67473
commit
9237c48aca
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Komga'
|
||||
extClass = '.KomgaFactory'
|
||||
extVersionCode = 52
|
||||
extVersionCode = 53
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -2,16 +2,17 @@ package eu.kanade.tachiyomi.extension.all.komga
|
|||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.text.Editable
|
||||
import android.text.InputType
|
||||
import android.text.TextWatcher
|
||||
import android.util.Log
|
||||
import android.widget.Button
|
||||
import android.widget.Toast
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.AppInfo
|
||||
import eu.kanade.tachiyomi.extension.all.komga.KomgaUtils.addEditTextPreference
|
||||
import eu.kanade.tachiyomi.extension.all.komga.KomgaUtils.isFromReadList
|
||||
import eu.kanade.tachiyomi.extension.all.komga.KomgaUtils.parseAs
|
||||
import eu.kanade.tachiyomi.extension.all.komga.KomgaUtils.toSManga
|
||||
import eu.kanade.tachiyomi.extension.all.komga.dto.AuthorDto
|
||||
import eu.kanade.tachiyomi.extension.all.komga.dto.BookDto
|
||||
import eu.kanade.tachiyomi.extension.all.komga.dto.CollectionDto
|
||||
|
@ -30,8 +31,6 @@ 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.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Credentials
|
||||
import okhttp3.Dns
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
|
@ -39,14 +38,15 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
|||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.apache.commons.text.StringSubstitutor
|
||||
import rx.Single
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.security.MessageDigest
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock
|
||||
import kotlin.concurrent.read
|
||||
import kotlin.concurrent.write
|
||||
|
||||
open class Komga(private val suffix: String = "") : ConfigurableSource, UnmeteredSource, HttpSource() {
|
||||
|
||||
|
@ -65,8 +65,6 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
|
|||
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
|
||||
}
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
internal val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
@ -100,7 +98,7 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
|
|||
)
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage =
|
||||
processSeriesPage(response)
|
||||
KomgaUtils.processSeriesPage(response, baseUrl)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request =
|
||||
searchMangaRequest(
|
||||
|
@ -110,13 +108,13 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
|
|||
)
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage =
|
||||
processSeriesPage(response)
|
||||
KomgaUtils.processSeriesPage(response, baseUrl)
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
runCatching { fetchFilterOptions() }
|
||||
|
||||
val collectionId = (filters.find { it is CollectionSelect } as? CollectionSelect)?.let {
|
||||
it.values[it.state].id
|
||||
it.collections[it.state].id
|
||||
}
|
||||
|
||||
val type = when {
|
||||
|
@ -129,75 +127,14 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
|
|||
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is UnreadFilter -> {
|
||||
if (filter.state) {
|
||||
url.addQueryParameter("read_status", "UNREAD")
|
||||
url.addQueryParameter("read_status", "IN_PROGRESS")
|
||||
}
|
||||
}
|
||||
is InProgressFilter -> {
|
||||
if (filter.state) {
|
||||
url.addQueryParameter("read_status", "IN_PROGRESS")
|
||||
}
|
||||
}
|
||||
is ReadFilter -> {
|
||||
if (filter.state) {
|
||||
url.addQueryParameter("read_status", "READ")
|
||||
}
|
||||
}
|
||||
is LibraryGroup -> {
|
||||
val libraryToInclude = filter.state.filter { it.state }.map { it.id }
|
||||
|
||||
if (libraryToInclude.isNotEmpty()) {
|
||||
url.addQueryParameter("library_id", libraryToInclude.joinToString(","))
|
||||
}
|
||||
}
|
||||
is StatusGroup -> {
|
||||
val statusToInclude = filter.state.filter { it.state }.map { it.name.uppercase(Locale.ROOT) }
|
||||
|
||||
if (statusToInclude.isNotEmpty()) {
|
||||
url.addQueryParameter("status", statusToInclude.joinToString(","))
|
||||
}
|
||||
}
|
||||
is GenreGroup -> {
|
||||
val genreToInclude = filter.state.filter { it.state }.map { it.name }
|
||||
|
||||
if (genreToInclude.isNotEmpty()) {
|
||||
url.addQueryParameter("genre", genreToInclude.joinToString(","))
|
||||
}
|
||||
}
|
||||
is TagGroup -> {
|
||||
val tagToInclude = filter.state.filter { it.state }.map { it.name }
|
||||
|
||||
if (tagToInclude.isNotEmpty()) {
|
||||
url.addQueryParameter("tag", tagToInclude.joinToString(","))
|
||||
}
|
||||
}
|
||||
is PublisherGroup -> {
|
||||
val publisherToInclude = mutableListOf<String>()
|
||||
filter.state.forEach { content ->
|
||||
if (content.state) {
|
||||
publisherToInclude.add(content.name)
|
||||
}
|
||||
}
|
||||
if (publisherToInclude.isNotEmpty()) {
|
||||
url.addQueryParameter("publisher", publisherToInclude.joinToString(","))
|
||||
}
|
||||
}
|
||||
is AuthorGroup -> {
|
||||
val authorToInclude = filter.state.filter { it.state }.map { it.author }
|
||||
|
||||
authorToInclude.forEach {
|
||||
url.addQueryParameter("author", "${it.name},${it.role}")
|
||||
}
|
||||
}
|
||||
is UriFilter -> filter.addToUri(url)
|
||||
is Filter.Sort -> {
|
||||
val state = filter.state ?: return@forEach
|
||||
|
||||
val sortCriteria = when (state.index) {
|
||||
0 -> if (type == "series") "metadata.titleSort" else "name"
|
||||
1 -> "createdDate"
|
||||
2 -> "lastModifiedDate"
|
||||
1 -> if (type == "series") "metadata.titleSort" else "name"
|
||||
2 -> "createdDate"
|
||||
3 -> "lastModifiedDate"
|
||||
else -> return@forEach
|
||||
} + "," + if (state.ascending) "asc" else "desc"
|
||||
|
||||
|
@ -211,17 +148,17 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
|
|||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage =
|
||||
processSeriesPage(response)
|
||||
KomgaUtils.processSeriesPage(response, baseUrl)
|
||||
|
||||
override fun getMangaUrl(manga: SManga) = manga.url.replace("/api/v1", "")
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga) = GET(manga.url)
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
return if (response.fromReadList()) {
|
||||
response.parseAs<ReadListDto>().toSManga()
|
||||
return if (response.isFromReadList()) {
|
||||
response.parseAs<ReadListDto>().toSManga(baseUrl)
|
||||
} else {
|
||||
response.parseAs<SeriesDto>().toSManga()
|
||||
response.parseAs<SeriesDto>().toSManga(baseUrl)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -236,34 +173,22 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
|
|||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val page = response.parseAs<PageWrapperDto<BookDto>>().content
|
||||
|
||||
val isFromReadList = response.isFromReadList()
|
||||
val r = page.mapIndexed { index, book ->
|
||||
SChapter.create().apply {
|
||||
chapter_number = if (!response.fromReadList()) book.metadata.numberSort else index + 1F
|
||||
chapter_number = if (!isFromReadList) book.metadata.numberSort else index + 1F
|
||||
url = "$baseUrl/api/v1/books/${book.id}"
|
||||
scanlator = book.metadata.authors.groupBy({ it.role }, { it.name })["translator"]?.joinToString()
|
||||
date_upload = book.metadata.releaseDate?.let { parseDate(it) }
|
||||
?: parseDateTime(book.fileLastModified)
|
||||
|
||||
val values = hashMapOf(
|
||||
"title" to book.metadata.title,
|
||||
"seriesTitle" to book.seriesTitle,
|
||||
"number" to book.metadata.number,
|
||||
"createdDate" to book.created,
|
||||
"releaseDate" to book.metadata.releaseDate,
|
||||
"size" to book.size,
|
||||
"sizeBytes" to book.sizeBytes.toString(),
|
||||
)
|
||||
val sub = StringSubstitutor(values, "{", "}")
|
||||
|
||||
name = (if (!response.fromReadList()) "" else "${book.seriesTitle} ") + sub.replace(chapterNameTemplate)
|
||||
name = KomgaUtils.formatChapterName(book, chapterNameTemplate, isFromReadList)
|
||||
scanlator = book.metadata.authors.filter { it.role == "translator" }.joinToString { it.name }
|
||||
date_upload = book.metadata.releaseDate?.let { KomgaUtils.parseDate(it) }
|
||||
?: KomgaUtils.parseDateTime(book.fileLastModified)
|
||||
}
|
||||
}
|
||||
|
||||
return r.sortedByDescending { it.chapter_number }
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request =
|
||||
GET("${chapter.url}/pages")
|
||||
override fun pageListRequest(chapter: SChapter) = GET("${chapter.url}/pages")
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val pages = response.parseAs<List<PageDto>>()
|
||||
|
@ -275,47 +200,69 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
|
|||
} else {
|
||||
""
|
||||
}
|
||||
Page(
|
||||
index = it.number,
|
||||
imageUrl = url,
|
||||
)
|
||||
|
||||
Page(it.number, imageUrl = url)
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
||||
|
||||
override fun getFilterList(): FilterList {
|
||||
val filters = try {
|
||||
mutableListOf<Filter<*>>(
|
||||
UnreadFilter(),
|
||||
InProgressFilter(),
|
||||
ReadFilter(),
|
||||
TypeSelect(),
|
||||
CollectionSelect(listOf(CollectionFilterEntry("None")) + collections.map { CollectionFilterEntry(it.name, it.id) }),
|
||||
LibraryGroup(libraries.map { LibraryFilter(it.id, it.name) }.sortedBy { it.name.lowercase(Locale.ROOT) }),
|
||||
StatusGroup(listOf("Ongoing", "Ended", "Abandoned", "Hiatus").map { StatusFilter(it) }),
|
||||
GenreGroup(genres.map { GenreFilter(it) }),
|
||||
TagGroup(tags.map { TagFilter(it) }),
|
||||
PublisherGroup(publishers.map { PublisherFilter(it) }),
|
||||
).also { list ->
|
||||
if (collections.isEmpty() && libraries.isEmpty() && genres.isEmpty() && tags.isEmpty() && publishers.isEmpty()) {
|
||||
list.add(0, Filter.Header("Press 'Reset' to show filtering options"))
|
||||
list.add(1, Filter.Separator())
|
||||
}
|
||||
|
||||
list.addAll(authors.map { (role, authors) -> AuthorGroup(role, authors.map { AuthorFilter(it) }) })
|
||||
list.add(SeriesSort())
|
||||
val filters = mutableListOf<Filter<*>>(
|
||||
UnreadFilter(),
|
||||
InProgressFilter(),
|
||||
ReadFilter(),
|
||||
TypeSelect(),
|
||||
CollectionSelect(
|
||||
buildList {
|
||||
add(CollectionFilterEntry("None"))
|
||||
collections.forEach {
|
||||
add(CollectionFilterEntry(it.name, it.id))
|
||||
}
|
||||
},
|
||||
),
|
||||
UriMultiSelectFilter(
|
||||
"Libraries",
|
||||
"library_id",
|
||||
libraries.map { UriMultiSelectOption(it.name, it.id) },
|
||||
),
|
||||
UriMultiSelectFilter(
|
||||
"Status",
|
||||
"status",
|
||||
listOf("Ongoing", "Ended", "Abandoned", "Hiatus").map {
|
||||
UriMultiSelectOption(it, it.uppercase(Locale.ROOT))
|
||||
},
|
||||
),
|
||||
UriMultiSelectFilter(
|
||||
"Genres",
|
||||
"genre",
|
||||
genres.map { UriMultiSelectOption(it) },
|
||||
),
|
||||
UriMultiSelectFilter(
|
||||
"Tags",
|
||||
"tag",
|
||||
tags.map { UriMultiSelectOption(it) },
|
||||
),
|
||||
UriMultiSelectFilter(
|
||||
"Publishers",
|
||||
"publisher",
|
||||
publishers.map { UriMultiSelectOption(it) },
|
||||
),
|
||||
).apply {
|
||||
if (collections.isEmpty() && libraries.isEmpty() && genres.isEmpty() && tags.isEmpty() && publishers.isEmpty()) {
|
||||
add(0, Filter.Header("Press 'Reset' to show filtering options"))
|
||||
add(1, Filter.Separator())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(logTag, "error while creating filter list", e)
|
||||
emptyList()
|
||||
|
||||
addAll(authors.map { (role, authors) -> AuthorGroup(role, authors.map { AuthorFilter(it) }) })
|
||||
add(SeriesSort())
|
||||
}
|
||||
|
||||
return FilterList(filters)
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
if (suffix.isBlank()) {
|
||||
if (suffix.isEmpty()) {
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_EXTRA_SOURCES_COUNT
|
||||
title = "Number of extra sources"
|
||||
|
@ -324,15 +271,9 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
|
|||
entryValues = PREF_EXTRA_SOURCES_ENTRIES
|
||||
|
||||
setDefaultValue(PREF_EXTRA_SOURCES_DEFAULT)
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
try {
|
||||
val setting = preferences.edit().putString(PREF_EXTRA_SOURCES_COUNT, newValue as String).commit()
|
||||
Toast.makeText(screen.context, "Restart Tachiyomi to apply new setting.", Toast.LENGTH_LONG).show()
|
||||
setting
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
setOnPreferenceChangeListener { _, _ ->
|
||||
Toast.makeText(screen.context, "Restart Tachiyomi to apply new setting.", Toast.LENGTH_LONG).show()
|
||||
true
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
|
@ -383,14 +324,8 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
|
|||
|
||||
setDefaultValue(PREF_CHAPTER_NAME_TEMPLATE_DEFAULT)
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
try {
|
||||
val setting = preferences.edit().putString(PREF_CHAPTER_NAME_TEMPLATE, newValue as String).commit()
|
||||
Toast.makeText(screen.context, "Restart Tachiyomi to apply new setting.", Toast.LENGTH_LONG).show()
|
||||
setting
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
Toast.makeText(screen.context, "Restart Tachiyomi to apply new setting.", Toast.LENGTH_LONG).show()
|
||||
true
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
|
@ -402,196 +337,54 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
|
|||
private var publishers = emptySet<String>()
|
||||
private var authors = emptyMap<String, List<AuthorDto>>() // roles to list of authors
|
||||
|
||||
private class TypeSelect : Filter.Select<String>("Search for", arrayOf(TYPE_SERIES, TYPE_READLISTS))
|
||||
private class LibraryFilter(val id: String, name: String) : Filter.CheckBox(name, false)
|
||||
private class LibraryGroup(libraries: List<LibraryFilter>) : Filter.Group<LibraryFilter>("Libraries", libraries)
|
||||
private class CollectionSelect(collections: List<CollectionFilterEntry>) : Filter.Select<CollectionFilterEntry>("Collection", collections.toTypedArray())
|
||||
private class SeriesSort(selection: Selection? = null) : Filter.Sort("Sort", arrayOf("Alphabetically", "Date added", "Date updated"), selection ?: Selection(0, true))
|
||||
private class StatusFilter(name: String) : Filter.CheckBox(name, false)
|
||||
private class StatusGroup(filters: List<StatusFilter>) : Filter.Group<StatusFilter>("Status", filters)
|
||||
private class UnreadFilter : Filter.CheckBox("Unread", false)
|
||||
private class InProgressFilter : Filter.CheckBox("In Progress", false)
|
||||
private class ReadFilter : Filter.CheckBox("Read", false)
|
||||
private class GenreFilter(genre: String) : Filter.CheckBox(genre, false)
|
||||
private class GenreGroup(genres: List<GenreFilter>) : Filter.Group<GenreFilter>("Genres", genres)
|
||||
private class TagFilter(tag: String) : Filter.CheckBox(tag, false)
|
||||
private class TagGroup(tags: List<TagFilter>) : Filter.Group<TagFilter>("Tags", tags)
|
||||
private class PublisherFilter(publisher: String) : Filter.CheckBox(publisher, false)
|
||||
private class PublisherGroup(publishers: List<PublisherFilter>) : Filter.Group<PublisherFilter>("Publishers", publishers)
|
||||
private class AuthorFilter(val author: AuthorDto) : Filter.CheckBox(author.name, false)
|
||||
private class AuthorGroup(role: String, authors: List<AuthorFilter>) : Filter.Group<AuthorFilter>(role.replaceFirstChar { it.titlecase() }, authors)
|
||||
|
||||
private data class CollectionFilterEntry(
|
||||
val name: String,
|
||||
val id: String? = null,
|
||||
) {
|
||||
override fun toString() = name
|
||||
}
|
||||
|
||||
private fun PreferenceScreen.addEditTextPreference(
|
||||
title: String,
|
||||
default: String,
|
||||
summary: String,
|
||||
inputType: Int? = null,
|
||||
validate: ((String) -> Boolean)? = null,
|
||||
validationMessage: String? = null,
|
||||
key: String = title,
|
||||
) {
|
||||
val preference = EditTextPreference(context).apply {
|
||||
this.key = key
|
||||
this.title = title
|
||||
this.summary = summary
|
||||
this.setDefaultValue(default)
|
||||
dialogTitle = title
|
||||
|
||||
setOnBindEditTextListener { editText ->
|
||||
if (inputType != null) {
|
||||
editText.inputType = inputType
|
||||
}
|
||||
|
||||
if (validate != null) {
|
||||
editText.addTextChangedListener(
|
||||
object : TextWatcher {
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||
|
||||
override fun afterTextChanged(editable: Editable?) {
|
||||
requireNotNull(editable)
|
||||
|
||||
val text = editable.toString()
|
||||
|
||||
val isValid = text.isBlank() || validate(text)
|
||||
|
||||
editText.error = if (!isValid) validationMessage else null
|
||||
editText.rootView.findViewById<Button>(android.R.id.button1)
|
||||
?.isEnabled = editText.error == null
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
try {
|
||||
val res = preferences.edit().putString(this.key, newValue as String).commit()
|
||||
Toast.makeText(context, "Restart Tachiyomi to apply new setting.", Toast.LENGTH_LONG).show()
|
||||
res
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addPreference(preference)
|
||||
}
|
||||
|
||||
private var fetchFiltersFailed = false
|
||||
|
||||
private var fetchFiltersAttempts = 0
|
||||
|
||||
private val fetchFiltersLock = ReentrantReadWriteLock()
|
||||
|
||||
private fun fetchFilterOptions() {
|
||||
if (baseUrl.isBlank()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (fetchFiltersAttempts > 3 || (fetchFiltersAttempts > 0 && !fetchFiltersFailed)) {
|
||||
return
|
||||
}
|
||||
|
||||
Single.fromCallable {
|
||||
val result = runCatching {
|
||||
libraries = client.newCall(GET("$baseUrl/api/v1/libraries")).execute().parseAs()
|
||||
collections = client
|
||||
.newCall(GET("$baseUrl/api/v1/collections?unpaged=true"))
|
||||
.execute()
|
||||
.parseAs<PageWrapperDto<CollectionDto>>()
|
||||
.content
|
||||
genres = client.newCall(GET("$baseUrl/api/v1/genres")).execute().parseAs()
|
||||
tags = client.newCall(GET("$baseUrl/api/v1/tags")).execute().parseAs()
|
||||
publishers = client.newCall(GET("$baseUrl/api/v1/publishers")).execute().parseAs()
|
||||
authors = client
|
||||
.newCall(GET("$baseUrl/api/v1/authors"))
|
||||
.execute()
|
||||
.parseAs<List<AuthorDto>>()
|
||||
.groupBy { it.role }
|
||||
fetchFiltersLock.read {
|
||||
if (fetchFiltersAttempts > 3 || (fetchFiltersAttempts > 0 && !fetchFiltersFailed)) {
|
||||
return@fromCallable
|
||||
}
|
||||
}
|
||||
.onFailure {
|
||||
Log.e(logTag, "Could not fetch filtering options", it)
|
||||
|
||||
fetchFiltersLock.write {
|
||||
fetchFiltersFailed = try {
|
||||
libraries = client.newCall(GET("$baseUrl/api/v1/libraries")).execute().parseAs()
|
||||
collections = client
|
||||
.newCall(GET("$baseUrl/api/v1/collections?unpaged=true"))
|
||||
.execute()
|
||||
.parseAs<PageWrapperDto<CollectionDto>>()
|
||||
.content
|
||||
genres = client.newCall(GET("$baseUrl/api/v1/genres")).execute().parseAs()
|
||||
tags = client.newCall(GET("$baseUrl/api/v1/tags")).execute().parseAs()
|
||||
publishers = client.newCall(GET("$baseUrl/api/v1/publishers")).execute().parseAs()
|
||||
authors = client
|
||||
.newCall(GET("$baseUrl/api/v1/authors"))
|
||||
.execute()
|
||||
.parseAs<List<AuthorDto>>()
|
||||
.groupBy { it.role }
|
||||
false
|
||||
} catch (e: Exception) {
|
||||
Log.e(logTag, "Could not fetch filter options", e)
|
||||
true
|
||||
}
|
||||
|
||||
fetchFiltersFailed = result.isFailure
|
||||
fetchFiltersAttempts++
|
||||
fetchFiltersAttempts++
|
||||
}
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
private fun processSeriesPage(response: Response): MangasPage {
|
||||
return if (response.fromReadList()) {
|
||||
val data = response.parseAs<PageWrapperDto<ReadListDto>>()
|
||||
|
||||
MangasPage(data.content.map { it.toSManga() }, !data.last)
|
||||
} else {
|
||||
val data = response.parseAs<PageWrapperDto<SeriesDto>>()
|
||||
|
||||
MangasPage(data.content.map { it.toSManga() }, !data.last)
|
||||
}
|
||||
}
|
||||
|
||||
private fun SeriesDto.toSManga(): SManga =
|
||||
SManga.create().apply {
|
||||
title = metadata.title
|
||||
url = "$baseUrl/api/v1/series/$id"
|
||||
thumbnail_url = "$url/thumbnail"
|
||||
status = when {
|
||||
metadata.status == "ENDED" && metadata.totalBookCount != null && booksCount < metadata.totalBookCount -> SManga.PUBLISHING_FINISHED
|
||||
metadata.status == "ENDED" -> SManga.COMPLETED
|
||||
metadata.status == "ONGOING" -> SManga.ONGOING
|
||||
metadata.status == "ABANDONED" -> SManga.CANCELLED
|
||||
metadata.status == "HIATUS" -> SManga.ON_HIATUS
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
genre = (metadata.genres + metadata.tags + booksMetadata.tags).distinct().joinToString(", ")
|
||||
description = metadata.summary.ifBlank { booksMetadata.summary }
|
||||
booksMetadata.authors.groupBy { it.role }.let { map ->
|
||||
author = map["writer"]?.map { it.name }?.distinct()?.joinToString()
|
||||
artist = map["penciller"]?.map { it.name }?.distinct()?.joinToString()
|
||||
}
|
||||
}
|
||||
|
||||
private fun ReadListDto.toSManga(): SManga =
|
||||
SManga.create().apply {
|
||||
title = name
|
||||
description = summary
|
||||
url = "$baseUrl/api/v1/readlists/$id"
|
||||
thumbnail_url = "$url/thumbnail"
|
||||
status = SManga.UNKNOWN
|
||||
}
|
||||
|
||||
private fun Response.fromReadList() = request.url.toString().contains("/api/v1/readlists")
|
||||
|
||||
private fun parseDate(date: String?): Long = runCatching {
|
||||
KomgaHelper.formatterDate.parse(date!!)!!.time
|
||||
}.getOrDefault(0L)
|
||||
|
||||
private fun parseDateTime(date: String?) = if (date == null) {
|
||||
0L
|
||||
} else {
|
||||
runCatching {
|
||||
KomgaHelper.formatterDateTime.parse(date)!!.time
|
||||
}
|
||||
.getOrElse {
|
||||
KomgaHelper.formatterDateTimeMilli.parse(date)?.time ?: 0L
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <reified T> Response.parseAs(): T {
|
||||
return json.decodeFromString(body.string())
|
||||
}
|
||||
|
||||
private val logTag = "komga${if (suffix.isNotBlank()) ".$suffix" else ""}"
|
||||
|
||||
companion object {
|
||||
|
@ -608,7 +401,7 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
|
|||
|
||||
private val supportedImageTypes = listOf("image/jpeg", "image/png", "image/gif", "image/webp", "image/jxl", "image/heif", "image/avif")
|
||||
|
||||
private const val TYPE_SERIES = "Series"
|
||||
private const val TYPE_READLISTS = "Read lists"
|
||||
internal const val TYPE_SERIES = "Series"
|
||||
internal const val TYPE_READLISTS = "Read lists"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
package eu.kanade.tachiyomi.extension.all.komga
|
||||
|
||||
import eu.kanade.tachiyomi.extension.all.komga.dto.AuthorDto
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import okhttp3.HttpUrl
|
||||
|
||||
interface UriFilter {
|
||||
fun addToUri(builder: HttpUrl.Builder)
|
||||
}
|
||||
|
||||
internal class TypeSelect : Filter.Select<String>(
|
||||
"Search for",
|
||||
arrayOf(
|
||||
Komga.TYPE_SERIES,
|
||||
Komga.TYPE_READLISTS,
|
||||
),
|
||||
)
|
||||
|
||||
internal class SeriesSort(selection: Selection? = null) : Filter.Sort(
|
||||
"Sort",
|
||||
arrayOf("None", "Alphabetically", "Date added", "Date updated"),
|
||||
selection ?: Selection(0, true),
|
||||
)
|
||||
|
||||
internal class UnreadFilter : Filter.CheckBox("Unread", false), UriFilter {
|
||||
override fun addToUri(builder: HttpUrl.Builder) {
|
||||
if (!state) {
|
||||
return
|
||||
}
|
||||
|
||||
builder.addQueryParameter("read_status", "UNREAD")
|
||||
builder.addQueryParameter("read_status", "IN_PROGRESS")
|
||||
}
|
||||
}
|
||||
|
||||
internal class InProgressFilter : Filter.CheckBox("In Progress", false), UriFilter {
|
||||
override fun addToUri(builder: HttpUrl.Builder) {
|
||||
if (!state) {
|
||||
return
|
||||
}
|
||||
|
||||
builder.addQueryParameter("read_status", "IN_PROGRESS")
|
||||
}
|
||||
}
|
||||
|
||||
internal class ReadFilter : Filter.CheckBox("Read", false), UriFilter {
|
||||
override fun addToUri(builder: HttpUrl.Builder) {
|
||||
if (!state) {
|
||||
return
|
||||
}
|
||||
|
||||
builder.addQueryParameter("read_status", "READ")
|
||||
}
|
||||
}
|
||||
|
||||
internal class UriMultiSelectOption(name: String, val id: String = name) : Filter.CheckBox(name, false)
|
||||
|
||||
internal class UriMultiSelectFilter(
|
||||
name: String,
|
||||
private val param: String,
|
||||
genres: List<UriMultiSelectOption>,
|
||||
) : Filter.Group<UriMultiSelectOption>(name, genres), UriFilter {
|
||||
override fun addToUri(builder: HttpUrl.Builder) {
|
||||
val whatToInclude = state.filter { it.state }.map { it.id }
|
||||
|
||||
if (whatToInclude.isNotEmpty()) {
|
||||
builder.addQueryParameter(param, whatToInclude.joinToString(","))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class AuthorFilter(val author: AuthorDto) : Filter.CheckBox(author.name, false)
|
||||
|
||||
internal class AuthorGroup(
|
||||
role: String,
|
||||
authors: List<AuthorFilter>,
|
||||
) : Filter.Group<AuthorFilter>(role.replaceFirstChar { it.titlecase() }, authors), UriFilter {
|
||||
override fun addToUri(builder: HttpUrl.Builder) {
|
||||
val authorToInclude = state.filter { it.state }.map { it.author }
|
||||
|
||||
authorToInclude.forEach {
|
||||
builder.addQueryParameter("author", "${it.name},${it.role}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class CollectionSelect(val collections: List<CollectionFilterEntry>) : Filter.Select<String>("Collection", collections.map { it.name }.toTypedArray())
|
||||
|
||||
internal data class CollectionFilterEntry(val name: String, val id: String? = null)
|
|
@ -1,14 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.all.komga
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
object KomgaHelper {
|
||||
val formatterDate = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||
.apply { timeZone = TimeZone.getTimeZone("UTC") }
|
||||
val formatterDateTime = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US)
|
||||
.apply { timeZone = TimeZone.getTimeZone("UTC") }
|
||||
val formatterDateTimeMilli = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.S", Locale.US)
|
||||
.apply { timeZone = TimeZone.getTimeZone("UTC") }
|
||||
}
|
|
@ -0,0 +1,175 @@
|
|||
package eu.kanade.tachiyomi.extension.all.komga
|
||||
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.widget.Button
|
||||
import android.widget.Toast
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.extension.all.komga.KomgaUtils.toSManga
|
||||
import eu.kanade.tachiyomi.extension.all.komga.dto.BookDto
|
||||
import eu.kanade.tachiyomi.extension.all.komga.dto.PageWrapperDto
|
||||
import eu.kanade.tachiyomi.extension.all.komga.dto.ReadListDto
|
||||
import eu.kanade.tachiyomi.extension.all.komga.dto.SeriesDto
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Response
|
||||
import org.apache.commons.text.StringSubstitutor
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
internal object KomgaUtils {
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
val formatterDate = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||
.apply { timeZone = TimeZone.getTimeZone("UTC") }
|
||||
val formatterDateTime = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US)
|
||||
.apply { timeZone = TimeZone.getTimeZone("UTC") }
|
||||
val formatterDateTimeMilli = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.S", Locale.US)
|
||||
.apply { timeZone = TimeZone.getTimeZone("UTC") }
|
||||
|
||||
fun parseDate(date: String?): Long = runCatching {
|
||||
formatterDate.parse(date!!)!!.time
|
||||
}.getOrDefault(0L)
|
||||
|
||||
fun parseDateTime(date: String?) = if (date == null) {
|
||||
0L
|
||||
} else {
|
||||
runCatching {
|
||||
formatterDateTime.parse(date)!!.time
|
||||
}
|
||||
.getOrElse {
|
||||
formatterDateTimeMilli.parse(date)?.time ?: 0L
|
||||
}
|
||||
}
|
||||
|
||||
fun Response.isFromReadList() = request.url.toString().contains("/api/v1/readlists")
|
||||
|
||||
fun processSeriesPage(response: Response, baseUrl: String): MangasPage {
|
||||
return if (response.isFromReadList()) {
|
||||
val data = response.parseAs<PageWrapperDto<ReadListDto>>()
|
||||
|
||||
MangasPage(data.content.map { it.toSManga(baseUrl) }, !data.last)
|
||||
} else {
|
||||
val data = response.parseAs<PageWrapperDto<SeriesDto>>()
|
||||
|
||||
MangasPage(data.content.map { it.toSManga(baseUrl) }, !data.last)
|
||||
}
|
||||
}
|
||||
|
||||
fun formatChapterName(book: BookDto, chapterNameTemplate: String, isFromReadList: Boolean): String {
|
||||
val values = hashMapOf(
|
||||
"title" to book.metadata.title,
|
||||
"seriesTitle" to book.seriesTitle,
|
||||
"number" to book.metadata.number,
|
||||
"createdDate" to book.created,
|
||||
"releaseDate" to book.metadata.releaseDate,
|
||||
"size" to book.size,
|
||||
"sizeBytes" to book.sizeBytes.toString(),
|
||||
)
|
||||
val sub = StringSubstitutor(values, "{", "}")
|
||||
|
||||
return buildString {
|
||||
if (isFromReadList) {
|
||||
append(book.seriesTitle)
|
||||
append(" ")
|
||||
}
|
||||
|
||||
append(sub.replace(chapterNameTemplate))
|
||||
}
|
||||
}
|
||||
|
||||
fun SeriesDto.toSManga(baseUrl: String): SManga =
|
||||
SManga.create().apply {
|
||||
title = metadata.title
|
||||
url = "$baseUrl/api/v1/series/$id"
|
||||
thumbnail_url = "$url/thumbnail"
|
||||
status = when {
|
||||
metadata.status == "ENDED" && metadata.totalBookCount != null && booksCount < metadata.totalBookCount -> SManga.PUBLISHING_FINISHED
|
||||
metadata.status == "ENDED" -> SManga.COMPLETED
|
||||
metadata.status == "ONGOING" -> SManga.ONGOING
|
||||
metadata.status == "ABANDONED" -> SManga.CANCELLED
|
||||
metadata.status == "HIATUS" -> SManga.ON_HIATUS
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
genre = (metadata.genres + metadata.tags + booksMetadata.tags).distinct().joinToString(", ")
|
||||
description = metadata.summary.ifBlank { booksMetadata.summary }
|
||||
booksMetadata.authors.groupBy { it.role }.let { map ->
|
||||
author = map["writer"]?.map { it.name }?.distinct()?.joinToString()
|
||||
artist = map["penciller"]?.map { it.name }?.distinct()?.joinToString()
|
||||
}
|
||||
}
|
||||
|
||||
fun ReadListDto.toSManga(baseUrl: String): SManga =
|
||||
SManga.create().apply {
|
||||
title = name
|
||||
description = summary
|
||||
url = "$baseUrl/api/v1/readlists/$id"
|
||||
thumbnail_url = "$url/thumbnail"
|
||||
status = SManga.UNKNOWN
|
||||
}
|
||||
|
||||
fun PreferenceScreen.addEditTextPreference(
|
||||
title: String,
|
||||
default: String,
|
||||
summary: String,
|
||||
inputType: Int? = null,
|
||||
validate: ((String) -> Boolean)? = null,
|
||||
validationMessage: String? = null,
|
||||
key: String = title,
|
||||
) {
|
||||
EditTextPreference(context).apply {
|
||||
this.key = key
|
||||
this.title = title
|
||||
this.summary = summary
|
||||
this.setDefaultValue(default)
|
||||
dialogTitle = title
|
||||
|
||||
setOnBindEditTextListener { editText ->
|
||||
if (inputType != null) {
|
||||
editText.inputType = inputType
|
||||
}
|
||||
|
||||
if (validate != null) {
|
||||
editText.addTextChangedListener(
|
||||
object : TextWatcher {
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||
|
||||
override fun afterTextChanged(editable: Editable?) {
|
||||
requireNotNull(editable)
|
||||
|
||||
val text = editable.toString()
|
||||
|
||||
val isValid = text.isBlank() || validate(text)
|
||||
|
||||
editText.error = if (!isValid) validationMessage else null
|
||||
editText.rootView.findViewById<Button>(android.R.id.button1)
|
||||
?.isEnabled = editText.error == null
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
setOnPreferenceChangeListener { _, _ ->
|
||||
try {
|
||||
Toast.makeText(context, "Restart Tachiyomi to apply new setting.", Toast.LENGTH_LONG).show()
|
||||
text.isBlank() || validate?.invoke(text) ?: true
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
}.also(::addPreference)
|
||||
}
|
||||
|
||||
inline fun <reified T> Response.parseAs(): T {
|
||||
return json.decodeFromString(body.string())
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue