Optimizations/Upgrades from the old code
This commit is contained in:
@@ -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
|
||||
# ================ #
|
||||
# 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=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_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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,8 +19,8 @@ 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"),
|
||||
)
|
||||
|
||||
@@ -47,9 +28,11 @@ class StatusFilter : Filter.Select<String>(
|
||||
"Status",
|
||||
arrayOf("Any", "Unread", "In Progress", "Read"),
|
||||
)
|
||||
class StatusSeparator :
|
||||
Filter.Separator()
|
||||
|
||||
class StatusFilterGroup : Filter.Group<StatusFilter>(
|
||||
"Status",
|
||||
class StatusFilterGroup(name: String = "Status") : Filter.Group<StatusFilter>(
|
||||
name,
|
||||
listOf(StatusFilter()),
|
||||
)
|
||||
|
||||
@@ -64,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>) :
|
||||
@@ -90,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()
|
||||
|
||||
|
||||
@@ -43,7 +43,6 @@ import eu.kanade.tachiyomi.extension.all.kavita.dto.SortOptions
|
||||
import eu.kanade.tachiyomi.extension.all.kavita.dto.VolumeDto
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.UnmeteredSource
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
@@ -55,6 +54,14 @@ 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.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
@@ -68,16 +75,11 @@ import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
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.io.IOException
|
||||
import java.net.ConnectException
|
||||
import java.security.MessageDigest
|
||||
import java.util.Locale
|
||||
|
||||
class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSource, HttpSource() {
|
||||
private val helper = KavitaHelper()
|
||||
@@ -193,65 +195,40 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
||||
* Custom implementation for fetch popular, latest and search
|
||||
* Handles and logs errors to provide a more detailed exception to the users.
|
||||
*/
|
||||
private fun fetch(request: Request): Observable<MangasPage> {
|
||||
return client.newCall(request)
|
||||
.asObservableSuccess()
|
||||
.onErrorResumeNext { throwable ->
|
||||
val field = throwable.javaClass.getDeclaredField("code")
|
||||
field.isAccessible = true
|
||||
try {
|
||||
var code = field.get(throwable)
|
||||
Log.e(LOG_TAG, "Error fetching manga: ${throwable.message}", throwable)
|
||||
if (code as Int !in intArrayOf(401, 201, 500)) {
|
||||
code = 500
|
||||
}
|
||||
return@onErrorResumeNext Observable.error(IOException("Http Error: $code\n ${helper.intl["http_errors_$code"]}\n${helper.intl["check_version"]}"))
|
||||
} catch (e: Exception) {
|
||||
Log.e(LOG_TAG, e.toString(), e)
|
||||
return@onErrorResumeNext Observable.error(e)
|
||||
}
|
||||
}
|
||||
.map { response ->
|
||||
popularMangaParse(response)
|
||||
private suspend fun fetch(request: Request): MangasPage = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val response = client.newCall(request).execute()
|
||||
if (!response.isSuccessful) {
|
||||
val code = response.code
|
||||
throw IOException("Http Error: $code\n ${helper.intl["http_errors_$code"]}\n${helper.intl["check_version"]}")
|
||||
}
|
||||
popularMangaParse(response)
|
||||
} catch (e: Exception) {
|
||||
Log.e(LOG_TAG, "Error fetching manga", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
"Use the non-RxJava API instead",
|
||||
replaceWith = ReplaceWith("getPopularManga(page)"),
|
||||
)
|
||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||
return client.newCall(popularMangaRequest(page))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
if (!response.isSuccessful) {
|
||||
throw IOException("HTTP error ${response.code}")
|
||||
}
|
||||
popularMangaParse(response)
|
||||
}
|
||||
.onErrorReturn { error ->
|
||||
Log.e(LOG_TAG, "Error fetching popular manga", error)
|
||||
MangasPage(emptyList(), false)
|
||||
override suspend fun getPopularManga(page: Int): MangasPage = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val response = client.newCall(popularMangaRequest(page)).execute()
|
||||
if (!response.isSuccessful) {
|
||||
throw IOException("HTTP error ${response.code}")
|
||||
}
|
||||
popularMangaParse(response)
|
||||
} catch (e: Exception) {
|
||||
Log.e(LOG_TAG, "Error fetching popular manga", e)
|
||||
MangasPage(emptyList(), false)
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
"Use the non-RxJava API instead",
|
||||
replaceWith = ReplaceWith("getLatestUpdates(page)"),
|
||||
)
|
||||
override fun fetchLatestUpdates(page: Int) =
|
||||
fetch(latestUpdatesRequest(page))
|
||||
override suspend fun getLatestUpdates(page: Int): MangasPage = fetch(latestUpdatesRequest(page))
|
||||
|
||||
@Deprecated(
|
||||
"Use the non-RxJava API instead",
|
||||
replaceWith = ReplaceWith("getSearchManga(page, query, filters)"),
|
||||
)
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
override suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage {
|
||||
val specialListFilter = filters.find { it is SpecialListFilter } as? SpecialListFilter
|
||||
|
||||
return when (specialListFilter?.state) {
|
||||
1 -> {
|
||||
// Want to Read selected
|
||||
1 -> { // Want to Read selected
|
||||
val payload = """{
|
||||
"id": 0,
|
||||
"name": "",
|
||||
@@ -268,14 +245,12 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
||||
headersBuilder().build(),
|
||||
payload.toRequestBody(JSON_MEDIA_TYPE),
|
||||
)
|
||||
return client.newCall(request)
|
||||
.asObservableSuccess()
|
||||
.map { popularMangaParse(it) }
|
||||
val response = client.newCall(request).execute()
|
||||
popularMangaParse(response)
|
||||
}
|
||||
2 -> { // Reading Lists
|
||||
client.newCall(readingListRequest())
|
||||
.asObservableSuccess()
|
||||
.map { readingListParse(it) }
|
||||
val response = client.newCall(readingListRequest()).execute()
|
||||
readingListParse(response)
|
||||
}
|
||||
else -> { // Regular searches
|
||||
fetch(searchMangaRequest(page, query, filters))
|
||||
@@ -283,15 +258,11 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
"Use the non-RxJava API instead",
|
||||
replaceWith = ReplaceWith("getChapterList(manga)"),
|
||||
)
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
override suspend fun getChapterList(manga: SManga): List<SChapter> {
|
||||
return if (manga.url.contains("/ReadingList/")) {
|
||||
fetchReadingListItems(manga)
|
||||
} else {
|
||||
super.fetchChapterList(manga)
|
||||
super.getChapterList(manga)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,7 +270,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
||||
try {
|
||||
val result = response.parseAs<List<SeriesDto>>()
|
||||
series = result
|
||||
val mangaList = result.map { item -> helper.createSeriesDto(item, apiUrl, baseUrl, apiKey) }
|
||||
val mangaList = result.map { item -> helper.createSeriesDto(item, apiUrl, baseUrl) }
|
||||
return MangasPage(mangaList, helper.hasNextPage(response))
|
||||
} catch (e: Exception) {
|
||||
Log.e(LOG_TAG, "Unhandled exception", e)
|
||||
@@ -431,7 +402,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
||||
is Filter.Group<*> -> {
|
||||
when (filter) {
|
||||
is StatusFilterGroup -> {
|
||||
val statusFilter = filter.state.firstOrNull() as? StatusFilter
|
||||
val statusFilter = filter.state.firstOrNull()
|
||||
statusFilter?.state?.let { selectedIndex ->
|
||||
when (selectedIndex) {
|
||||
1 -> { // Unread (0%)
|
||||
@@ -505,22 +476,30 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
||||
}
|
||||
|
||||
is TagFilterGroup -> {
|
||||
filter.state.forEach { tagFilter ->
|
||||
val tag = tagsListMeta.find { it.title == (tagFilter as Filter.TriState).name }
|
||||
tag?.let {
|
||||
when (tagFilter.state) {
|
||||
STATE_INCLUDE -> filterV2.addStatement(
|
||||
FilterComparison.Contains,
|
||||
FilterField.Tags,
|
||||
tag.id.toString(),
|
||||
)
|
||||
STATE_EXCLUDE -> filterV2.addStatement(
|
||||
FilterComparison.NotContains,
|
||||
FilterField.Tags,
|
||||
tag.id.toString(),
|
||||
)
|
||||
}
|
||||
val included = filter.state
|
||||
.filter { it.state == STATE_INCLUDE }
|
||||
.mapNotNull { tagFilter ->
|
||||
tagsListMeta.find { it.title == tagFilter.name }?.id?.toString()
|
||||
}
|
||||
val excluded = filter.state
|
||||
.filter { it.state == STATE_EXCLUDE }
|
||||
.mapNotNull { tagFilter ->
|
||||
tagsListMeta.find { it.title == tagFilter.name }?.id?.toString()
|
||||
}
|
||||
|
||||
if (included.isNotEmpty()) {
|
||||
filterV2.addStatement(
|
||||
FilterComparison.Contains,
|
||||
FilterField.Tags,
|
||||
included.joinToString(","),
|
||||
)
|
||||
}
|
||||
if (excluded.isNotEmpty()) {
|
||||
filterV2.addStatement(
|
||||
FilterComparison.NotContains,
|
||||
FilterField.Tags,
|
||||
excluded.joinToString(","),
|
||||
)
|
||||
}
|
||||
}
|
||||
is AgeRatingFilterGroup -> {
|
||||
@@ -802,102 +781,88 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
||||
"Use the non-RxJava API instead",
|
||||
replaceWith = ReplaceWith("getMangaDetails(manga)"),
|
||||
)
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
override suspend fun getMangaDetails(manga: SManga): SManga {
|
||||
val serieId = helper.getIdFromUrl(manga.url)
|
||||
|
||||
return if (manga.url.contains("source=readinglist")) {
|
||||
val readingListId = manga.url.substringAfter("readingListId=").substringBefore("&").toIntOrNull()
|
||||
val now = java.util.Calendar.getInstance()
|
||||
val readingList = cachedReadingLists.find { it.id == readingListId }
|
||||
if (readingList != null) {
|
||||
val groupTags = preferences.groupTags
|
||||
val starting = readingList.startingYear.takeIf { it > 0 }?.let {
|
||||
"${readingList.startingYear}-${readingList.startingMonth.toString().padStart(2, '0')}"
|
||||
}
|
||||
val ending = readingList.endingYear.takeIf { it > 0 }?.let {
|
||||
"${readingList.endingYear}-${readingList.endingMonth.toString().padStart(2, '0')}"
|
||||
}
|
||||
|
||||
Observable.fromCallable {
|
||||
val readingList = cachedReadingLists.find { it.id == readingListId }
|
||||
if (readingList != null) {
|
||||
val groupTags = preferences.groupTags
|
||||
val starting = readingList.startingYear.takeIf { it > 0 }?.let {
|
||||
"${readingList.startingYear}-${readingList.startingMonth.toString().padStart(2, '0')}"
|
||||
}
|
||||
val ending = readingList.endingYear.takeIf { it > 0 }?.let {
|
||||
"${readingList.endingYear}-${readingList.endingMonth.toString().padStart(2, '0')}"
|
||||
}
|
||||
val itemsRequest = GET("$apiUrl/ReadingList/items?readingListId=$readingListId", headersBuilder().build())
|
||||
val items = try {
|
||||
client.newCall(itemsRequest).execute().parseAs<List<ReadingListItemDto>>()
|
||||
} catch (e: Exception) {
|
||||
Log.e(LOG_TAG, "Error parsing reading list items", e)
|
||||
emptyList()
|
||||
}
|
||||
|
||||
val itemsRequest = GET("$apiUrl/ReadingList/items?readingListId=$readingListId", headersBuilder().build())
|
||||
val items = try {
|
||||
client.newCall(itemsRequest).execute().parseAs<List<ReadingListItemDto>>()
|
||||
val allGenres = items.flatMap { item ->
|
||||
try {
|
||||
val seriesMetaRequest = GET("$apiUrl/series/metadata?seriesId=${item.seriesId}", headersBuilder().build())
|
||||
client.newCall(seriesMetaRequest).execute()
|
||||
.parseAs<SeriesDetailPlusDto>()
|
||||
.genres.map { it.title }
|
||||
} catch (e: Exception) {
|
||||
Log.e(LOG_TAG, "Error parsing reading list items", e)
|
||||
Log.e(LOG_TAG, "Error fetching series metadata for item ${item.seriesId}", e)
|
||||
emptyList()
|
||||
}
|
||||
}.distinct()
|
||||
|
||||
val allGenres = items.flatMap { item ->
|
||||
try {
|
||||
val seriesMetaRequest = GET("$apiUrl/series/metadata?seriesId=${item.seriesId}", headersBuilder().build())
|
||||
client.newCall(seriesMetaRequest).execute()
|
||||
.parseAs<SeriesDetailPlusDto>()
|
||||
.genres.map { it.title }
|
||||
} catch (e: Exception) {
|
||||
Log.e(LOG_TAG, "Error fetching series metadata for item ${item.seriesId}", e)
|
||||
emptyList()
|
||||
}
|
||||
}.distinct()
|
||||
|
||||
val genreString = if (groupTags) {
|
||||
buildList {
|
||||
add("Type: Reading List")
|
||||
starting?.let { add("Starting: $it") }
|
||||
ending?.let { add("Ending: $it") }
|
||||
if (allGenres.isNotEmpty()) addAll(allGenres.map { "Genres: $it" })
|
||||
}.joinToString(", ")
|
||||
} else {
|
||||
buildList {
|
||||
starting?.let { add("Starting: $it") }
|
||||
ending?.let { add("Ending: $it") }
|
||||
add("Reading List")
|
||||
if (allGenres.isNotEmpty()) addAll(allGenres)
|
||||
}.joinToString(", ")
|
||||
}
|
||||
|
||||
val hasDates = readingList.startingYear > 0 && readingList.endingYear > 0
|
||||
val status = if (hasDates) {
|
||||
val endCal = java.util.Calendar.getInstance().apply {
|
||||
set(readingList.endingYear, readingList.endingMonth - 1, 1)
|
||||
}
|
||||
if (now.after(endCal)) SManga.PUBLISHING_FINISHED else SManga.ONGOING
|
||||
} else {
|
||||
SManga.COMPLETED
|
||||
}
|
||||
|
||||
manga.apply {
|
||||
title = (if (readingList.promoted) "🔺 " else "") + readingList.title
|
||||
artist = "${readingList.itemCount} items"
|
||||
thumbnail_url = if (!readingList.coverImage.isNullOrBlank()) {
|
||||
"$apiUrl/image/${readingList.coverImage}?apiKey=$apiKey"
|
||||
} else {
|
||||
"$apiUrl/image/readinglist-cover?readingListId=$readingListId&apiKey=$apiKey"
|
||||
}
|
||||
description = readingList.summary ?: "Reading List"
|
||||
genre = genreString
|
||||
url = manga.url
|
||||
initialized = true
|
||||
this.status = status
|
||||
}
|
||||
val genreString = if (groupTags) {
|
||||
buildList {
|
||||
add("Type: Reading List")
|
||||
starting?.let { add("Starting: $it") }
|
||||
ending?.let { add("Ending: $it") }
|
||||
if (allGenres.isNotEmpty()) addAll(allGenres.map { "Genres: $it" })
|
||||
}.joinToString(", ")
|
||||
} else {
|
||||
manga.apply {
|
||||
initialized = true
|
||||
buildList {
|
||||
starting?.let { add("Starting: $it") }
|
||||
ending?.let { add("Ending: $it") }
|
||||
add("Reading List")
|
||||
if (allGenres.isNotEmpty()) addAll(allGenres)
|
||||
}.joinToString(", ")
|
||||
}
|
||||
|
||||
val hasDates = readingList.startingYear > 0 && readingList.endingYear > 0
|
||||
val status = if (hasDates) {
|
||||
val endCal = java.util.Calendar.getInstance().apply {
|
||||
set(readingList.endingYear, readingList.endingMonth - 1, 1)
|
||||
}
|
||||
if (now.after(endCal)) SManga.PUBLISHING_FINISHED else SManga.ONGOING
|
||||
} else {
|
||||
SManga.COMPLETED
|
||||
}
|
||||
}.onErrorReturn { error ->
|
||||
Log.e(LOG_TAG, "Error fetching reading list details", error)
|
||||
|
||||
manga.apply {
|
||||
description = helper.intl["error_loading_reading_list"]
|
||||
title = (if (readingList.promoted) "🔺 " else "") + readingList.title
|
||||
artist = "${readingList.itemCount} items"
|
||||
thumbnail_url = if (!readingList.coverImage.isNullOrBlank()) {
|
||||
"$apiUrl/image/${readingList.coverImage}?apiKey=$apiKey"
|
||||
} else {
|
||||
"$apiUrl/image/readinglist-cover?readingListId=$readingListId&apiKey=$apiKey"
|
||||
}
|
||||
description = readingList.summary ?: "Reading List"
|
||||
genre = genreString
|
||||
url = manga.url
|
||||
initialized = true
|
||||
this.status = status
|
||||
}
|
||||
} else {
|
||||
manga.apply { initialized = true }
|
||||
}
|
||||
} else {
|
||||
client.newCall(GET("$apiUrl/series/metadata?seriesId=$serieId", headersBuilder().build()))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
mangaDetailsParse(response).apply { initialized = true }
|
||||
}
|
||||
val response = client.newCall(GET("$apiUrl/series/metadata?seriesId=$serieId", headersBuilder().build())).execute()
|
||||
mangaDetailsParse(response).apply { initialized = true }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -965,9 +930,9 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
||||
.filterNot { it.equals(foundDemographic, ignoreCase = true) }
|
||||
.filterNot { tag -> filteredGenres.any { genre -> genre.equals(tag, ignoreCase = true) } }
|
||||
|
||||
val manga = helper.createSeriesDto(existingSeries, apiUrl, baseUrl, apiKey)
|
||||
val manga = helper.createSeriesDto(existingSeries, apiUrl, baseUrl)
|
||||
manga.title = existingSeries.name
|
||||
manga.url = "$baseUrl/library/${existingSeries.libraryId}/series/${result.seriesId}" // "$apiUrl/Series/${result.seriesId}"
|
||||
manga.url = "$baseUrl/Series/${result.seriesId}" // "$baseUrl/library/${existingSeries.libraryId}/series/${result.seriesId}"
|
||||
manga.artist = result.coverArtists.joinToString { it.name }
|
||||
manga.description = listOfNotNull(
|
||||
ratingLine.takeIf { it.isNotBlank() },
|
||||
@@ -1092,7 +1057,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
||||
.filterNot { it.equals(foundDemographic, ignoreCase = true) }
|
||||
.filterNot { tag -> filteredGenres.any { genre -> genre.equals(tag, ignoreCase = true) } }
|
||||
|
||||
url = "$baseUrl/library/${result.getLibraryId(serieDto)}/series/${result.seriesId}" // "$apiUrl/Series/${result.seriesId}"
|
||||
url = "$baseUrl/Series/${result.seriesId}" // "$baseUrl/library/${result.getLibraryId(serieDto)}/series/${result.seriesId}"
|
||||
artist = result.coverArtists.joinToString { it.name }
|
||||
description = listOfNotNull(
|
||||
ratingLine.takeIf { it.isNotBlank() },
|
||||
@@ -1173,13 +1138,11 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchReadingListItems(manga: SManga): Observable<List<SChapter>> {
|
||||
private fun fetchReadingListItems(manga: SManga): List<SChapter> {
|
||||
val readingListId = manga.url.substringAfter("readingListId=").substringBefore("&")
|
||||
val request = GET("$apiUrl/ReadingList/items?readingListId=$readingListId", headersBuilder().build())
|
||||
|
||||
return client.newCall(request)
|
||||
.asObservableSuccess()
|
||||
.map { parseReadingListItems(it) }
|
||||
val response = client.newCall(request).execute()
|
||||
return parseReadingListItems(response)
|
||||
}
|
||||
|
||||
private fun parseReadingListItems(response: Response): List<SChapter> {
|
||||
@@ -1321,7 +1284,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
||||
try {
|
||||
val seriesDto = client.newCall(GET("$apiUrl/Series/${item.seriesId}", headersBuilder().build()))
|
||||
.execute().parseAs<SeriesDto>()
|
||||
helper.createSeriesDto(seriesDto, apiUrl, baseUrl, apiKey)
|
||||
helper.createSeriesDto(seriesDto, apiUrl, baseUrl)
|
||||
} catch (e: Exception) {
|
||||
Log.e(LOG_TAG, "Error fetching series for related reading list", e)
|
||||
null
|
||||
@@ -1531,11 +1494,10 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
||||
return GET("$apiUrl/$chapterId", headersBuilder().build())
|
||||
}
|
||||
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList(chapter)"))
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||
override suspend fun getPageList(chapter: SChapter): List<Page> = withContext(Dispatchers.IO) {
|
||||
// Check if this is a reading list item (has readingListId in URL)
|
||||
if (chapter.url.contains("readingListId=")) {
|
||||
return Observable.fromCallable {
|
||||
try {
|
||||
// Extract seriesId from the chapter URL
|
||||
val seriesId = chapter.url.substringAfter("seriesId=").substringBefore("&").toIntOrNull()
|
||||
val readingListId = chapter.url.substringAfter("readingListId=").substringBefore("&").toIntOrNull()
|
||||
@@ -1560,32 +1522,39 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
||||
} ?: throw IOException("Item not found in reading list")
|
||||
|
||||
// Get item details based on type
|
||||
val chapterDetails = if (listItem.chapterId != null) {
|
||||
val request = GET("$apiUrl/Chapter?chapterId=${listItem.chapterId}", headersBuilder().build())
|
||||
client.newCall(request).execute().parseAs<ChapterDto>()
|
||||
} else if (listItem.volumeId != null) {
|
||||
// For volumes, get all chapters and let user choose
|
||||
val volumeRequest = GET("$apiUrl/Volume/${listItem.volumeId}", headersBuilder().build())
|
||||
val volume = client.newCall(volumeRequest).execute().parseAs<VolumeDto>()
|
||||
volume.chapters.firstOrNull() ?: throw IOException(helper.intl["error_no_chapters_found"])
|
||||
} else {
|
||||
throw IOException(helper.intl["error_invalid_reading_list_item"])
|
||||
val chapterDetails = when {
|
||||
listItem.chapterId != null -> {
|
||||
// Standard chapter
|
||||
val request = GET("$apiUrl/Chapter?chapterId=${listItem.chapterId}", headersBuilder().build())
|
||||
client.newCall(request).execute().parseAs<ChapterDto>()
|
||||
}
|
||||
listItem.volumeId != null -> {
|
||||
// Volume - get first chapter
|
||||
val volumeRequest = GET("$apiUrl/Volume/${listItem.volumeId}", headersBuilder().build())
|
||||
val volume = client.newCall(volumeRequest).execute().parseAs<VolumeDto>()
|
||||
volume.chapters.firstOrNull()?.let {
|
||||
val chapRequest = GET("$apiUrl/Chapter?chapterId=${it.id}", headersBuilder().build())
|
||||
client.newCall(chapRequest).execute().parseAs<ChapterDto>()
|
||||
} ?: throw IOException(helper.intl["error_no_chapters_found"])
|
||||
}
|
||||
else -> throw IOException(helper.intl["error_invalid_reading_list_item"])
|
||||
}
|
||||
|
||||
(0 until chapterDetails.pages).map { i ->
|
||||
// Generate pages with consistent URL format
|
||||
return@withContext (0 until chapterDetails.pages).map { i ->
|
||||
Page(
|
||||
index = i,
|
||||
imageUrl = "$apiUrl/Reader/image?chapterId=${chapterDetails.id}&page=$i&extractPdf=true&apiKey=$apiKey",
|
||||
)
|
||||
}
|
||||
}.onErrorResumeNext { error ->
|
||||
Log.e(LOG_TAG, "Error fetching reading list chapter pages", error)
|
||||
Observable.error(error)
|
||||
} catch (e: Exception) {
|
||||
Log.e(LOG_TAG, "Error processing reading list item", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// For normal titles
|
||||
return Observable.fromCallable {
|
||||
try {
|
||||
// Check if this is a volume URL
|
||||
if (chapter.url.contains("/Volume/") || chapter.url.startsWith("volume_")) {
|
||||
val volumeId = when {
|
||||
@@ -1621,7 +1590,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
||||
}
|
||||
}
|
||||
|
||||
(initialPages + remainingPages).toList()
|
||||
return@withContext (initialPages + remainingPages).toList()
|
||||
} else {
|
||||
// Original chapter handling
|
||||
val chapterId = when {
|
||||
@@ -1643,23 +1612,23 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
||||
throw IOException("${helper.intl["error_failed_parse_chapter"]}: ${e.message}")
|
||||
}
|
||||
|
||||
(0 until chapterDetails.pages).map { i ->
|
||||
return@withContext (0 until chapterDetails.pages).map { i ->
|
||||
Page(
|
||||
index = i,
|
||||
imageUrl = "$apiUrl/Reader/image?chapterId=$chapterId&page=$i&extractPdf=true&apiKey=$apiKey",
|
||||
)
|
||||
}
|
||||
}
|
||||
}.onErrorResumeNext { error ->
|
||||
} catch (e: Exception) {
|
||||
// Fallback to using the scanlator field if we can't get chapter details
|
||||
Log.e(LOG_TAG, "Error fetching chapter details, using fallback", error)
|
||||
Log.e(LOG_TAG, "Error fetching chapter details, using fallback", e)
|
||||
val fallbackPageCount = chapter.scanlator?.replace(" pages", "")?.toIntOrNull() ?: 1
|
||||
(0 until fallbackPageCount).map { i ->
|
||||
return@withContext (0 until fallbackPageCount).map { i ->
|
||||
Page(
|
||||
index = i,
|
||||
imageUrl = "$apiUrl/Reader/image?chapterId=${chapter.url.substringBefore("_")}&page=$i&extractPdf=true&apiKey=$apiKey",
|
||||
)
|
||||
}.let { Observable.just(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1672,7 +1641,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
return try {
|
||||
val result = response.parseAs<List<SeriesDto>>()
|
||||
val mangaList = result.map { helper.createSeriesDto(it, apiUrl, baseUrl, apiKey) }
|
||||
val mangaList = result.map { helper.createSeriesDto(it, apiUrl, baseUrl) }
|
||||
MangasPage(mangaList, false)
|
||||
} catch (e: Exception) {
|
||||
Log.e(LOG_TAG, "Error parsing search results", e)
|
||||
@@ -1701,101 +1670,77 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
||||
private var cachedReadingLists: List<ReadingListDto> = emptyList()
|
||||
|
||||
/**
|
||||
* Loads the enabled filters if they are not empty so Mihon can show them to the user
|
||||
* Loads the filters if they are not empty so Mihon can show them to the user
|
||||
*/
|
||||
private fun getFilterList(
|
||||
genres: List<String>,
|
||||
tags: List<String>,
|
||||
ageRatings: List<String>,
|
||||
collections: List<String>,
|
||||
languages: List<String>,
|
||||
libraries: List<String>,
|
||||
people: List<String>,
|
||||
pubStatus: List<String>,
|
||||
smartFilters: Array<String>,
|
||||
): FilterList {
|
||||
return FilterList(
|
||||
SortFilter(sortableList.map { it.first }.toTypedArray()),
|
||||
|
||||
StatusSeparator(),
|
||||
StatusFilterGroup(),
|
||||
|
||||
// Release year filter
|
||||
ReleaseYearRangeGroup(
|
||||
listOf(
|
||||
ReleaseYearRange("Min"),
|
||||
ReleaseYearRange("Max"),
|
||||
),
|
||||
),
|
||||
|
||||
GenreFilterGroup(genres.map { GenreFilter(it) }),
|
||||
TagFilterGroup(tags.map { TagFilter(it) }),
|
||||
CollectionFilterGroup(collections.map { CollectionFilter(it) }),
|
||||
LibrariesFilterGroup(libraries.map { LibraryFilter(it) }),
|
||||
LanguageFilterGroup(languages.map { LanguageFilter(it) }),
|
||||
PubStatusFilterGroup(pubStatus.map { PubStatusFilter(it) }),
|
||||
AgeRatingFilterGroup(ageRatings.map { AgeRatingFilter(it) }),
|
||||
|
||||
UserRating(),
|
||||
UserRatingSeparator(),
|
||||
|
||||
// Special filters section
|
||||
Filter.Header(helper.intl["filters_special"]),
|
||||
SpecialListFilter(helper.intl["filters_special_list"]),
|
||||
SmartFiltersFilter(smartFilters),
|
||||
|
||||
// @todo People filters (but not as filters, as a text search)
|
||||
// PeopleHeaderFilter(helper.intl["filters_people"]),
|
||||
// PeopleSeparatorFilter(),
|
||||
// WriterPeopleFilterGroup(people.map { WriterPeopleFilter(it) }),
|
||||
// PencillerPeopleFilterGroup(people.map { PencillerPeopleFilter(it) }),
|
||||
// InkerPeopleFilterGroup(people.map { InkerPeopleFilter(it) }),
|
||||
// ColoristPeopleFilterGroup(people.map { ColoristPeopleFilter(it) }),
|
||||
// LettererPeopleFilterGroup(people.map { LettererPeopleFilter(it) }),
|
||||
// CoverArtistPeopleFilterGroup(people.map { CoverArtistPeopleFilter(it) }),
|
||||
// EditorPeopleFilterGroup(people.map { EditorPeopleFilter(it) }),
|
||||
// PublisherPeopleFilterGroup(people.map { PublisherPeopleFilter(it) }),
|
||||
// CharacterPeopleFilterGroup(people.map { CharacterPeopleFilter(it) }),
|
||||
// TranslatorPeopleFilterGroup(people.map { TranslatorPeopleFilter(it) }),
|
||||
)
|
||||
}
|
||||
|
||||
override fun getFilterList(): FilterList {
|
||||
val toggledFilters = getToggledFilters()
|
||||
|
||||
val filters = try {
|
||||
val filtersLoaded = mutableListOf<Filter<*>>()
|
||||
|
||||
filtersLoaded.add(SortFilter(sortableList.map { it.first }.toTypedArray()))
|
||||
|
||||
if (smartFilters.isNotEmpty()) {
|
||||
filtersLoaded.add(SmartFiltersFilter(smartFilters.map { it.name }.toTypedArray()))
|
||||
}
|
||||
|
||||
filtersLoaded.add(SpecialListFilter())
|
||||
|
||||
if (toggledFilters.contains("Read Status")) {
|
||||
filtersLoaded.add(StatusFilterGroup())
|
||||
}
|
||||
if (toggledFilters.contains("ReleaseYearRange")) {
|
||||
filtersLoaded.add(
|
||||
ReleaseYearRangeGroup(
|
||||
listOf("Min", "Max").map { ReleaseYearRange(it) },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if (genresListMeta.isNotEmpty() and toggledFilters.contains("Genres")) {
|
||||
filtersLoaded.add(
|
||||
GenreFilterGroup(genresListMeta.map { GenreFilter(it.title) }),
|
||||
)
|
||||
}
|
||||
if (tagsListMeta.isNotEmpty() and toggledFilters.contains("Tags")) {
|
||||
filtersLoaded.add(
|
||||
TagFilterGroup(tagsListMeta.map { TagFilter(it.title) }),
|
||||
)
|
||||
}
|
||||
if (ageRatingsListMeta.isNotEmpty() and toggledFilters.contains("Age Rating")) {
|
||||
filtersLoaded.add(
|
||||
AgeRatingFilterGroup(ageRatingsListMeta.map { AgeRatingFilter(it.title) }),
|
||||
)
|
||||
}
|
||||
if (toggledFilters.contains("Format")) {
|
||||
filtersLoaded.add(
|
||||
FormatsFilterGroup(
|
||||
listOf(
|
||||
"Image",
|
||||
"Archive",
|
||||
"Pdf",
|
||||
"Epub",
|
||||
"Unknown",
|
||||
).map { FormatFilter(it) },
|
||||
),
|
||||
)
|
||||
}
|
||||
if (collectionsListMeta.isNotEmpty() and toggledFilters.contains("Collections")) {
|
||||
filtersLoaded.add(
|
||||
CollectionFilterGroup(collectionsListMeta.map { CollectionFilter(it.title) }),
|
||||
)
|
||||
}
|
||||
if (languagesListMeta.isNotEmpty() and toggledFilters.contains("Languages")) {
|
||||
filtersLoaded.add(
|
||||
LanguageFilterGroup(languagesListMeta.map { LanguageFilter(it.title) }),
|
||||
)
|
||||
}
|
||||
if (libraryListMeta.isNotEmpty() and toggledFilters.contains("Libraries")) {
|
||||
filtersLoaded.add(
|
||||
LibrariesFilterGroup(libraryListMeta.map { LibraryFilter(it.name) }),
|
||||
)
|
||||
}
|
||||
if (pubStatusListMeta.isNotEmpty() and toggledFilters.contains("Publication Status")) {
|
||||
filtersLoaded.add(
|
||||
PubStatusFilterGroup(
|
||||
listOf(
|
||||
PubStatusFilter("Ongoing"),
|
||||
PubStatusFilter("Hiatus"),
|
||||
PubStatusFilter("Completed"),
|
||||
PubStatusFilter("Cancelled"),
|
||||
PubStatusFilter("Ended"),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
if (userRatingsListMeta.isNotEmpty() and toggledFilters.contains("Rating")) {
|
||||
filtersLoaded.add(
|
||||
UserRating(),
|
||||
)
|
||||
}
|
||||
|
||||
filtersLoaded
|
||||
} catch (e: Exception) {
|
||||
Log.e(LOG_TAG, "[FILTERS] Error while creating filter list", e)
|
||||
FilterList(emptyList())
|
||||
}
|
||||
return FilterList(filters)
|
||||
return getFilterList(
|
||||
genres = genresListMeta.map { it.title },
|
||||
tags = tagsListMeta.map { it.title },
|
||||
ageRatings = ageRatingsListMeta.map { it.title },
|
||||
collections = collectionsListMeta.map { it.title },
|
||||
languages = languagesListMeta.map { it.title },
|
||||
libraries = libraryListMeta.map { it.name },
|
||||
people = peopleListMeta.map { it.name },
|
||||
pubStatus = pubStatusListMeta.map { it.title },
|
||||
smartFilters = smartFilters.map { it.name }.toTypedArray(),
|
||||
)
|
||||
}
|
||||
|
||||
class LoginErrorException(message: String? = null, cause: Throwable? = null) : Exception(message, cause) {
|
||||
@@ -2179,165 +2124,88 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
||||
Log.v(LOG_TAG, "[Login] Login successful")
|
||||
}
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
init {
|
||||
if (apiUrl.isNotBlank()) {
|
||||
Single.fromCallable {
|
||||
doLogin()
|
||||
try { // Get current version
|
||||
val requestUrl = "$apiUrl/Server/server-info-slim"
|
||||
val serverInfoDto = client.newCall(GET(requestUrl, headersBuilder().build()))
|
||||
scope.launch {
|
||||
try {
|
||||
doLogin()
|
||||
|
||||
// Load all filters in parallel
|
||||
val deferredFilters = listOf(
|
||||
async {
|
||||
genresListMeta = loadMetadata("$apiUrl/Metadata/genres")
|
||||
Log.d(LOG_TAG, "Loaded ${genresListMeta.size} genres")
|
||||
},
|
||||
async {
|
||||
tagsListMeta = loadMetadata("$apiUrl/Metadata/tags")
|
||||
Log.d(LOG_TAG, "Loaded ${tagsListMeta.size} tags")
|
||||
},
|
||||
async {
|
||||
ageRatingsListMeta = loadMetadata("$apiUrl/Metadata/age-ratings")
|
||||
Log.d(LOG_TAG, "Loaded ${ageRatingsListMeta.size} age ratings")
|
||||
},
|
||||
async {
|
||||
collectionsListMeta = loadMetadata("$apiUrl/Collection")
|
||||
Log.d(LOG_TAG, "Loaded ${collectionsListMeta.size} collections")
|
||||
},
|
||||
async {
|
||||
languagesListMeta = loadMetadata("$apiUrl/Metadata/languages")
|
||||
Log.d(LOG_TAG, "Loaded ${languagesListMeta.size} languages")
|
||||
},
|
||||
async {
|
||||
libraryListMeta = loadMetadata("$apiUrl/Library/libraries")
|
||||
Log.d(LOG_TAG, "Loaded ${libraryListMeta.size} libraries")
|
||||
},
|
||||
async {
|
||||
peopleListMeta = loadMetadata("$apiUrl/Metadata/people")
|
||||
Log.d(LOG_TAG, "Loaded ${peopleListMeta.size} people")
|
||||
},
|
||||
async {
|
||||
pubStatusListMeta = loadMetadata("$apiUrl/Metadata/publication-status")
|
||||
Log.d(LOG_TAG, "Loaded ${pubStatusListMeta.size} publication statuses")
|
||||
},
|
||||
async {
|
||||
smartFilters = loadMetadata("$apiUrl/filter")
|
||||
Log.d(LOG_TAG, "Loaded ${smartFilters.size} smart filters")
|
||||
},
|
||||
)
|
||||
|
||||
deferredFilters.awaitAll()
|
||||
|
||||
// Get server info
|
||||
val serverInfoDto = client.newCall(GET("$apiUrl/Server/server-info-slim", headersBuilder().build()))
|
||||
.execute()
|
||||
.parseAs<ServerInfoDto>()
|
||||
Log.e(
|
||||
LOG_TAG,
|
||||
"Extension version: code=${AppInfo.getVersionCode()} name=${AppInfo.getVersionName()}" +
|
||||
" - - Kavita version: ${serverInfoDto.kavitaVersion} - - Lang:${Locale.getDefault()}",
|
||||
) // this is not a real error. Using this so it gets printed in dump logs if there's any error
|
||||
} catch (e: EmptyRequestBody) {
|
||||
Log.e(LOG_TAG, "Extension version: code=${AppInfo.getVersionCode()} - name=${AppInfo.getVersionName()}")
|
||||
Log.d(LOG_TAG, "Kavita version: ${serverInfoDto.kavitaVersion}")
|
||||
} catch (e: Exception) {
|
||||
Log.e(LOG_TAG, "Tachiyomi version: code=${AppInfo.getVersionCode()} - name=${AppInfo.getVersionName()}", e)
|
||||
}
|
||||
try { // Load Filters
|
||||
// Genres
|
||||
Log.v(LOG_TAG, "[Filter] Fetching filters ")
|
||||
client.newCall(GET("$apiUrl/Metadata/genres", headersBuilder().build()))
|
||||
.execute().use { response ->
|
||||
|
||||
genresListMeta = try {
|
||||
response.body.use { json.decodeFromString(it.string()) }
|
||||
} catch (e: Exception) {
|
||||
Log.e(LOG_TAG, "[Filter] Error decoding JSON for genres filter -> ${response.body}", e)
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
// tagsListMeta
|
||||
client.newCall(GET("$apiUrl/Metadata/tags", headersBuilder().build()))
|
||||
.execute().use { response ->
|
||||
tagsListMeta = try {
|
||||
response.body.use { json.decodeFromString(it.string()) }
|
||||
} catch (e: Exception) {
|
||||
Log.e(LOG_TAG, "[Filter] Error decoding JSON for tagsList filter", e)
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
// age-ratings
|
||||
client.newCall(GET("$apiUrl/Metadata/age-ratings", headersBuilder().build()))
|
||||
.execute().use { response ->
|
||||
ageRatingsListMeta = try {
|
||||
response.body.use { json.decodeFromString(it.string()) }
|
||||
} catch (e: Exception) {
|
||||
Log.e(
|
||||
LOG_TAG,
|
||||
"[Filter] Error decoding JSON for age-ratings filter",
|
||||
e,
|
||||
)
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
// collectionsListMeta
|
||||
client.newCall(GET("$apiUrl/Collection", headersBuilder().build()))
|
||||
.execute().use { response ->
|
||||
collectionsListMeta = try {
|
||||
response.body.use { json.decodeFromString(it.string()) }
|
||||
} catch (e: Exception) {
|
||||
Log.e(
|
||||
LOG_TAG,
|
||||
"[Filter] Error decoding JSON for collectionsListMeta filter",
|
||||
e,
|
||||
)
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
// languagesListMeta
|
||||
client.newCall(GET("$apiUrl/Metadata/languages", headersBuilder().build()))
|
||||
.execute().use { response ->
|
||||
languagesListMeta = try {
|
||||
response.body.use { json.decodeFromString(it.string()) }
|
||||
} catch (e: Exception) {
|
||||
Log.e(
|
||||
LOG_TAG,
|
||||
"[Filter] Error decoding JSON for languagesListMeta filter",
|
||||
e,
|
||||
)
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
// libraries
|
||||
client.newCall(GET("$apiUrl/Library/libraries", headersBuilder().build()))
|
||||
.execute().use { response ->
|
||||
libraryListMeta = try {
|
||||
response.body.use { json.decodeFromString(it.string()) }
|
||||
} catch (e: Exception) {
|
||||
Log.e(
|
||||
LOG_TAG,
|
||||
"[Filter] Error decoding JSON for libraries filter",
|
||||
e,
|
||||
)
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
// peopleListMeta
|
||||
client.newCall(GET("$apiUrl/Metadata/people", headersBuilder().build()))
|
||||
.execute().use { response ->
|
||||
peopleListMeta = try {
|
||||
val jsonString = response.body.string()
|
||||
// Log.d(LOG_TAG, "Raw people metadata: $jsonString") // Debug log
|
||||
json.decodeFromString<List<MetadataPeople>>(jsonString)
|
||||
.filter { it.role != null } // Filter out entries without role
|
||||
} catch (e: Exception) {
|
||||
Log.e(LOG_TAG, "Error decoding people metadata", e)
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
client.newCall(GET("$apiUrl/Metadata/publication-status", headersBuilder().build()))
|
||||
.execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
Log.e(LOG_TAG, "Failed to fetch publication status: ${response.code}")
|
||||
pubStatusListMeta = emptyList()
|
||||
return@use
|
||||
}
|
||||
try {
|
||||
pubStatusListMeta = response.body.use { json.decodeFromString(it.string()) }
|
||||
} catch (e: Exception) {
|
||||
Log.e(LOG_TAG, "Error decoding publication status JSON", e)
|
||||
pubStatusListMeta = emptyList()
|
||||
}
|
||||
}
|
||||
client.newCall(GET("$apiUrl/filter", headersBuilder().build()))
|
||||
.execute().use { response ->
|
||||
smartFilters = try {
|
||||
response.body.use { json.decodeFromString(it.string()) }
|
||||
} catch (e: Exception) {
|
||||
Log.e(
|
||||
LOG_TAG,
|
||||
"error while decoding JSON for smartfilters",
|
||||
e,
|
||||
)
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
throw LoadingFilterFailed("Failed Loading Filters", e.cause)
|
||||
Log.e(LOG_TAG, "Initialization error", e)
|
||||
}
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(Schedulers.io())
|
||||
.subscribe(
|
||||
{},
|
||||
{ tr ->
|
||||
// Avoid polluting logs with traces of exception
|
||||
if (tr is EmptyRequestBody || tr is LoginErrorException) {
|
||||
Log.e(LOG_TAG, "error while doing initial calls\n${tr.cause}")
|
||||
return@subscribe
|
||||
}
|
||||
if (tr is ConnectException) { // avoid polluting logs with traces of exception
|
||||
Log.e(LOG_TAG, "Error while doing initial calls\n${tr.cause}")
|
||||
return@subscribe
|
||||
}
|
||||
Log.e(LOG_TAG, "error while doing initial calls", tr)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Add cleanup for the coroutine scope
|
||||
fun cleanup() {
|
||||
scope.cancel()
|
||||
// Clear any cached data if needed
|
||||
genresListMeta = emptyList()
|
||||
tagsListMeta = emptyList()
|
||||
ageRatingsListMeta = emptyList()
|
||||
peopleListMeta = emptyList()
|
||||
pubStatusListMeta = emptyList()
|
||||
languagesListMeta = emptyList()
|
||||
libraryListMeta = emptyList()
|
||||
collectionsListMeta = emptyList()
|
||||
smartFilters = emptyList()
|
||||
cachedReadingLists = emptyList()
|
||||
Log.d(LOG_TAG, "Cleanup completed")
|
||||
}
|
||||
|
||||
private inline fun <reified T> loadMetadata(url: String): T {
|
||||
return client.newCall(GET(url, headersBuilder().build()))
|
||||
.execute()
|
||||
.parseAs()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,18 +47,17 @@ class KavitaHelper {
|
||||
}
|
||||
}
|
||||
|
||||
// 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"
|
||||
@@ -102,16 +101,24 @@ class KavitaHelper {
|
||||
}
|
||||
|
||||
ChapterType.SingleFileVolume -> {
|
||||
cleanChapterTitle(
|
||||
when {
|
||||
volume.name.any { it.isLetter() } -> "v${formatVolumeNumber(volume)} - ${volume.name}"
|
||||
else -> "Volume ${formatVolumeNumber(volume)}"
|
||||
},
|
||||
ChapterTitleContext(
|
||||
mangaTitle = mangaTitle,
|
||||
volumeName = volume.name,
|
||||
),
|
||||
)
|
||||
val volumeNumber = formatVolumeNumber(volume)
|
||||
val cleanVolumeName = volume.name.trim()
|
||||
|
||||
// Always use the API's volume number, never parse from title
|
||||
when {
|
||||
// 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 -> {
|
||||
@@ -163,47 +170,6 @@ class KavitaHelper {
|
||||
}
|
||||
}
|
||||
|
||||
// @todo Keeping this in case we're switching the logic here this after the Mihon PR
|
||||
// chapter_number = when {
|
||||
// type == ChapterType.SingleFileVolume && singleFileVolumeNumber != null ->
|
||||
// singleFileVolumeNumber.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_${chapter.id}"
|
||||
// }
|
||||
|
||||
// Handle decimal chapter numbers properly
|
||||
chapter_number = when {
|
||||
type == ChapterType.SingleFileVolume && singleFileVolumeNumber != null ->
|
||||
|
||||
Reference in New Issue
Block a user