diff --git a/src/all/komga/build.gradle b/src/all/komga/build.gradle index 5800f78e0..db502b2d6 100644 --- a/src/all/komga/build.gradle +++ b/src/all/komga/build.gradle @@ -1,7 +1,7 @@ ext { extName = 'Komga' extClass = '.KomgaFactory' - extVersionCode = 52 + extVersionCode = 53 } apply from: "$rootDir/common.gradle" diff --git a/src/all/komga/src/eu/kanade/tachiyomi/extension/all/komga/Komga.kt b/src/all/komga/src/eu/kanade/tachiyomi/extension/all/komga/Komga.kt index af808791e..cbbae05a7 100644 --- a/src/all/komga/src/eu/kanade/tachiyomi/extension/all/komga/Komga.kt +++ b/src/all/komga/src/eu/kanade/tachiyomi/extension/all/komga/Komga.kt @@ -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().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() - 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().toSManga() + return if (response.isFromReadList()) { + response.parseAs().toSManga(baseUrl) } else { - response.parseAs().toSManga() + response.parseAs().toSManga(baseUrl) } } @@ -236,34 +173,22 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere override fun chapterListParse(response: Response): List { val page = response.parseAs>().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 { val pages = response.parseAs>() @@ -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>( - 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>( + 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() private var authors = emptyMap>() // roles to list of authors - private class TypeSelect : Filter.Select("Search for", arrayOf(TYPE_SERIES, TYPE_READLISTS)) - private class LibraryFilter(val id: String, name: String) : Filter.CheckBox(name, false) - private class LibraryGroup(libraries: List) : Filter.Group("Libraries", libraries) - private class CollectionSelect(collections: List) : Filter.Select("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) : Filter.Group("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) : Filter.Group("Genres", genres) - private class TagFilter(tag: String) : Filter.CheckBox(tag, false) - private class TagGroup(tags: List) : Filter.Group("Tags", tags) - private class PublisherFilter(publisher: String) : Filter.CheckBox(publisher, false) - private class PublisherGroup(publishers: List) : Filter.Group("Publishers", publishers) - private class AuthorFilter(val author: AuthorDto) : Filter.CheckBox(author.name, false) - private class AuthorGroup(role: String, authors: List) : Filter.Group(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