Overhaul 2025 (See PR for changes)

This commit is contained in:
Mio.
2025-06-27 23:44:38 +02:00
parent c52d8831aa
commit 965c2a4f43
81 changed files with 4404 additions and 2086 deletions

View File

@@ -1,2 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="eu.kanade.tachiyomi.permission.READ_SOURCES" />
<uses-permission android:name="eu.kanade.tachiyomi.permission.WRITE_SOURCES" />
<application
android:usesCleartextTraffic="true" />
</manifest>

View File

@@ -1,93 +0,0 @@
## 1.3.13
### Fixed
* Fixed 'null cannot be cast to non-null type' exception
## 1.3.12
### Features
* Migrate filters to v2
* Implemented smartFilters
* Added localization support
### Fixed
* Fixed publication status not showing
## 1.3.10
### Features
* API Change for Kavita v0.7.2
## 1.3.9
### Features
* Added pdf support
## 1.3.8
### Fix
* Fixed `Expected URL scheme 'http' or 'https` when downloading
## 1.3.7
### Features
* New Sort filter: Time to read
* New Filter: Year release filter
### Fix
* Filters can now be used together with search
* Epub and pdfs no longer show in format filter (currently not supported)
## 1.3.6
### Fix
* Fixed "lateinit property title not initialized"
## 1.3.5
### Features
* Ignore DOH
* Added sort option `Item Added`
* Latest button now shows latest `Item Added`
## 1.3.4
### Features
* Exclude from bulk update warnings
## 1.2.3
### Fix
* Fixed Rating filter
* Fixed Chapter list not sorting correctly
* Fixed search
* Fixed manga details not showing correctly
* Fixed filters not populating if account was not admin
### Features
* The extension is now ready to implement tracking.
* Min required version for the extension to work properly: `v0.5.1.1`
## 1.2.2
### Features
* Add `CHANGELOG.md` & `README.md`
## 1.2.1
### Features
* first version

View File

@@ -1,37 +1,41 @@
# Kavita
# Komga
Table of Content
- [FAQ](#FAQ)
- [Why do I see no manga?](#why-do-i-see-no-manga)
- [Where can I get more information about Kavita?](#where-can-i-get-more-information-about-kavita)
- [The Kavita extension stopped working?](#the-kavita-extension-stopped-working)
- [Can I add more than one Kavita server or user?](#can-i-add-more-than-one-kavita-server-or-user)
- [Can I test the Kavita extension before setting up my own server?](#can-i-test-the-kavita-extension-before-setting-up-my-own-server)
- [Why do I see no manga?](#why-do-i-see-no-manga)
- [Where can I get more information about Komga?](#where-can-i-get-more-information-about-komga)
- [The Komga extension stopped working?](#the-komga-extension-stopped-working)
- [Can I add more than one Komga server or user?](#can-i-add-more-than-one-komga-server-or-user)
- [Can I test the Komga extension before setting up my own server?](#can-i-test-the-komga-extension-before-setting-up-my-own-server)
- [Guides](#Guides)
- [How do I add my Kavita server to Tachiyomi?](#how-do-i-add-my-kavita-server-to-tachiyomi)
Don't find the question you are look for go check out our general FAQs and Guides over at [Extension FAQ](https://tachiyomi.org/help/faq/#extensions) or [Getting Started](https://tachiyomi.org/help/guides/getting-started/#installation)
Kavita also has a documentation about the Tachiyomi Kavita extension at the [Kavita wiki](https://wiki.kavitareader.com/en/guides/misc/tachiyomi).
- [How do I add my Komga server to the app?](#how-do-i-add-my-komga-server-to-the-app)
## FAQ
### Why do I see no manga?
Kavita is a self-hosted comic/manga media server.
Komga is a self-hosted comic/manga media server.
### Where can I get more information about Kavita?
You can visit the [Kavita](https://www.kavitareader.com/) website for for more information.
### Where can I get more information about Komga?
You can visit the [Komga](https://komga.org/) website for for more information.
### The Kavita extension stopped working?
Make sure that your Kavita server and extension are on the newest version.
### The Komga extension stopped working?
Make sure that your Komga server and extension are on the newest version.
### Can I add more than one Kavita server or user?
Yes, currently you can add up to 3 different Kavita instances to Tachiyomi.
### Can I add more than one Komga server or user?
Yes, currently you can add up to 11 different Komga instances to the app. The number of instances
available can be customized in extension settings.
### Can I test the Kavita extension before setting up my own server?
Yes, you can try it out with the DEMO servers OPDS url `https://demo.kavitareader.com/api/opds/aca1c50d-7e08-4f37-b356-aecd6bf69b72`.
### Can I test the Komga extension before setting up my own server?
Yes, you can try it out with the DEMO server `https://demo.komga.org`, username `demo@komga.org` and
password `komga-demo`.
### Why are EPUB chapters/books missing?
Mihon/Tachiyomi does not support reading text. EPUB files containing only images will be available
if your Komga server version is [1.9.0](https://github.com/gotson/komga/releases/tag/1.9.0)
or newer.
## Guides
### How do I add my Kavita server to Tachiyomi?
Go into the settings of the Kavita extension from the Extension tab in Browse and fill in your OPDS url.
### How do I add my Komga server to the app?
Go into the settings of the Komga extension (under Browse -> Extensions) and fill in your server
address and login details.

View File

@@ -3,6 +3,20 @@ login_errors_header_token_empty="Error: The JSON Web Token is empty.\nTry openin
login_errors_invalid_url=Invalid URL:
login_errors_parse_tokendto=There was an error parsing the auth token
pref_customsource_title=Displayed name for source
pref_group_tags_title=Group Tags (fork must support grouping)
pref_group_tags_summary_on=Will prefix tags with their type
pref_group_tags_summary_off=List all tags together
pref_last_volume_cover_title=Keep cover updated
pref_last_volume_cover_summary_off=Keep current cover
pref_last_volume_cover_summary_on=Use the last volume cover when updating
pref_show_epub_title=Show EPUB (EPUB can't be read in Mihon)
pref_show_epub_summary_off=Hide EPUB titles in browsing
pref_show_epub_summary_on=Show EPUB titles in browsing
pref_rd_date_title=Reading List: Display Release Date
pref_rd_date_summary_off=Do not display the release date
pref_rd_date_summary_on=Display the release date | !! Once you toggle it on, if you toggle it off again, it will stays as N/A !!
pref_reset_title=Reset All Settings
pref_reset_summary=Clear all preferences for this Kavita instance (Only affect the selected instance)
pref_edit_customsource_summary=Here you can change this source name.\nYou can write a descriptive name to identify this OPDS URL.
pref_filters_summary=Show these filters in the filter list
pref_filters_title=Default filters shown
@@ -10,7 +24,7 @@ pref_opds_badformed_url=Incorrect OPDS address. Please copy it from User setting
pref_opds_duplicated_source_url=The URL is configured in a different source ->
pref_opds_must_setup_address=You must set up the address to communicate with Kavita
pref_opds_summary=The OPDS URL copied from User Settings. This should include address and end with the API key.
restartapp_settings=Restart Tachiyomi to apply new setting.
restartapp_settings=Restart Mihon to apply new setting.
version_exceptions_chapters_parse=Unhandled exception parsing chapters. Send your logs to the Kavita devs.
check_version=Ensure you have the newest version of the extension and Kavita. (0.7.8 or newer.)\nIf the issue persists, report it to the Kavita developers with the accompanying logs.
version_exceptions_smart_filter=Could not decode SmartFilter. Ensure you are using Kavita version 0.7.11 or later.

View File

@@ -1,6 +1,4 @@
version_exceptions_chapters_parse=Exception non trait\u00E9e durant l'analyse des chapitres. Envoyez les journaux aux d\u00E9velopeurs de Kavita
version_exceptions_chapters_parse=Exception non trait\u00E9e durant l'analyse des chapitres. Envoyez les logs aux d\u00E9velopeurs de Kavita.
pref_customsource_title=Nom d'affichage pour la source
version_exceptions_smart_filter=\u00C9chec du d\u00E9codage de SmartFilter. Assurez-vous que vous utilisez au moins Kavita version 0.7.11
pref_opds_summary=L'URL OPDS a \u00E9t\u00E9 copi\u00E9e \u00E0 partir des param\u00E8tres de l'utilisateur. Ceci devrait inclure l'adresse et la cl\u00E9 API.
@@ -8,13 +6,13 @@ pref_filters_summary=Afficher ces filtres dans la liste des filtres
check_version=Assurez-vous que vous avez l'extension et Kavita mises \u00E0 jour. (version Mini\u202F: 0.7.8)\nSi le probl\u00E8me persiste, signalez-le aux d\u00E9veloppeurs de Kavita en fournissant des journaux
pref_filters_title=Filtres par d\u00E9faut affich\u00E9s
pref_edit_customsource_summary=Ici vous pouvez changer ce nom source.\nVous pouvez \u00E9crire un nom descriptif pour identifier cette URL opds
pref_opds_badformed_url=L'adresse OPDS n'est pas correcte. Veuillez la copiez \u00E0 partir des param\u00E8tres de l'utilisateur - > Applis tierces -> URL OPDS
pref_opds_badformed_url=L'adresse OPDS n'est pas correcte. Veuillez la copier \u00E0 partir des param\u00E8tres de l'utilisateur - > Applis tierces -> URL OPDS
login_errors_parse_tokendto=Il y a eu une erreur pendant l'analyse du jeton d'authentification
restartapp_settings=Red\u00E9marrez Tachiyomi pour appliquer le nouveau r\u00E9glage.
restartapp_settings=Red\u00E9marrez Mihon pour appliquer le nouveau r\u00E9glage.
pref_opds_duplicated_source_url=L'URL est configur\u00E9e dans une autre source ->
pref_opds_must_setup_address=Vous devez configurer l'adresse pour communiquer avec Kavita
login_errors_failed_login=\u00C9chec de la connexion. Quelque chose s'est mal pass\u00E9
http_errors_500=Quelque chose s'est mal pass\u00E9
login_errors_header_token_empty=\u00AB\u00A0Erreur\u202F: le jeton jwt est vide.\nEssayez d'abord d'ouvrir l'extension\u00A0\u00BB
login_errors_header_token_empty=\u00AB\u00A0Erreur\u202F: le token jwt est vide.\nEssayez d'abord d'ouvrir l'extension\u00A0\u00BB
login_errors_invalid_url=URL invalide\u202F:
http_errors_401=Il y a eu une erreur. Essayez de nouveau ou rechargez l'application
http_errors_401=Il y a eu une erreur. Essayez de nouveau ou relancez l'application

View File

@@ -1,8 +1,6 @@
pref_customsource_title=Vist kildenavn
pref_edit_customsource_summary=Her kan du endre dette kildenavnet.\nDu kan skrive et beskrivende navn for \u00E5 identifisere denne OPDS-nettadressen.
restartapp_settings=Ny innstilling trer i kraft n\u00E5r du starter Tachiyomi p\u00E5 ny.
restartapp_settings=Ny innstilling trer i kraft n\u00E5r du starter Mihon p\u00E5 ny.
duplicated_source_url=Nettadressen er satt opp i en annen Kavita-instans
pref_filters_summary=Vis disse filterne i filterlisten
pref_filters_title=Forvalgte filtre valgt

View File

@@ -1,17 +1,14 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
kmkVersionCode = 1
extName = 'Kavita'
pkgNameSuffix = 'all.kavita'
extClass = '.KavitaFactory'
extVersionCode = 16
}
dependencies {
implementation 'info.debatty:java-string-similarity:2.0.0'
implementation(project(':lib-i18n'))
extVersionCode = 19
isNsfw = false
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation("org.apache.commons:commons-text:1.11.0")
implementation project(':lib:i18n')
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -3,18 +3,25 @@ package eu.kanade.tachiyomi.extension.all.kavita
import eu.kanade.tachiyomi.extension.all.kavita.KavitaConstants.noSmartFilterSelected
import eu.kanade.tachiyomi.source.model.Filter
class UserRating :
Filter.Select<String>(
"Minimum Rating",
arrayOf(
"Any",
"1 star",
"2 stars",
"3 stars",
"4 stars",
"5 stars",
),
)
class UserRating : Filter.Select<Int>(
"Minimum Rating",
arrayOf(
"Any",
"1 star",
"2 stars",
"3 stars",
"4 stars",
"5 stars",
).mapIndexed { index, _ -> index }.toTypedArray(),
) {
override fun toString(): String {
return when (state) {
0 -> "Any"
else -> "$state star${if (state > 1) "s" else ""}"
}
}
}
class SmartFiltersFilter(smartFilters: Array<String>) :
Filter.Select<String>("Smart Filters", arrayOf(noSmartFilterSelected) + smartFilters)
class SortFilter(sortables: Array<String>) : Filter.Sort("Sort by", sortables, Selection(0, true))
@@ -26,15 +33,27 @@ val sortableList = listOf(
Pair("Item added", 4),
Pair("Time to Read", 5),
Pair("Release year", 6),
Pair("Read Progress", 7),
Pair("Average Rating", 8),
Pair("Random", 9),
)
class SpecialListFilter : Filter.Select<String>(
"Special Lists",
arrayOf("None", "Want to Read", "Reading Lists"),
)
// Use Filter.CheckBox directly for status
class StatusFilter(name: String) : Filter.CheckBox(name, false)
class StatusFilterGroup(filters: List<StatusFilter>) :
Filter.Group<StatusFilter>("Status", filters)
class ReleaseYearRange(name: String) : Filter.Text(name)
// Use Filter.Text directly for release year range
class ReleaseYearRange(name: String) : Filter.Text(name, "")
class ReleaseYearRangeGroup(filters: List<ReleaseYearRange>) :
Filter.Group<ReleaseYearRange>("Release Year", filters)
// Use Filter.TriState for genres/tags/age ratings/collections/languages/libraries
class GenreFilter(name: String) : Filter.TriState(name)
class GenreFilterGroup(genres: List<GenreFilter>) :
Filter.Group<GenreFilter>("Genres", genres)

View File

@@ -1,10 +1,18 @@
package eu.kanade.tachiyomi.extension.all.kavita
object KavitaConstants {
val PERSON_ROLES = listOf(
"Writer", "Penciller", "Inker", "Colorist",
"Letterer", "CoverArtist", "Editor",
"Publisher", "Character", "Translator",
)
// toggle filters
const val toggledFiltersPref = "toggledFilters"
val filterPrefEntries = arrayOf(
"Sort Options",
"Special Lists",
"Format",
"Libraries",
"Read Status",
@@ -26,9 +34,13 @@ object KavitaConstants {
"Character",
"Translators",
"ReleaseYearRange",
"Average Rating",
"Random",
"Read Progress",
)
val filterPrefEntriesValue = arrayOf(
"Sort Options",
"Special Lists",
"Format",
"Libraries",
"Read Status",
@@ -50,9 +62,13 @@ object KavitaConstants {
"Character",
"Translators",
"ReleaseYearRange",
"Average Rating",
"Random",
"Read Progress",
)
val defaultFilterPrefEntries = setOf(
"Sort Options",
"Special Lists",
"Format",
"Libraries",
"Read Status",
@@ -74,6 +90,9 @@ object KavitaConstants {
"Character",
"Translators",
"ReleaseYearRange",
"Average Rating",
"Random",
"Read Progress",
)
const val customSourceNamePref = "customSourceName"

View File

@@ -1,7 +1,9 @@
package eu.kanade.tachiyomi.extension.all.kavita
import android.util.Log
import eu.kanade.tachiyomi.extension.all.kavita.dto.ChapterDto
import eu.kanade.tachiyomi.extension.all.kavita.dto.ChapterType
import eu.kanade.tachiyomi.extension.all.kavita.dto.LibraryTypeEnum
import eu.kanade.tachiyomi.extension.all.kavita.dto.PaginationInfo
import eu.kanade.tachiyomi.extension.all.kavita.dto.SeriesDto
import eu.kanade.tachiyomi.extension.all.kavita.dto.VolumeDto
@@ -42,62 +44,138 @@ class KavitaHelper {
}
fun getIdFromUrl(url: String): Int {
return url.split("/").last().toInt()
return try {
val id = url.substringAfterLast("/").toInt()
Log.d("KavitaHelper", "Extracted ID $id from URL: $url")
id
} catch (e: Exception) {
Log.e("KavitaHelper", "Failed to extract ID from URL: $url", e)
-1
}
}
fun createSeriesDto(obj: SeriesDto, baseUrl: String, apiKey: String): SManga =
// Rating Providers from Series-Details-Plus
private fun getProviderName(provider: Int): String = when (provider) {
0 -> "User"
1 -> "AniList"
2 -> "MyAnimeList"
3 -> "MangaUpdates"
else -> "Rating"
}
fun createSeriesDto(obj: SeriesDto, baseUrl: String, apiUrl: String, apiKey: String): SManga =
SManga.create().apply {
// url = "$baseUrl/library/${obj.libraryId}/series/${obj.id}"
url = "$baseUrl/Series/${obj.id}"
title = obj.name
// Deprecated: description = obj.summary
thumbnail_url = "$baseUrl/image/series-cover?seriesId=${obj.id}&apiKey=$apiKey"
}
class CompareChapters {
companion object : Comparator<SChapter> {
override fun compare(a: SChapter, b: SChapter): Int {
if (a.chapter_number < 1.0 && b.chapter_number < 1.0) {
// Both are volumes, multiply by 100 and do normal sort
return if ((a.chapter_number * 100) < (b.chapter_number * 100)) {
1
} else {
-1
}
} else {
if (a.chapter_number < 1.0 && b.chapter_number >= 1.0) {
// A is volume, b is not. A should sort first
return 1
} else if (a.chapter_number >= 1.0 && b.chapter_number < 1.0) {
return -1
}
}
if (a.chapter_number < b.chapter_number) return 1
if (a.chapter_number > b.chapter_number) return -1
return 0
// Set status based on read progress
status = when {
obj.pagesRead >= obj.pages -> SManga.COMPLETED
obj.pagesRead > 0 -> SManga.ONGOING
else -> SManga.UNKNOWN
}
}
}
fun chapterFromVolume(chapter: ChapterDto, volume: VolumeDto): SChapter =
// class CompareChapters {
// companion object : Comparator<SChapter> {
// override fun compare(a: SChapter, b: SChapter): Int {
// if (a.chapter_number < 1.0 && b.chapter_number < 1.0) {
// // Both are volumes, multiply by 100 and do normal sort
// return if ((a.chapter_number * 100) < (b.chapter_number * 100)) {
// 1
// } else {
// -1
// }
// } else {
// if (a.chapter_number < 1.0 && b.chapter_number >= 1.0) {
// // A is volume, b is not. A should sort first
// return 1
// } else if (a.chapter_number >= 1.0 && b.chapter_number < 1.0) {
// return -1
// }
// }
// if (a.chapter_number < b.chapter_number) return 1
// if (a.chapter_number > b.chapter_number) return -1
// return 0
// }
// }
// }
fun chapterFromVolume(chapter: ChapterDto, volume: VolumeDto, singleFileVolumeNumber: Int? = null, libraryType: LibraryTypeEnum? = null): SChapter =
SChapter.create().apply {
val type = ChapterType.of(chapter, volume)
val type = ChapterType.of(chapter, volume, libraryType)
name = when (type) {
ChapterType.Regular -> "Volume ${volume.number} Chapter ${chapter.number}"
ChapterType.SingleFileVolume -> "Volume ${volume.number}"
ChapterType.Special -> chapter.range
ChapterType.LooseLeaf -> {
val cleanedName = chapter.title.replaceFirst("^0+(?!$)".toRegex(), "")
"Chapter $cleanedName"
ChapterType.Regular -> {
val volumeNum = volume.number.toString().toIntOrNull()?.let {
it.toString().padStart(2, '0')
} ?: volume.number.toString()
val chapterNum = chapter.number.toIntOrNull()?.let {
it.toString().padStart(2, '0')
} ?: chapter.number
when {
chapter.titleName.isBlank() -> "Volume $volumeNum Chapter $chapterNum"
chapter.titleName.trim().matches(Regex("^\\d+$")) -> "Volume $volumeNum Chapter ${chapter.titleName.trim().padStart(2, '0')}"
else -> "Volume $volumeNum $chapterNum - ${chapter.titleName}"
}
}
ChapterType.SingleFileVolume -> {
when {
volume.name.isBlank() -> "Volume ${volume.number}"
volume.name.trim().matches(Regex("^\\d+$")) -> "Volume ${volume.name.trim().toIntOrNull()?.toString() ?: volume.name.trim()}"
else -> "${volume.number} - ${volume.name}"
}
}
ChapterType.Special -> chapter.titleName.takeIf { it.isNotBlank() } ?: chapter.range
ChapterType.Chapter -> {
val chapterNum = chapter.number.toIntOrNull()?.let {
it.toString().padStart(2, '0')
} ?: chapter.number
when {
chapter.titleName.isBlank() -> "Chapter $chapterNum"
chapter.titleName.trim().matches(Regex("^\\d+$")) -> "Chapter ${chapter.titleName.trim().padStart(2, '0')}"
else -> "$chapterNum - ${chapter.titleName}"
}
}
ChapterType.Issue -> {
val issueNum = chapter.number.toIntOrNull()?.let {
it.toString().padStart(3, '0')
} ?: chapter.number
when {
chapter.titleName.isNotBlank() && !chapter.titleName.trim().matches(Regex("^\\d+$")) ->
"#$issueNum ${chapter.titleName}"
chapter.title.isNotBlank() && !chapter.title.trim().matches(Regex("^\\d+$")) ->
"#$issueNum ${chapter.title}"
else -> "Issue #$issueNum"
}
}
}
chapter_number = if (type == ChapterType.SingleFileVolume) {
volume.number.toFloat() / 10000
} else {
chapter.number.toFloat()
// chapter_number = try {
// // if (type == ChapterType.SingleFileVolume) {
// // volume.number.toFloat() / 10000
// // } else {
// // chapter.number.toFloatOrNull() ?: 0f
// // }
// // } catch (e: NumberFormatException) {
// // 0f
// // }
chapter_number = when {
type == ChapterType.SingleFileVolume && singleFileVolumeNumber != null -> singleFileVolumeNumber.toFloat()
type != ChapterType.SingleFileVolume -> chapter.number.toFloatOrNull() ?: 0f
else -> 0f
}
url = chapter.id.toString()
url = when {
type == ChapterType.SingleFileVolume && singleFileVolumeNumber != null ->
"volume_${volume.id}"
else -> chapter.id.toString()
}
// url = chapter.id.toString()
if (chapter.fileCount > 1) {
// salt/offset to recognize chapters with new merged part-chapters as new and hence unread
@@ -105,8 +183,14 @@ class KavitaHelper {
url = "${url}_${chapter.fileCount}"
}
date_upload = parseDate(chapter.created)
scanlator = "${chapter.pages} pages"
date_upload = parseDateSafe(chapter.created)
scanlator = when (type) {
ChapterType.SingleFileVolume -> "Volume"
ChapterType.Special -> "Special"
ChapterType.Issue -> "Issue"
ChapterType.Chapter -> "Chapter"
ChapterType.Regular -> "Chapter"
}
}
val intl = Intl(

View File

@@ -0,0 +1,93 @@
package eu.kanade.tachiyomi.extension.all.kavita
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 java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
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") }
fun parseDateSafe(date: String?): Long {
return date?.let {
try {
val sdf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US)
sdf.timeZone = TimeZone.getTimeZone("UTC")
sdf.parse(it)?.time ?: 0L
} catch (_: Exception) {
0L
}
} ?: 0L
}
fun PreferenceScreen.addEditTextPreference(
title: String,
default: String,
summary: String,
dialogMessage: String? = null,
inputType: Int? = null,
validate: ((String) -> Boolean)? = null,
validationMessage: String? = null,
key: String = title,
restartRequired: Boolean = false,
) {
EditTextPreference(context).apply {
this.key = key
this.title = title
this.summary = summary
this.setDefaultValue(default)
dialogTitle = title
this.dialogMessage = dialogMessage
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 text = newValue as String
val result = text.isBlank() || validate?.invoke(text) ?: true
if (restartRequired && result) {
Toast.makeText(context, "Restart Tachiyomi to apply new setting.", Toast.LENGTH_LONG).show()
}
result
} catch (e: Exception) {
e.printStackTrace()
false
}
}
}.also(::addPreference)
}

View File

@@ -8,7 +8,7 @@ data class FilterV2Dto(
val id: Int? = null,
val name: String? = null,
val statements: MutableList<FilterStatementDto> = mutableListOf(),
val combination: Int = 0, // FilterCombination = FilterCombination.And,
val combination: Int = FilterCombination.And.ordinal,
val sortOptions: SortOptions = SortOptions(),
val limitTo: Int = 0,
) {
@@ -17,20 +17,22 @@ data class FilterV2Dto(
statements.add(FilterStatementDto(comparison.type, field.type, value))
}
}
fun addStatement(comparison: FilterComparison, field: FilterField, values: java.util.ArrayList<out Any>) {
fun addStatement(comparison: FilterComparison, field: FilterField, values: List<Any>) {
if (values.isNotEmpty()) {
statements.add(FilterStatementDto(comparison.type, field.type, values.joinToString(",")))
}
}
fun addContainsNotTriple(list: List<Triple<FilterField, java.util.ArrayList<out Any>, ArrayList<Int>>>) {
list.map {
fun addContainsNotTriple(list: List<Triple<FilterField, List<Any>, List<Int>>>) {
list.forEach {
addStatement(FilterComparison.Contains, it.first, it.second)
addStatement(FilterComparison.NotContains, it.first, it.third)
}
}
fun addPeople(list: List<Pair<FilterField, ArrayList<Int>>>) {
list.map {
fun addPeople(list: List<Pair<FilterField, List<Int>>>) {
list.forEach {
addStatement(FilterComparison.MustContains, it.first, it.second)
}
}
@@ -38,11 +40,15 @@ data class FilterV2Dto(
@Serializable
data class FilterStatementDto(
// todo: Create custom serializator for comparison and field and remove .type extension in Kavita.kt
val comparison: Int,
val field: Int,
val value: String,
)
@Serializable
data class SortOptions(
var sortField: Int = SortFieldEnum.AverageRating.type,
var isAscending: Boolean = true,
)
@Serializable
@@ -53,20 +59,17 @@ enum class SortFieldEnum(val type: Int) {
LastChapterAdded(4),
TimeToRead(5),
ReleaseYear(6),
ReadProgress(7),
AverageRating(8),
Random(9),
;
companion object {
private val map = SortFieldEnum.values().associateBy(SortFieldEnum::type)
private val map = values().associateBy(SortFieldEnum::type)
fun fromInt(type: Int) = map[type]
}
}
@Serializable
data class SortOptions(
var sortField: Int = SortFieldEnum.SortName.type,
var isAscending: Boolean = true,
)
@Serializable
enum class FilterCombination {
Or,
@@ -101,6 +104,18 @@ enum class FilterField(val type: Int) {
ReadTime(23),
Path(24),
FilePath(25),
WantToRead(26),
ReadingDate(27),
AverageRating(28),
Imprint(29),
Team(30),
Location(31),
ReadLast(32),
;
companion object {
fun fromType(type: Int): FilterField? = values().find { it.type == type }
}
}
@Serializable
@@ -121,4 +136,46 @@ enum class FilterComparison(val type: Int) {
IsAfter(13),
IsInLast(14),
IsNotInLast(15),
IsEmpty(16),
}
@Serializable
data class PersonSearchDto(
val id: Int,
val name: String,
val role: Int? = null,
)
enum class PersonRole(val id: Int) {
Writer(0),
Penciller(1),
Inker(2),
Colorist(3),
Letterer(4),
CoverArtist(5),
Editor(6),
Publisher(7),
Character(8),
Translator(9),
;
companion object {
private val map = values().associateBy(PersonRole::id)
fun fromId(id: Int): PersonRole? = map[id]
}
}
fun PersonRole.toFilterField(): FilterField? {
return when (this) {
PersonRole.Writer -> FilterField.Writers
PersonRole.Penciller -> FilterField.Penciller
PersonRole.Inker -> FilterField.Inker
PersonRole.Colorist -> FilterField.Colorist
PersonRole.Letterer -> FilterField.Letterer
PersonRole.CoverArtist -> FilterField.CoverArtist
PersonRole.Editor -> FilterField.Editor
PersonRole.Publisher -> FilterField.Publisher
PersonRole.Character -> FilterField.Characters
PersonRole.Translator -> FilterField.Translators
}
}

View File

@@ -1,7 +1,13 @@
package eu.kanade.tachiyomi.extension.all.kavita.dto
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
interface ConvertibleToSManga {
fun toSManga(baseUrl: String, apiUrl: String, apiKey: String): SManga
}
@Serializable
enum class MangaFormat(val format: Int) {
Image(0),
@@ -10,26 +16,32 @@ enum class MangaFormat(val format: Int) {
Epub(3),
Pdf(4),
;
companion object {
private val map = PersonRole.values().associateBy(PersonRole::role)
fun fromInt(type: Int) = map[type]
private val map = values().associateBy(MangaFormat::format)
fun fromInt(type: Int): MangaFormat? = map[type]
}
}
enum class PersonRole(val role: Int) {
Other(1),
Writer(3),
Penciller(4),
Inker(5),
Colorist(6),
Letterer(7),
CoverArtist(8),
Editor(9),
Publisher(10),
Character(11),
Translator(12),
@Serializable
data class LibraryDto(
val id: Int,
val name: String,
val type: Int,
)
@Serializable // https://github.com/Kareadita/Kavita/blob/develop/API/Entities/Enums/LibraryType.cs
enum class LibraryTypeEnum(val type: Int) {
Manga(0),
Comic(1),
Book(2),
Image(3),
LightNovel(4),
ComicVine(5),
;
companion object {
private val map = PersonRole.values().associateBy(PersonRole::role)
private val map = values().associateBy(LibraryTypeEnum::type)
fun fromInt(type: Int) = map[type]
}
}
@@ -51,28 +63,127 @@ data class SeriesDto(
val created: String? = "",
val libraryId: Int,
val libraryName: String? = "",
val ratings: List<RatingDto> = emptyList(),
)
@Serializable
data class SeriesMetadataDto(
data class SeriesDetailPlusDto(
val seriesId: Int? = null,
val libraryName: String? = "",
val libraryId: Int? = null,
val summary: String? = null,
val genres: List<MetadataGenres> = emptyList(),
val tags: List<MetadataTag> = emptyList(),
val writers: List<MetadataPeople> = emptyList(),
val coverArtists: List<MetadataPeople> = emptyList(),
val publicationStatus: Int? = null,
val averageScore: Float = 0f,
val ratings: List<RatingDto> = emptyList(),
) {
// Helper function to get the library name from SeriesDto if needed
fun getLibraryName(seriesDto: SeriesDto?): String? {
return if (!libraryName.isNullOrEmpty()) {
libraryName
} else {
seriesDto?.libraryName
}
}
// Helper function to get the library ID from SeriesDto if needed
fun getLibraryId(seriesDto: SeriesDto?): Int? {
return libraryId ?: seriesDto?.libraryId
}
}
@Serializable
data class SeriesDetailPlusWrapperDto(
val series: SeriesPlus? = null,
val ratings: List<RatingDto> = emptyList(),
val recommendations: RecommendationsDto? = null,
)
@Serializable
data class SeriesPlus(
val averageScore: Float = 0f,
val summary: String = "",
val relations: List<RelationDto> = emptyList(),
)
@Serializable
data class RecommendationsDto(
val ownedSeries: List<SeriesDto> = emptyList(),
)
@Serializable
data class RatingDto(
val averageScore: Float = 0f,
val favoriteCount: Int = 0,
val provider: Int = 0,
val authority: Int = 0,
val providerUrl: String? = null,
)
@Serializable
data class RelationDto(
val seriesName: RelationName,
val relation: Int,
val aniListId: Int? = null,
val malId: Int? = null,
val provider: Int? = null,
val plusMediaFormat: Int? = null,
)
@Serializable
data class RelationName(
val englishTitle: String? = null,
val romajiTitle: String? = null,
val nativeTitle: String? = null,
val preferredTitle: String? = null,
)
@Serializable
data class RelatedSeriesResponse(
val sourceSeriesId: Int,
val sequels: List<RelatedSeriesItem> = emptyList(),
val prequels: List<RelatedSeriesItem> = emptyList(),
val spinOffs: List<RelatedSeriesItem> = emptyList(),
val adaptations: List<RelatedSeriesItem> = emptyList(),
val sideStories: List<RelatedSeriesItem> = emptyList(),
val characters: List<RelatedSeriesItem> = emptyList(),
val contains: List<RelatedSeriesItem> = emptyList(),
val others: List<RelatedSeriesItem> = emptyList(),
val alternativeSettings: List<RelatedSeriesItem> = emptyList(),
val alternativeVersions: List<RelatedSeriesItem> = emptyList(),
val doujinshis: List<RelatedSeriesItem> = emptyList(),
val parent: List<RelatedSeriesItem> = emptyList(),
val editions: List<RelatedSeriesItem> = emptyList(),
val annuals: List<RelatedSeriesItem> = emptyList(),
)
@Serializable
data class RelatedSeriesItem(
val id: Int,
val summary: String? = "",
val writers: List<Person> = emptyList(),
val coverArtists: List<Person> = emptyList(),
val genres: List<Genres> = emptyList(),
val seriesId: Int,
val ageRating: Int,
val publicationStatus: Int,
)
@Serializable
data class Genres(
val title: String,
)
@Serializable
data class Person(
val name: String,
val coverImage: String? = null,
val libraryId: Int,
val libraryName: String? = null,
val format: Int = 0,
) : ConvertibleToSManga {
override fun toSManga(baseUrl: String, apiUrl: String, apiKey: String): SManga = SManga.create().apply {
title = name
url = "$baseUrl/Series/$id"
thumbnail_url = when {
!coverImage.isNullOrBlank() && (coverImage.startsWith("http://") || coverImage.startsWith("https://")) -> coverImage
else -> "$apiUrl/image/series-cover?seriesId=$id&apiKey=$apiKey"
}
initialized = true
}
}
@Serializable
data class AuthorDto(
val name: String,
val role: String,
)
@Serializable
@@ -85,27 +196,33 @@ data class VolumeDto(
val lastModified: String,
val created: String,
val seriesId: Int,
val coverImage: String,
val chapters: List<ChapterDto> = emptyList(),
)
@Serializable
enum class ChapterType {
Regular, // chapter with volume information
LooseLeaf, // chapter without volume information
Chapter, // manga chapter without volume information
SingleFileVolume,
Special,
Issue, // For comics
;
companion object {
fun of(chapter: ChapterDto, volume: VolumeDto): ChapterType =
if (volume.number == 100_000) {
Special
} else if (volume.number == -100_000) {
LooseLeaf
} else {
if (chapter.number == "-100000") {
SingleFileVolume
} else {
Regular
fun of(chapter: ChapterDto, volume: VolumeDto, libraryType: LibraryTypeEnum? = null): ChapterType =
when {
volume.number == 100_000 -> Special
volume.number == -100_000 -> when (libraryType) {
LibraryTypeEnum.Comic, LibraryTypeEnum.ComicVine -> Issue
LibraryTypeEnum.Manga, LibraryTypeEnum.LightNovel, LibraryTypeEnum.Book -> Chapter
else -> Chapter // Default to Chapter for other types
}
chapter.number == "-100000" -> SingleFileVolume
else -> when (libraryType) {
LibraryTypeEnum.Comic, LibraryTypeEnum.ComicVine -> Issue
LibraryTypeEnum.Manga, LibraryTypeEnum.LightNovel, LibraryTypeEnum.Book -> Chapter
else -> Regular
}
}
}
@@ -119,10 +236,12 @@ data class ChapterDto(
val pages: Int,
val isSpecial: Boolean,
val title: String,
val titleName: String,
val pagesRead: Int,
val coverImageLocked: Boolean,
val volumeId: Int,
val created: String,
val lastModifiedUtc: String,
val files: List<FileDto>? = null,
) {
val fileCount: Int
@@ -133,3 +252,37 @@ data class ChapterDto(
data class FileDto(
val id: Int,
)
@Serializable
data class ReadingListDto(
val id: Int,
val title: String,
val coverImage: String? = null,
val promoted: Boolean = false,
val summary: String?,
val itemCount: Int,
val startingYear: Int,
val startingMonth: Int,
val endingYear: Int,
val endingMonth: Int,
val ownerUserName: String?,
@SerialName("items") val items: List<ReadingListItemDto> = emptyList(),
)
@Serializable
data class ReadingListItemDto(
val id: Int,
val order: Int,
val chapterId: Int?,
val seriesId: Int,
val seriesName: String,
val chapterNumber: String?,
val volumeNumber: String?,
val chapterTitleName: String?,
val volumeId: Int?,
val title: String?,
val summary: String?,
val releaseDate: String?,
val libraryName: String?,
@SerialName("readingListId") val readingListId: Int,
)

View File

@@ -1,100 +1,35 @@
// Metadata descriptors for filters and categorization
package eu.kanade.tachiyomi.extension.all.kavita.dto
import kotlinx.serialization.Serializable
/**
* This file contains all class for filtering
* */
@Serializable
data class MetadataGenres(
val id: Int,
val title: String,
)
data class MetadataGenres(val id: Int, val title: String)
@Serializable
data class MetadataPeople(
val id: Int,
val name: String,
val role: Int,
val role: Int? = null,
)
@Serializable
data class MetadataPubStatus(
val value: Int,
val title: String,
)
data class MetadataPubStatus(val value: Int, val title: String)
@Serializable
data class MetadataTag(
val id: Int,
val title: String,
)
data class MetadataTag(val id: Int, val title: String)
@Serializable
data class MetadataAgeRatings(
val value: Int,
val title: String,
)
data class MetadataAgeRatings(val value: Int, val title: String)
@Serializable
data class MetadataLanguages(
val isoCode: String,
val title: String,
)
data class MetadataLanguages(val isoCode: String, val title: String)
@Serializable
data class MetadataLibrary(
val id: Int,
val name: String,
val type: Int,
)
data class MetadataLibrary(val id: Int, val name: String, val type: Int)
@Serializable
data class MetadataCollections(
val id: Int,
val title: String,
)
data class MetadataPayload(
val forceUseMetadataPayload: Boolean = true,
var sorting: Int = 1,
var sorting_asc: Boolean = true,
var readStatus: ArrayList<String> = arrayListOf<String>(),
val readStatusList: List<String> = listOf("notRead", "inProgress", "read"),
// _i = included, _e = excluded
var genres_i: ArrayList<Int> = arrayListOf<Int>(),
var genres_e: ArrayList<Int> = arrayListOf<Int>(),
var tags_i: ArrayList<Int> = arrayListOf<Int>(),
var tags_e: ArrayList<Int> = arrayListOf<Int>(),
var ageRating_i: ArrayList<Int> = arrayListOf<Int>(),
var ageRating_e: ArrayList<Int> = arrayListOf<Int>(),
var formats: ArrayList<Int> = arrayListOf<Int>(),
var collections_i: ArrayList<Int> = arrayListOf<Int>(),
var collections_e: ArrayList<Int> = arrayListOf<Int>(),
var userRating: Int = 0,
var people: ArrayList<Int> = arrayListOf<Int>(),
// _i = included, _e = excluded
var language_i: ArrayList<String> = arrayListOf<String>(),
var language_e: ArrayList<String> = arrayListOf<String>(),
var libraries_i: ArrayList<Int> = arrayListOf<Int>(),
var libraries_e: ArrayList<Int> = arrayListOf<Int>(),
var pubStatus: ArrayList<Int> = arrayListOf<Int>(),
var seriesNameQuery: String = "",
var releaseYearRangeMin: Int = 0,
var releaseYearRangeMax: Int = 0,
var peopleWriters: ArrayList<Int> = arrayListOf<Int>(),
var peoplePenciller: ArrayList<Int> = arrayListOf<Int>(),
var peopleInker: ArrayList<Int> = arrayListOf<Int>(),
var peoplePeoplecolorist: ArrayList<Int> = arrayListOf<Int>(),
var peopleLetterer: ArrayList<Int> = arrayListOf<Int>(),
var peopleCoverArtist: ArrayList<Int> = arrayListOf<Int>(),
var peopleEditor: ArrayList<Int> = arrayListOf<Int>(),
var peoplePublisher: ArrayList<Int> = arrayListOf<Int>(),
var peopleCharacter: ArrayList<Int> = arrayListOf<Int>(),
var peopleTranslator: ArrayList<Int> = arrayListOf<Int>(),
)
data class MetadataCollections(val id: Int, val title: String)
@Serializable
data class SmartFilter(
@@ -102,3 +37,40 @@ data class SmartFilter(
val name: String,
val filter: String,
)
@Serializable
data class MetadataPayload(
val forceUseMetadataPayload: Boolean = true,
var sorting: Int = 1,
var sorting_asc: Boolean = true,
var readStatus: ArrayList<String> = arrayListOf("notRead", "inProgress", "read"),
var genres_i: ArrayList<Int> = arrayListOf(),
var genres_e: ArrayList<Int> = arrayListOf(),
var tags_i: ArrayList<Int> = arrayListOf(),
var tags_e: ArrayList<Int> = arrayListOf(),
var ageRating_i: ArrayList<Int> = arrayListOf(),
var ageRating_e: ArrayList<Int> = arrayListOf(),
var formats: ArrayList<Int> = arrayListOf(),
var collections_i: ArrayList<Int> = arrayListOf(),
var collections_e: ArrayList<Int> = arrayListOf(),
var userRating: Int = 0,
var people: ArrayList<Int> = arrayListOf(),
var language_i: ArrayList<String> = arrayListOf(),
var language_e: ArrayList<String> = arrayListOf(),
var libraries_i: ArrayList<Int> = arrayListOf(),
var libraries_e: ArrayList<Int> = arrayListOf(),
var pubStatus: ArrayList<Int> = arrayListOf(),
var seriesNameQuery: String = "",
var releaseYearRangeMin: Int = 0,
var releaseYearRangeMax: Int = 0,
var peopleWriters: ArrayList<Int> = arrayListOf(),
var peoplePenciller: ArrayList<Int> = arrayListOf(),
var peopleInker: ArrayList<Int> = arrayListOf(),
var peopleColorist: ArrayList<Int> = arrayListOf(),
var peopleLetterer: ArrayList<Int> = arrayListOf(),
var peopleCoverArtist: ArrayList<Int> = arrayListOf(),
var peopleEditor: ArrayList<Int> = arrayListOf(),
var peoplePublisher: ArrayList<Int> = arrayListOf(),
var peopleCharacter: ArrayList<Int> = arrayListOf(),
var peopleTranslator: ArrayList<Int> = arrayListOf(),
)

View File

@@ -1,8 +1,9 @@
// Generic response and authentication models
package eu.kanade.tachiyomi.extension.all.kavita.dto
import kotlinx.serialization.Serializable
@Serializable // Used to process login
@Serializable
data class AuthenticationDto(
val username: String,
val token: String,
@@ -20,9 +21,9 @@ data class PaginationInfo(
@Serializable
data class ServerInfoDto(
val installId: String,
val os: String,
// val os: String,
val isDocker: Boolean,
val dotnetVersion: String,
// val dotnetVersion: String,
val kavitaVersion: String,
val numOfCores: Int,
// val numOfCores: Int,
)