Merge pull request #23 from dear-clouds/master

1.4.21
This commit is contained in:
Joe Milazzo
2025-07-15 07:51:42 -05:00
committed by GitHub
18 changed files with 1505 additions and 1397 deletions

View File

@@ -3,26 +3,6 @@ description: Report a source issue in Tachiyomi
labels: [Bug]
body:
- type: input
id: source
attributes:
label: Source information
description: |
You can find the extension name and version in **Browse → Extensions**.
placeholder: |
Example: "Mangahere 1.3.18"
validations:
required: true
- type: input
id: language
attributes:
label: Source language
placeholder: |
Example: "English"
validations:
required: true
- type: textarea
id: reproduce-steps
attributes:
@@ -59,14 +39,25 @@ body:
- type: input
id: tachiyomi-version
attributes:
label: Tachiyomi version
label: Mihon/Komikku version
description: |
You can find your Tachiyomi version in **More → About**.
You can find your Mihon/Komikku version in **More → About**.
placeholder: |
Example: "0.14.7"
validations:
required: true
- type: input
id: kavita-version
attributes:
label: Kavita version
description: |
You can find your Kavita version in **Settings → System**.
placeholder: |
Example: "0.8.7.2"
validations:
required: true
- type: input
id: android-version
attributes:
@@ -95,13 +86,11 @@ body:
required: true
- label: I have written a short but informative title.
required: true
- label: I have updated the app to version **[0.14.7](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
- label: I have updated Mihon/Komikku to last version.
required: true
- label: I have updated all installed extensions.
- label: I have updated the extension to last version.
required: true
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
required: true
- label: If this is an issue with the app itself, I should be opening an issue in the [app repository](https://github.com/tachiyomiorg/tachiyomi/issues/new/choose).
- label: If this is an issue with the app itself, I should be opening an issue in the [app repository](https://github.com/mihonapp/mihon/issues/new/choose).
required: true
- label: I will fill out all of the requested information in this form.
required: true

View File

@@ -3,26 +3,6 @@ description: Suggest a feature to improve an existing source
labels: [Feature request]
body:
- type: input
id: source
attributes:
label: Source name
description: |
You can find the extension name in **Browse → Extensions**.
placeholder: |
Example: "Mangahere"
validations:
required: true
- type: input
id: language
attributes:
label: Source language
placeholder: |
Example: "English"
validations:
required: true
- type: textarea
id: feature-description
attributes:
@@ -51,9 +31,7 @@ body:
required: true
- label: I have written a short but informative title.
required: true
- label: If this is an issue with the app itself, I should be opening an issue in the [app repository](https://github.com/tachiyomiorg/tachiyomi/issues/new/choose).
required: true
- label: I have updated the app to version **[0.14.7](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
- label: If this is an issue with the app itself, I should be opening an issue in the [app repository](https://github.com/mihonapp/mihon/issues/new/choose).
required: true
- label: I will fill out all of the requested information in this form.
required: true

View File

@@ -1,59 +0,0 @@
name: 🌐 Source request
description: Suggest a new source for Tachiyomi
labels: [Source request]
body:
- type: input
id: name
attributes:
label: Source name
placeholder: |
Example: "Not Real Scans"
validations:
required: true
- type: input
id: link
attributes:
label: Source link
placeholder: |
Example: "https://notrealscans.org"
validations:
required: true
- type: input
id: language
attributes:
label: Source language
placeholder: |
Example: "English"
validations:
required: true
- type: textarea
id: other-details
attributes:
label: Other details
placeholder: |
Additional details and attachments.
Example:
"18+/NSFW = yes"
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
description: Your issue will be closed if you haven't done these steps.
options:
- label: I have checked that the extension does not already exist on the [website extensions list](https://tachiyomi.org/extensions/) or the app.
required: true
- label: I have checked that the extension is not on [the removed sources list](https://github.com/tachiyomiorg/tachiyomi-extensions/blob/master/REMOVED_SOURCES.md).
required: true
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue.
required: true
- label: I have checked that the extension does not already exist by searching the [GitHub repository](https://github.com/tachiyomiorg/tachiyomi-extensions/) and verified it does not appear in the code base.
required:
- label: I have written a meaningful title with the source name.
required: true
- label: I will fill out all of the requested information in this form.
required: true

View File

@@ -1,57 +0,0 @@
name: 🔗 URL change report
description: Report URL change of an existing source
labels: [Bug,Domain changed]
body:
- type: input
id: source
attributes:
label: Source information
description: |
You can find the extension name and version in **Browse → Extensions**.
placeholder: |
Example: "NotRealScans 1.3.1"
validations:
required: true
- type: input
id: language
attributes:
label: Source language
placeholder: |
Example: "English"
validations:
required: true
- type: input
id: link
attributes:
label: Source new URL
placeholder: |
Example: "https://notrealscans.org"
validations:
required: true
- type: textarea
id: other-details
attributes:
label: Other details
placeholder: |
Additional details and attachments.
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
description: Your issue will be closed if you haven't done these steps.
options:
- label: I have updated all installed extensions.
required: true
- label: I have opened WebView and checked that the source URL is not updated yet.
required: true
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue.
required: true
- label: I have written a short but informative title.
required: true
- label: I will fill out all of the requested information in this form.
required: true

View File

@@ -31,11 +31,7 @@ body:
required: true
- label: I have written a short but informative title.
required: true
- label: If this is an issue with the app itself, I should be opening an issue in the [app repository](https://github.com/tachiyomiorg/tachiyomi/issues/new/choose).
required: true
- label: I have updated the app to version **[0.14.7](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
required: true
- label: I have updated all installed extensions.
- label: If this is an issue with the app itself, I should be opening an issue in the [app repository](https://github.com/mihonapp/mihon/issues/new/choose).
required: true
- label: I will fill out all of the requested information in this form.
required: true

View File

@@ -1,63 +0,0 @@
name: ❌ Dead source report
description: Source is down and website is closed
labels: [Source is down]
body:
- type: markdown
attributes:
value: |
### Notice
If you have a lot of dead sources to report, please go back and submit a single meta request.
- type: input
id: source
attributes:
label: Source name
description: |
You can find the extension name in **Browse → Extensions**.
placeholder: |
Example: "NotRealScans"
validations:
required: true
- type: input
id: language
attributes:
label: Source language
placeholder: |
Example: "English"
validations:
required: true
- type: input
id: link
attributes:
label: Source link
placeholder: |
Example: "https://notrealscans.org"
validations:
required: true
- type: textarea
id: other-details
attributes:
label: Other details
placeholder: |
Additional details and attachments.
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
description: Your issue will be closed if you haven't done these steps.
options:
- label: I have updated all installed extensions.
required: true
- label: I have opened WebView and checked that the source website is down.
required: true
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue.
required: true
- label: I have written a meaningful title with the source name.
required: true
- label: I will fill out all of the requested information in this form.
required: true

View File

@@ -1,33 +0,0 @@
name: 🗑 Source removal request
description: Scanlators can request their site to be removed
labels: [Meta request]
body:
- type: input
id: link
attributes:
label: Source link
placeholder: |
Example: "https://notrealscans.org"
validations:
required: true
- type: textarea
id: other-details
attributes:
label: Other details
placeholder: |
Additional details and attachments.
- type: checkboxes
id: requirements
attributes:
label: Requirements
description: Your request will be denied if you don't meet these requirements.
options:
- label: I've added a `TXT` DNS record for my domain with the value `tachiyomi-verification`
required: true
- label: Site only hosts content scanlated by the group and not stolen from other scanlators or official releases (i.e., not an aggregator site)
required: true
- label: Site is not infested with user-hostile features (e.g., invasive or malicious ads)
required: true

View File

@@ -2,7 +2,7 @@
* Add or remove modules to load as needed for local development here.
*/
loadAllIndividualExtensions()
// loadIndividualExtension("all", "mangadex")
loadIndividualExtension("all", "kavita")
/**
* ===================================== COMMON CONFIGURATION ======================================

View File

@@ -1,32 +1,107 @@
login_errors_failed_login=Login failed. Something went wrong
login_errors_header_token_empty="Error: The JSON Web Token is empty.\nTry opening the extension first."
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
# ================ #
# Authentication #
# ================ #
login_errors_failed_login=Authentication failed.\nPlease check your credentials
login_errors_header_token_empty=Authorization token missing\nPlease open the extension first to generate a token
login_errors_invalid_url=Invalid server URL:
login_errors_parse_tokendto=Failed to process authentication token
http_errors_401=Authentication expired\nPlease log in again
http_errors_500=Server error occurred
# ============== #
# Preferences #
# ============== #
pref_customsource_title=Source Display Name
pref_edit_customsource_summary=Customize how this Kavita instance appears in your library\nExample: "Home Server" or "Cloud Instance"
pref_group_tags_title=Tag Grouping (fork must support grouping)
pref_group_tags_summary_on=Show tags with category prefixes
pref_group_tags_summary_off=Display tags as flat list
pref_last_volume_cover_title=Keep Cover Updated
pref_last_volume_cover_summary_off=Keep series cover
pref_last_volume_cover_summary_on=Always show current Volume/Issue cover on refresh
pref_show_epub_title=EPUB Visibility
pref_show_epub_summary_off=Hide EPUB entries
pref_show_epub_summary_on=Show EPUB entries (unreadable in Mihon)
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
pref_opds_badformed_url=Incorrect OPDS address. Please copy it from User settings \u2192 3rd party apps \u2192 OPDS URL
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 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.
http_errors_500=Something went wrong
http_errors_401=There was an error logging in. Try again or reload the app
pref_rd_date_summary_off=Hide publication dates
pref_rd_date_summary_on=Show publication dates\nNote: Once you toggle it on, if you toggle it off again, it will stays as N/A.
pref_reset_title=Reset Settings
pref_reset_summary=Clear all preferences for this Kavita instance
pref_filters_title=Default Filters
pref_filters_summary=Select which filters appear by default
# ============== #
# OPDS Setup #
# ============== #
pref_opds_must_setup_address=OPDS URL required for connection
pref_opds_badformed_url=Invalid OPDS address\nCopy from: User Settings > API Key / OPDS > OPDS URL (settings#clients)
pref_opds_duplicated_source_url=URL already used in another source:
pref_opds_summary=Complete OPDS URL including API key\nFormat: http://address:port/opds?apiKey=XXX
# ============== #
# System Messages #
# ============== #
restartapp_settings=Restart Mihon to apply changes
check_version=Requires Kavita v0.8.7+\nIf issues persist, contact support with logs
version_exceptions_chapters_parse=Chapter parsing error\nPlease report with logs
version_exceptions_smart_filter=Smart Filter error\nRequires Kavita v0.7.11+
# ============== #
# Filters #
# ============== #
# Filter headers
filters_special=These filters can't be combined with other filters
filters_people=People Filters
filters_special_list=Special Lists
filters_smart_filters=Smart Filters
# @todo
# Basic filters
filters_user_rating=User Rating
filters_status=Status
filters_sort=Sort By
filters_release_year=Release Year
filters_genres=Genres
filters_tags=Tags
filters_age_ratings=Age Ratings
filters_collections=Collections
filters_languages=Languages
filters_libraries=Libraries
filters_pub_status=Publication Status
# People filters
filters_writers=Writers
filters_pencillers=Pencillers
filters_inkers=Inkers
filters_colorists=Colorists
filters_letterers=Letterers
filters_cover_artists=Cover Artists
filters_editors=Editors
filters_publishers=Publishers
filters_characters=Characters
filters_translators=Translators
# Status options
filters_status_any=Any
filters_status_unread=Unread
filters_status_in_progress=In Progress
filters_status_read=Read
# Sort options
filters_sort_title=Title
filters_sort_rating=Rating
filters_sort_date_added=Date Added
filters_sort_release_date=Release Date
filters_sort_read_progress=Read Progress
# Error messages
filters_error_loading=Failed to load filters
filters_empty_list=No filters available

View File

@@ -1,18 +1,107 @@
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.
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 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 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 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 relancez l'application
# ================ #
# Authentification #
# ================ #
login_errors_failed_login=Échec de l'authentification. Vérifiez vos identifiants
login_errors_header_token_empty=Token d'autorisation manquant\nOuvrez d'abord l'extension pour générer un token
login_errors_invalid_url=URL du serveur invalide :
login_errors_parse_tokendto=Échec du traitement du token d'authentification
http_errors_401=Authentification expirée. Veuillez vous reconnecter
http_errors_500=Erreur du serveur
# ============== #
# Préférences #
# ============== #
pref_customsource_title=Nom d'affichage de la source
pref_edit_customsource_summary=Personnalisez l'affichage de cette instance Kavita\nExemple : "Serveur Maison" ou "Instance Cloud"
pref_group_tags_title=Groupement des tags (fork doit supporter le groupement)
pref_group_tags_summary_on=Afficher les tags avec préfixes
pref_group_tags_summary_off=Afficher les tags en liste plate
pref_last_volume_cover_title=Garder la couverture à jour
pref_last_volume_cover_summary_off=Conserver la couverture de la série
pref_last_volume_cover_summary_on=Toujours afficher la dernière couverture
pref_show_epub_title=Visibilité EPUB
pref_show_epub_summary_off=Masquer les EPUB
pref_show_epub_summary_on=Afficher les EPUB (illisibles dans Mihon)
pref_rd_date_title=Listes de Lecture: Affichage date de publication
pref_rd_date_summary_off=Masquer les dates de publication
pref_rd_date_summary_on=Afficher les dates de publication\nNote : Désactiver affichera toujours "N/A" après activation
pref_reset_title=Réinitialisation
pref_reset_summary=Effacer toutes les préférences pour cette instance Kavita
pref_filters_title=Filtres par défaut
pref_filters_summary=Sélectionnez les filtres visibles par défaut
# ============== #
# Configuration OPDS #
# ============== #
pref_opds_must_setup_address=URL OPDS requise pour la connexion
pref_opds_badformed_url=URL OPDS invalide\nCopiez depuis : Paramètres > Clé API / OPDS > URL OPDS
pref_opds_duplicated_source_url=URL déjà utilisée dans une autre source :
pref_opds_summary=URL OPDS complète incluant la clé API\nFormat : http://adresse:port/opds?apiKey=XXX
# ============== #
# Messages système #
# ============== #
restartapp_settings=Redémarrez Mihon pour appliquer les changements
check_version=Nécessite Kavita v0.8.7+\nSi problème persiste, contactez le support avec les logs
version_exceptions_chapters_parse=Erreur d'analyse des chapitres\nMerci de rapporter avec les logs
version_exceptions_smart_filter=Erreur Smart Filter\nNécessite Kavita v0.7.11+
# ============== #
# Filtres #
# ============== #
# Filter headers
filters_special=Ces filtres ne peuvent pas être combinés avec d'autres filtres
filters_people=Filtres Personnalités
# Special filters
filters_special_list=Listes Spéciales
filters_smart_filters=Filtres Intelligents
# Basic filters
filters_user_rating=Note Utilisateur
filters_status=Statut
filters_sort=Trier Par
filters_release_year=Année de Sortie
filters_genres=Genres
filters_tags=Étiquettes
filters_age_ratings=Classifications d'Âge
filters_collections=Collections
filters_languages=Langues
filters_libraries=Bibliothèques
filters_pub_status=Statut de Publication
# People filters
filters_writers=Auteur
filters_pencillers=Dessinateurs
filters_inkers=Encreurs
filters_colorists=Coloristes
filters_letterers=Lettrists
filters_cover_artists=Artistes de Couverture
filters_editors=Éditeurs
filters_publishers=Éditeurs
filters_characters=Personnalités
filters_translators=Traducteurs
# Status options
filters_status_any=Tous
filters_status_unread=Non Lu
filters_status_in_progress=En Cours
filters_status_read=Lu
# Sort options
filters_sort_title=Titre
filters_sort_rating=Note
filters_sort_date_added=Date d'Ajout
filters_sort_release_date=Date de Sortie
filters_sort_read_progress=Progrès de Lecture
# Error messages
filters_error_loading=Échec du chargement des filtres
filters_empty_list=Aucun filtre disponible

View File

@@ -2,7 +2,7 @@ ext {
kmkVersionCode = 1
extName = 'Kavita'
extClass = '.KavitaFactory'
extVersionCode = 19
extVersionCode = 20
isNsfw = false
}

View File

@@ -3,25 +3,6 @@ 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<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))
@@ -38,15 +19,22 @@ val sortableList = listOf(
Pair("Random", 9),
)
class SpecialListFilter : Filter.Select<String>(
"Special Lists",
class SpecialListFilter(name: String = "Special Lists") : Filter.Select<String>(
name,
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 StatusFilter : Filter.Select<String>(
"Status",
arrayOf("Any", "Unread", "In Progress", "Read"),
)
class StatusSeparator :
Filter.Separator()
class StatusFilterGroup(name: String = "Status") : Filter.Group<StatusFilter>(
name,
listOf(StatusFilter()),
)
// Use Filter.Text directly for release year range
class ReleaseYearRange(name: String) : Filter.Text(name, "")
@@ -59,7 +47,8 @@ class GenreFilterGroup(genres: List<GenreFilter>) :
Filter.Group<GenreFilter>("Genres", genres)
class TagFilter(name: String) : Filter.TriState(name)
class TagFilterGroup(tags: List<TagFilter>) : Filter.Group<TagFilter>("Tags", tags)
class TagFilterGroup(tags: List<TagFilter>) :
Filter.Group<TagFilter>("Tags", tags)
class AgeRatingFilter(name: String) : Filter.TriState(name)
class AgeRatingFilterGroup(ageRatings: List<AgeRatingFilter>) :
@@ -85,8 +74,29 @@ class PubStatusFilter(name: String) : Filter.CheckBox(name, false)
class PubStatusFilterGroup(status: List<PubStatusFilter>) :
Filter.Group<PubStatusFilter>("Publication Status", status)
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 UserRatingSeparator :
Filter.Separator()
class PeopleHeaderFilter(name: String) :
Filter.Header(name)
Filter.Header(name.toString())
class PeopleSeparatorFilter :
Filter.Separator()

View File

@@ -2,6 +2,11 @@ package eu.kanade.tachiyomi.extension.all.kavita
object KavitaConstants {
const val UNNUMBERED_VOLUME = -100000
const val UNNUMBERED_VOLUME_STR = "-100000"
const val SPECIAL_NUMBER = 100_000
const val VOLUME_NUMBER_OFFSET = 10000f
val PERSON_ROLES = listOf(
"Writer", "Penciller", "Inker", "Colorist",
"Letterer", "CoverArtist", "Editor",
@@ -23,18 +28,7 @@ object KavitaConstants {
"Publication Status",
"Rating",
"Age Rating",
"Writers",
"Penciller",
"Inker",
"Colorist",
"Letterer",
"Cover Artist",
"Editor",
"Publisher",
"Character",
"Translators",
"ReleaseYearRange",
"Average Rating",
"Random",
"Read Progress",
)
@@ -51,18 +45,7 @@ object KavitaConstants {
"Publication Status",
"Rating",
"Age Rating",
"Writers",
"Penciller",
"Inker",
"Colorist",
"Letterer",
"CoverArtist",
"Editor",
"Publisher",
"Character",
"Translators",
"ReleaseYearRange",
"Average Rating",
"Random",
"Read Progress",
)
@@ -79,18 +62,7 @@ object KavitaConstants {
"Publication Status",
"Rating",
"Age Rating",
"Writers",
"Penciller",
"Inker",
"Colorist",
"Letterer",
"CoverArtist",
"Editor",
"Publisher",
"Character",
"Translators",
"ReleaseYearRange",
"Average Rating",
"Random",
"Read Progress",
)

View File

@@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.extension.all.kavita
import android.util.Log
import android.annotation.SuppressLint
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
@@ -13,170 +13,204 @@ import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Response
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
class KavitaHelper {
val json = Json {
isLenient = true
ignoreUnknownKeys = true
coerceInputValues = true
explicitNulls = false
allowSpecialFloatingPointValues = true
useArrayPolymorphism = true
prettyPrint = true
}
inline fun <reified T : Enum<T>> safeValueOf(type: String): T {
return java.lang.Enum.valueOf(T::class.java, type)
}
val dateFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSS", Locale.US)
.apply { timeZone = TimeZone.getTimeZone("UTC") }
fun parseDate(dateAsString: String): Long =
dateFormatter.parse(dateAsString)?.time ?: 0
fun hasNextPage(response: Response): Boolean {
val paginationHeader = response.header("Pagination")
var hasNextPage = false
if (!paginationHeader.isNullOrEmpty()) {
val paginationInfo = json.decodeFromString<PaginationInfo>(paginationHeader)
hasNextPage = paginationInfo.currentPage + 1 > paginationInfo.totalPages
hasNextPage = paginationInfo.currentPage + 1 < paginationInfo.totalPages
}
return !hasNextPage
return hasNextPage
}
fun getIdFromUrl(url: String): Int {
return try {
val id = url.substringAfterLast("/").toInt()
Log.d("KavitaHelper", "Extracted ID $id from URL: $url")
// Log.d("KavitaHelper", "Extracted ID $id from URL: $url")
id
} catch (e: Exception) {
Log.e("KavitaHelper", "Failed to extract ID from URL: $url", e)
// Log.e("KavitaHelper", "Failed to extract ID from URL: $url", e)
-1
}
}
// Rating Providers from Series-Details-Plus
private fun getProviderName(provider: Int): String = when (provider) {
0 -> "User"
1 -> "AniList"
2 -> "MyAnimeList"
3 -> "MangaUpdates"
else -> "Rating"
}
// @todo 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 =
fun createSeriesDto(obj: SeriesDto, baseUrl: String, apiKey: String): SManga =
SManga.create().apply {
// url = "$baseUrl/library/${obj.libraryId}/series/${obj.id}"
url = "$baseUrl/Series/${obj.id}"
title = obj.name
thumbnail_url = "$baseUrl/image/series-cover?seriesId=${obj.id}&apiKey=$apiKey"
// Set status based on read progress
status = when {
obj.pagesRead >= obj.pages -> SManga.COMPLETED
obj.pagesRead > 0 -> SManga.ONGOING
else -> SManga.UNKNOWN
}
}
// 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 =
fun chapterFromVolume(
chapter: ChapterDto,
volume: VolumeDto,
singleFileVolumeNumber: Double? = null,
libraryType: LibraryTypeEnum? = null,
isWebtoon: Boolean = false,
mangaTitle: String = "",
): SChapter =
SChapter.create().apply {
val type = ChapterType.of(chapter, volume, libraryType)
val titleName = chapter.titleName
val title = chapter.title
val range = chapter.range
// For webtoon
val chapterLabel = if (isWebtoon) "Episode" else "Chapter"
val volumeLabel = if (isWebtoon) "Season" else "Volume"
if (titleName != null) {
name = when (type) {
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
val chapterNum = formatChapterNumber(chapter)
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}"
titleName.isNotBlank() && titleName.any { it.isLetter() } ->
"$chapterNum - ${cleanChapterTitle(
titleName,
ChapterTitleContext(
mangaTitle = mangaTitle,
chapterNumber = chapterNum,
volumeName = volume.name.ifBlank { title },
),
)}"
else ->
"$volumeLabel ${formatVolumeNumber(volume)} $chapterLabel $chapterNum"
}
}
ChapterType.SingleFileVolume -> {
val volumeNumber = formatVolumeNumber(volume)
val cleanVolumeName = volume.name.trim()
// Always use the API's volume number, never parse from title
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}"
// If name is empty or just numbers (Kavita default)
cleanVolumeName.isEmpty() || cleanVolumeName.none { it.isLetter() } -> {
cleanChapterTitle("Volume $volumeNumber")
}
// If name already contains volume info (Vol. 3, V3, etc.)
cleanVolumeName.contains(Regex("(?i)(vol|volume|v)[.\\s]*\\d+")) -> {
cleanChapterTitle(cleanVolumeName)
}
// Normal case - prefix with volume number
else -> {
cleanChapterTitle("$volumeNumber - $cleanVolumeName")
}
}
ChapterType.Special -> chapter.titleName.takeIf { it.isNotBlank() } ?: chapter.range
}
ChapterType.Special -> {
cleanChapterTitle(
when {
title.isNotBlank() -> title
range.isNotBlank() -> range
else -> "Special"
},
ChapterTitleContext(
mangaTitle = mangaTitle,
volumeName = volume.name,
),
)
}
ChapterType.Chapter -> {
val chapterNum = chapter.number.toIntOrNull()?.let {
it.toString().padStart(2, '0')
} ?: chapter.number
val chapterNum = formatChapterNumber(chapter)
when {
chapter.titleName.isBlank() -> "Chapter $chapterNum"
chapter.titleName.trim().matches(Regex("^\\d+$")) -> "Chapter ${chapter.titleName.trim().padStart(2, '0')}"
else -> "$chapterNum - ${chapter.titleName}"
titleName.isNotBlank() && titleName.any { it.isLetter() } ->
"$chapterNum - ${cleanChapterTitle(
titleName,
ChapterTitleContext(
mangaTitle = mangaTitle,
chapterNumber = chapterNum,
volumeName = volume.name,
),
)}"
else ->
"$chapterLabel $chapterNum"
}
}
ChapterType.Issue -> {
val issueNum = chapter.number.toIntOrNull()?.let {
it.toString().padStart(3, '0')
} ?: chapter.number
val issueNum = chapter.number.toIntOrNull()?.toString()?.padStart(3, '0') ?: chapter.number
cleanChapterTitle(
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}"
titleName.any { it.isLetter() } -> "$titleName (#$issueNum)"
else -> "Issue #$issueNum"
},
ChapterTitleContext(
mangaTitle = mangaTitle,
chapterNumber = issueNum,
volumeName = volume.name,
),
)
}
}
}
// chapter_number = try {
// // if (type == ChapterType.SingleFileVolume) {
// // volume.number.toFloat() / 10000
// // } else {
// // chapter.number.toFloatOrNull() ?: 0f
// // }
// // } catch (e: NumberFormatException) {
// // 0f
// // }
// Handle decimal chapter numbers properly
chapter_number = when {
type == ChapterType.SingleFileVolume && singleFileVolumeNumber != null -> singleFileVolumeNumber.toFloat()
type != ChapterType.SingleFileVolume -> chapter.number.toFloatOrNull() ?: 0f
else -> 0f
type == ChapterType.SingleFileVolume && singleFileVolumeNumber != null ->
volume.minNumber.toFloat()
type == ChapterType.SingleFileVolume -> {
// For standalone volumes, use positive numbers
volume.minNumber.toFloat()
}
// For regular chapters
type != ChapterType.SingleFileVolume && type != ChapterType.Special -> {
// Handle decimal chapter numbers
if (chapter.minNumber % 1 != 0.0) {
chapter.minNumber.toFloat()
} else {
chapter.minNumber.toInt().toFloat()
}
}
// For volumes/specials in mixed content, place them below chapter 0
else -> {
val volumeNum = try {
if (volume.minNumber % 1 != 0.0) {
volume.minNumber.toFloat()
} else {
volume.minNumber.toInt().toFloat()
}
} catch (e: NumberFormatException) {
0f
}
volumeNum / KavitaConstants.VOLUME_NUMBER_OFFSET
}
}
url = when {
type == ChapterType.SingleFileVolume && singleFileVolumeNumber != null ->
"volume_${volume.id}"
else -> chapter.id.toString()
else -> "chapter_${chapter.id}"
}
// url = chapter.id.toString()
if (chapter.fileCount > 1) {
// salt/offset to recognize chapters with new merged part-chapters as new and hence unread
chapter_number += 0.001f * chapter.fileCount
@@ -185,11 +219,86 @@ class KavitaHelper {
date_upload = parseDateSafe(chapter.created)
scanlator = when (type) {
ChapterType.SingleFileVolume -> "Volume"
ChapterType.SingleFileVolume -> if (isWebtoon) "Season" else "Volume"
ChapterType.Special -> "Special"
ChapterType.Issue -> "Issue"
ChapterType.Chapter -> "Chapter"
ChapterType.Regular -> "Chapter"
ChapterType.Chapter -> if (isWebtoon) "Episode" else "Chapter"
ChapterType.Regular -> if (isWebtoon) "Episode" else "Chapter"
}
}
private fun formatVolumeNumber(volume: VolumeDto): String {
return when {
volume.maxNumber > volume.minNumber ->
"${removeTrailingZero(volume.minNumber)}-${removeTrailingZero(volume.maxNumber)}"
else -> removeTrailingZero(volume.minNumber)
}
}
private fun formatChapterNumber(chapter: ChapterDto, padLength: Int = 2): String {
val chapterNum = when {
chapter.maxNumber > chapter.minNumber ->
"${removeTrailingZero(chapter.minNumber)}-${removeTrailingZero(chapter.maxNumber)}"
else -> removeTrailingZero(chapter.minNumber)
}
// Only pad if it's a single whole number (not a range and not decimal)
return if (!chapterNum.contains('-') && !chapterNum.contains('.')) {
chapterNum.toIntOrNull()?.toString()?.padStart(padLength, '0') ?: chapterNum
} else {
chapterNum
}
}
// Helper function to remove .0 from whole numbers while preserving actual decimals
@SuppressLint("DefaultLocale")
private fun removeTrailingZero(number: Double): String {
return if (number % 1 == 0.0) {
number.toInt().toString()
} else {
// Use String.format to avoid scientific notation for very small decimals
String.format("%.10f", number).replace(",", ".").trimEnd('0').trimEnd('.')
}
}
private fun cleanChapterTitle(
originalTitle: String,
context: ChapterTitleContext? = null,
): String {
var title = originalTitle.trim()
val mangaTitle = context?.mangaTitle ?: ""
// Remove manga name if present
if (mangaTitle.isNotBlank()) {
title = title.replace(Regex("(?i)\\b${Regex.escape(mangaTitle)}\\b[\\s\\-:]*"), "").trim()
}
// Remove chapter number patterns like c0116, c001, etc.
title = title.replace(Regex("""(?i)\bc\d+\b"""), "").trim()
// Clean up problematic hyphen/space patterns
title = title.replace(Regex("""\s*[-.]\s*[-.]\s*"""), " - ") // Handles -.-, - . -, etc.
.replace(Regex("""\s*-\s*-\s*"""), " - ") // Double hyphens with spaces
.replace(Regex("""^\s*[-.]\s*|\s*[-.]\s*$"""), "") // Leading/trailing hyphens or dots
.trim()
return title.ifBlank { originalTitle }
}
/**
* Data class to hold context information for cleaning chapter titles
*/
data class ChapterTitleContext(
val mangaTitle: String = "",
val chapterNumber: String = "",
val volumeName: String = "",
val isWebtoon: Boolean = false,
)
fun isWebtoonOrLongStrip(tags: List<String>): Boolean {
return tags.any {
val normalized = it.trim().lowercase()
normalized.contains("webtoon") || normalized.contains("long strip")
}
}

View File

@@ -10,11 +10,6 @@ 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 {
@@ -80,7 +75,7 @@ fun PreferenceScreen.addEditTextPreference(
val result = text.isBlank() || validate?.invoke(text) ?: true
if (restartRequired && result) {
Toast.makeText(context, "Restart Tachiyomi to apply new setting.", Toast.LENGTH_LONG).show()
Toast.makeText(context, "Restart Mihon to apply new setting.", Toast.LENGTH_LONG).show()
}
result

View File

@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.extension.all.kavita.dto
import eu.kanade.tachiyomi.extension.all.kavita.KavitaConstants
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@@ -8,7 +9,7 @@ interface ConvertibleToSManga {
fun toSManga(baseUrl: String, apiUrl: String, apiKey: String): SManga
}
@Serializable
@Serializable // https://github.com/Kareadita/Kavita/blob/develop/API/Entities/Enums/MangaFormat.cs
enum class MangaFormat(val format: Int) {
Image(0),
Archive(1),
@@ -19,7 +20,6 @@ enum class MangaFormat(val format: Int) {
companion object {
private val map = values().associateBy(MangaFormat::format)
fun fromInt(type: Int): MangaFormat? = map[type]
}
}
@@ -189,8 +189,9 @@ data class AuthorDto(
@Serializable
data class VolumeDto(
val id: Int,
val number: Int,
val name: String,
val minNumber: Double,
val maxNumber: Double,
val name: String = "",
val pages: Int,
val pagesRead: Int,
val lastModified: String,
@@ -202,27 +203,34 @@ data class VolumeDto(
@Serializable
enum class ChapterType {
Regular, // chapter with volume information
Chapter, // manga chapter without volume information
Regular, // Chapter with volume information
Chapter, // Chapter without volume information
SingleFileVolume,
Special,
Issue, // For comics
;
companion object {
private const val UNNUMBERED_VOLUME_NUMBER = -100_000
fun of(chapter: ChapterDto, volume: VolumeDto, libraryType: LibraryTypeEnum? = null): ChapterType =
when {
volume.number == 100_000 -> Special
volume.number == -100_000 -> when (libraryType) {
// Special
volume.minNumber.toInt() == KavitaConstants.SPECIAL_NUMBER ||
chapter.minNumber.toInt() == KavitaConstants.SPECIAL_NUMBER -> Special
// Issue
volume.minNumber.toInt() == UNNUMBERED_VOLUME_NUMBER -> when (libraryType) {
LibraryTypeEnum.Comic, LibraryTypeEnum.ComicVine -> Issue
LibraryTypeEnum.Manga, LibraryTypeEnum.LightNovel, LibraryTypeEnum.Book -> Chapter
else -> Chapter // Default to Chapter for other types
else -> Chapter
}
chapter.number == "-100000" -> SingleFileVolume
// SingleFileVolume
chapter.number == KavitaConstants.UNNUMBERED_VOLUME_STR -> SingleFileVolume
// Regular
volume.minNumber > 0 -> Regular
// Everything else depends on library type
else -> when (libraryType) {
LibraryTypeEnum.Comic, LibraryTypeEnum.ComicVine -> Issue
LibraryTypeEnum.Manga, LibraryTypeEnum.LightNovel, LibraryTypeEnum.Book -> Chapter
else -> Regular
else -> Chapter
}
}
}
@@ -233,12 +241,15 @@ data class ChapterDto(
val id: Int,
val range: String,
val number: String,
val minNumber: Double,
val maxNumber: Double,
val pages: Int,
val isSpecial: Boolean,
val title: String,
val titleName: String,
val titleName: String?,
val pagesRead: Int,
val coverImageLocked: Boolean,
val coverImage: String,
val volumeId: Int,
val created: String,
val lastModifiedUtc: String,
@@ -259,13 +270,13 @@ data class ReadingListDto(
val title: String,
val coverImage: String? = null,
val promoted: Boolean = false,
val summary: String?,
val summary: String? = null,
val itemCount: Int,
val startingYear: Int,
val startingMonth: Int,
val endingYear: Int,
val endingMonth: Int,
val ownerUserName: String?,
val ownerUserName: String? = null,
@SerialName("items") val items: List<ReadingListItemDto> = emptyList(),
)
@@ -278,11 +289,11 @@ data class ReadingListItemDto(
val seriesName: String,
val chapterNumber: String?,
val volumeNumber: String?,
val chapterTitleName: String?,
val chapterTitleName: String? = null,
val volumeId: Int?,
val title: String?,
val summary: String?,
val releaseDate: String?,
val title: String? = null,
val summary: String? = null,
val releaseDate: String? = null,
val libraryName: String?,
@SerialName("readingListId") val readingListId: Int,
)

View File

@@ -22,6 +22,9 @@ data class MetadataTag(val id: Int, val title: String)
@Serializable
data class MetadataAgeRatings(val value: Int, val title: String)
@Serializable
data class MetadataUserRatings(val value: Int, val title: String)
@Serializable
data class MetadataLanguages(val isoCode: String, val title: String)
@@ -37,40 +40,3 @@ 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(),
)