Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion locale/de_DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -5658,6 +5658,9 @@
"proposto da": "vorgeschlagen von",
"scade il": "endet am",
"scaduta il": "endete am",
"Votazione terminata": "Abstimmung beendet",
"Questo libro è già collegato a un'altra proposta di questo club: non è stato acquisito di nuovo.": "Dieses Buch ist bereits mit einem anderen Vorschlag dieses Clubs verknüpft: Es wurde nicht erneut übernommen.",
"La scadenza della votazione è passata: la votazione è stata chiusa e il tuo voto non è stato registrato.": "Die Frist der Abstimmung ist abgelaufen: Die Abstimmung wurde geschlossen und deine Stimme wurde nicht gezählt.",
"sì": "ja",
"votanti": "Teilnehmende",
"voti": "Stimmen",
Expand Down Expand Up @@ -6577,5 +6580,27 @@
"Genere e sottogeneri eliminati con successo!": "Genre und Untergenres erfolgreich gelöscht!",
"Questa azione elimina il genere e tutti i suoi sottogeneri in modo permanente. I libri associati verranno scollegati da questi generi.": "Diese Aktion löscht das Genre und alle seine Untergenres dauerhaft. Zugeordnete Bücher werden von diesen Genres getrennt.",
"Sei sicuro di voler eliminare questo genere e tutti i suoi sottogeneri? I libri associati verranno scollegati da questi generi.": "Sind Sie sicher, dass Sie dieses Genre und alle seine Untergenres löschen möchten? Zugeordnete Bücher werden von diesen Genres getrennt.",
"Elimina genere e sottogeneri": "Genre und Untergenres löschen"
"Elimina genere e sottogeneri": "Genre und Untergenres löschen",
"Proposte non selezionate": "Nicht ausgewählte Vorschläge",
"Proposte ancora in attesa di essere scelte. La lista include i libri mai votati e quelli che non hanno mai vinto una votazione chiusa.": "Vorschläge, die noch auf eine Auswahl warten. Dazu gehören Bücher, über die noch nie abgestimmt wurde, und Bücher, die noch nie eine abgeschlossene Abstimmung gewonnen haben.",
"votazione": "Abstimmung",
"votazioni": "Abstimmungen",
"ultima": "zuletzt",
"Non in catalogo": "Nicht im Katalog",
"Votazioni chiuse": "Geschlossene Abstimmungen",
"mai votata": "noch nicht abgestimmt",
"Seleziona tutti i prestiti estendibili": "Alle verlängerbaren Ausleihen auswählen",
"Estendi di": "Verlängern um",
"Estendi prestiti selezionati": "Ausgewählte Ausleihen verlängern",
"%d prestiti selezionati": "%d Ausleihen ausgewählt",
"Estendere i prestiti selezionati?": "Ausgewählte Ausleihen verlängern?",
"%n prestiti verranno estesi di %d giorni.": "%n Ausleihen werden um %d Tage verlängert.",
"Prestiti estesi": "Ausleihen verlängert",
"Nessun prestito esteso": "Keine Ausleihe verlängert",
"%n prestiti estesi di %d giorni.": "%n Ausleihen um %d Tage verlängert.",
"Selezione o numero di giorni non validi.": "Ungültige Auswahl oder Anzahl der Tage.",
"Estensione non riuscita, riprova.": "Verlängerung fehlgeschlagen, bitte erneut versuchen.",
"Estensione annullata: almeno un prestito entrerebbe in conflitto con un altro prestito o una prenotazione.": "Verlängerung abgebrochen: Mindestens eine Ausleihe würde mit einer anderen Ausleihe oder Reservierung in Konflikt geraten.",
"Estendi": "Verlängern",
"Lingue non disponibili.": "Sprachen sind nicht verfügbar."
}
27 changes: 26 additions & 1 deletion locale/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -5658,6 +5658,9 @@
"proposto da": "proposed by",
"scade il": "closes on",
"scaduta il": "closed on",
"Votazione terminata": "Voting ended",
"Questo libro è già collegato a un'altra proposta di questo club: non è stato acquisito di nuovo.": "This book is already linked to another proposal in this club: it was not acquired again.",
"La scadenza della votazione è passata: la votazione è stata chiusa e il tuo voto non è stato registrato.": "The poll's deadline has passed: the poll has been closed and your vote was not recorded.",
"sì": "yes",
"votanti": "voters",
"voti": "votes",
Expand Down Expand Up @@ -6577,5 +6580,27 @@
"Genere e sottogeneri eliminati con successo!": "Genre and subgenres deleted successfully!",
"Questa azione elimina il genere e tutti i suoi sottogeneri in modo permanente. I libri associati verranno scollegati da questi generi.": "This action permanently deletes the genre and all of its subgenres. Associated books will be unlinked from these genres.",
"Sei sicuro di voler eliminare questo genere e tutti i suoi sottogeneri? I libri associati verranno scollegati da questi generi.": "Are you sure you want to delete this genre and all of its subgenres? Associated books will be unlinked from these genres.",
"Elimina genere e sottogeneri": "Delete genre and subgenres"
"Elimina genere e sottogeneri": "Delete genre and subgenres",
"Proposte non selezionate": "Proposals not selected",
"Proposte ancora in attesa di essere scelte. La lista include i libri mai votati e quelli che non hanno mai vinto una votazione chiusa.": "Proposals still awaiting selection. The list includes books not yet voted on and books that have never won a closed poll.",
"votazione": "poll",
"votazioni": "polls",
"ultima": "last",
"Non in catalogo": "Not in catalog",
"Votazioni chiuse": "Closed polls",
"mai votata": "not yet voted on",
"Seleziona tutti i prestiti estendibili": "Select all extendable loans",
"Estendi di": "Extend by",
"Estendi prestiti selezionati": "Extend selected loans",
"%d prestiti selezionati": "%d loans selected",
"Estendere i prestiti selezionati?": "Extend the selected loans?",
"%n prestiti verranno estesi di %d giorni.": "%n loans will be extended by %d days.",
"Prestiti estesi": "Loans extended",
"Nessun prestito esteso": "No loan extended",
"%n prestiti estesi di %d giorni.": "%n loans extended by %d days.",
"Selezione o numero di giorni non validi.": "Invalid selection or number of days.",
"Estensione non riuscita, riprova.": "Extension failed, please try again.",
"Estensione annullata: almeno un prestito entrerebbe in conflitto con un altro prestito o una prenotazione.": "Extension cancelled: at least one loan would conflict with another loan or reservation.",
"Estendi": "Extend",
"Lingue non disponibili.": "Languages are unavailable."
}
27 changes: 26 additions & 1 deletion locale/fr_FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -5658,6 +5658,9 @@
"proposto da": "proposé par",
"scade il": "expire le",
"scaduta il": "expiré le",
"Votazione terminata": "Vote terminé",
"Questo libro è già collegato a un'altra proposta di questo club: non è stato acquisito di nuovo.": "Ce livre est déjà lié à une autre proposition de ce club : il n'a pas été acquis à nouveau.",
"La scadenza della votazione è passata: la votazione è stata chiusa e il tuo voto non è stato registrato.": "La date limite du vote est dépassée : le vote a été clôturé et votre vote n'a pas été enregistré.",
"sì": "oui",
"votanti": "votants",
"voti": "votes",
Expand Down Expand Up @@ -6577,5 +6580,27 @@
"Genere e sottogeneri eliminati con successo!": "Genre et sous-genres supprimés avec succès !",
"Questa azione elimina il genere e tutti i suoi sottogeneri in modo permanente. I libri associati verranno scollegati da questi generi.": "Cette action supprime définitivement le genre et tous ses sous-genres. Les livres associés seront dissociés de ces genres.",
"Sei sicuro di voler eliminare questo genere e tutti i suoi sottogeneri? I libri associati verranno scollegati da questi generi.": "Voulez-vous vraiment supprimer ce genre et tous ses sous-genres ? Les livres associés seront dissociés de ces genres.",
"Elimina genere e sottogeneri": "Supprimer le genre et les sous-genres"
"Elimina genere e sottogeneri": "Supprimer le genre et les sous-genres",
"Proposte non selezionate": "Propositions non retenues",
"Proposte ancora in attesa di essere scelte. La lista include i libri mai votati e quelli che non hanno mai vinto una votazione chiusa.": "Propositions encore en attente de sélection. La liste comprend les livres jamais soumis au vote et ceux qui n'ont jamais remporté un vote clos.",
"votazione": "vote",
"votazioni": "votes",
"ultima": "dernière",
"Non in catalogo": "Pas au catalogue",
"Votazioni chiuse": "Votes clos",
"mai votata": "pas encore soumise au vote",
"Seleziona tutti i prestiti estendibili": "Sélectionner tous les prêts prolongeables",
"Estendi di": "Prolonger de",
"Estendi prestiti selezionati": "Prolonger les prêts sélectionnés",
"%d prestiti selezionati": "%d prêts sélectionnés",
"Estendere i prestiti selezionati?": "Prolonger les prêts sélectionnés ?",
"%n prestiti verranno estesi di %d giorni.": "%n prêts seront prolongés de %d jours.",
"Prestiti estesi": "Prêts prolongés",
"Nessun prestito esteso": "Aucun prêt prolongé",
"%n prestiti estesi di %d giorni.": "%n prêts prolongés de %d jours.",
"Selezione o numero di giorni non validi.": "Sélection ou nombre de jours invalide.",
"Estensione non riuscita, riprova.": "La prolongation a échoué, veuillez réessayer.",
"Estensione annullata: almeno un prestito entrerebbe in conflitto con un altro prestito o una prenotazione.": "Prolongation annulée : au moins un prêt entrerait en conflit avec un autre prêt ou une réservation.",
"Estendi": "Prolonger",
"Lingue non disponibili.": "Les langues ne sont pas disponibles."
}
27 changes: 26 additions & 1 deletion locale/it_IT.json
Original file line number Diff line number Diff line change
Expand Up @@ -5658,6 +5658,9 @@
"proposto da": "proposto da",
"scade il": "scade il",
"scaduta il": "scaduta il",
"Votazione terminata": "Votazione terminata",
"Questo libro è già collegato a un'altra proposta di questo club: non è stato acquisito di nuovo.": "Questo libro è già collegato a un'altra proposta di questo club: non è stato acquisito di nuovo.",
"La scadenza della votazione è passata: la votazione è stata chiusa e il tuo voto non è stato registrato.": "La scadenza della votazione è passata: la votazione è stata chiusa e il tuo voto non è stato registrato.",
"sì": "sì",
"votanti": "votanti",
"voti": "voti",
Expand Down Expand Up @@ -6577,5 +6580,27 @@
"Genere e sottogeneri eliminati con successo!": "Genere e sottogeneri eliminati con successo!",
"Questa azione elimina il genere e tutti i suoi sottogeneri in modo permanente. I libri associati verranno scollegati da questi generi.": "Questa azione elimina il genere e tutti i suoi sottogeneri in modo permanente. I libri associati verranno scollegati da questi generi.",
"Sei sicuro di voler eliminare questo genere e tutti i suoi sottogeneri? I libri associati verranno scollegati da questi generi.": "Sei sicuro di voler eliminare questo genere e tutti i suoi sottogeneri? I libri associati verranno scollegati da questi generi.",
"Elimina genere e sottogeneri": "Elimina genere e sottogeneri"
"Elimina genere e sottogeneri": "Elimina genere e sottogeneri",
"Proposte non selezionate": "Proposte non selezionate",
"Proposte ancora in attesa di essere scelte. La lista include i libri mai votati e quelli che non hanno mai vinto una votazione chiusa.": "Proposte ancora in attesa di essere scelte. La lista include i libri mai votati e quelli che non hanno mai vinto una votazione chiusa.",
"votazione": "votazione",
"votazioni": "votazioni",
"ultima": "ultima",
"Non in catalogo": "Non in catalogo",
"Votazioni chiuse": "Votazioni chiuse",
"mai votata": "mai votata",
"Seleziona tutti i prestiti estendibili": "Seleziona tutti i prestiti estendibili",
"Estendi di": "Estendi di",
"Estendi prestiti selezionati": "Estendi prestiti selezionati",
"%d prestiti selezionati": "%d prestiti selezionati",
"Estendere i prestiti selezionati?": "Estendere i prestiti selezionati?",
"%n prestiti verranno estesi di %d giorni.": "%n prestiti verranno estesi di %d giorni.",
"Prestiti estesi": "Prestiti estesi",
"Nessun prestito esteso": "Nessun prestito esteso",
"%n prestiti estesi di %d giorni.": "%n prestiti estesi di %d giorni.",
"Selezione o numero di giorni non validi.": "Selezione o numero di giorni non validi.",
"Estensione non riuscita, riprova.": "Estensione non riuscita, riprova.",
"Estensione annullata: almeno un prestito entrerebbe in conflitto con un altro prestito o una prenotazione.": "Estensione annullata: almeno un prestito entrerebbe in conflitto con un altro prestito o una prenotazione.",
"Estendi": "Estendi",
"Lingue non disponibili.": "Lingue non disponibili."
}
8 changes: 8 additions & 0 deletions storage/plugins/mobile-api/MobileApiPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,14 @@ public function registerRoutes(\Slim\App $app): void
return (new CatalogController($db))->genres($request, $response);
})->add($quotaMw())->add($authMw());

// #282: real catalogue language values for the app's language filter.
$group->get('/catalog/languages', function (
ServerRequestInterface $request,
ResponseInterface $response
) use ($db): ResponseInterface {
return (new CatalogController($db))->languages($request, $response);
})->add($quotaMw())->add($authMw());

// ── User actions (loans / reservations / wishlist / profile / msg) ──
// Every handler is bearer-authenticated and strictly scoped to the
// token-resolved user; loan/reservation overlap + availability reuse
Expand Down
5 changes: 3 additions & 2 deletions storage/plugins/mobile-api/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "mobile-api",
"display_name": "Mobile API",
"description": "API REST/JSON versionata (/api/v1) per l'app companion mobile di Pinakes: discovery, autenticazione a token per dispositivo, ricerca catalogo, prestiti/prenotazioni, wishlist, profilo, messaggi e notifiche push. Disattivata per default finché non viene abilitata.",
"version": "1.4.0",
"version": "1.4.1",
"author": "Fabiodalez",
"author_url": "",
"plugin_url": "",
Expand All @@ -22,5 +22,6 @@
],
"optional": true,
"status": "stable"
}
},
"changelog": "v1.4.1: endpoint GET /api/v1/catalog/languages che restituisce i valori di lingua reali del catalogo (con conteggi) per popolare il filtro lingua dell'app dai dati veri invece che da una lista fissa (#282); il filtro lingua della ricerca ora e' case/space-insensitive. "
}
69 changes: 68 additions & 1 deletion storage/plugins/mobile-api/src/Controllers/CatalogController.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* GET /api/v1/catalog/search — filtered, cursor-paginated search.
* GET /api/v1/catalog/books/{id} — full book detail + personal history.
* GET /api/v1/catalog/genres — genre cascade tree for the filter UI.
* GET /api/v1/catalog/languages — real catalogue language values + counts.
*
* Design notes:
* - Soft-delete is non-negotiable: EVERY query on `libri` carries
Expand Down Expand Up @@ -365,6 +366,69 @@ public function genres(
}
}

// ─── GET /catalog/languages ──────────────────────────────────────────────

/**
* Distinct language values actually present in the (non-deleted) catalogue,
* each with its book count. `libri.lingua` is unnormalized free text, so a
* hardcoded language list in the app cannot match it (e.g. the app sends
* "German" while the row says "Deutsch"): filtering by language then returns
* nothing, which is exactly issue #282. The app should populate its language
* filter from THIS endpoint — the real collection values — the same way it
* already sources genres / authors / publishers. Read-only, ETag-cached.
*/
public function languages(
ServerRequestInterface $request,
ResponseInterface $response
): ResponseInterface {
try {
$rows = [];
// Static query, no user input; soft-delete honoured per CLAUDE.md rule 2.
$res = $this->db->query(
"SELECT MIN(TRIM(lingua)) AS language, COUNT(*) AS count
FROM libri
WHERE deleted_at IS NULL AND lingua IS NOT NULL AND TRIM(lingua) <> ''
GROUP BY LOWER(TRIM(lingua))
ORDER BY count DESC, language ASC"
);
if ($res !== false) {
while ($r = $res->fetch_assoc()) {
$rows[] = [
'language' => (string) $r['language'],
'count' => (int) $r['count'],
];
}
}

$etag = $this->computeLanguagesEtag($rows);
if ($this->notModified($request, $etag)) {
return $this->notModifiedResponse($response, $etag);
}

$response = ResponseEnvelope::success($response, $rows, ['count' => count($rows)], 200);

return $response
->withHeader('ETag', $etag)
->withHeader('Cache-Control', 'private, max-age=0, must-revalidate');
} catch (\Throwable $e) {
SecureLogger::error('[MobileApi] catalog languages failed: ' . $e->getMessage());
return ResponseEnvelope::error($response, 'internal_error', __('Lingue non disponibili.'), 500);
}
}

/**
* @param list<array{language: string, count: int}> $rows
*/
private function computeLanguagesEtag(array $rows): string
{
$seed = '';
foreach ($rows as $r) {
$seed .= $r['language'] . ':' . $r['count'] . '|';
}

return '"' . sha1('languages:' . $seed) . '"';
}

// ─── Filters ─────────────────────────────────────────────────────────────

/**
Expand Down Expand Up @@ -460,7 +524,10 @@ private function buildFilters(array $params): array

$language = isset($params['language']) ? trim((string) $params['language']) : '';
if ($language !== '') {
$conditions[] = 'l.lingua = ?';
// libri.lingua is unnormalized free text (#282): tolerate case /
// surrounding whitespace so a value taken from /catalog/languages
// matches regardless of how the client echoes it back.
$conditions[] = 'LOWER(TRIM(l.lingua)) = LOWER(TRIM(?))';
$bind[] = $language;
$types .= 's';
}
Expand Down
24 changes: 24 additions & 0 deletions storage/plugins/mobile-api/src/Controllers/OpenApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,30 @@ private function paths(): array
],
],

'/catalog/languages' => [
'get' => [
'tags' => ['catalog'],
'summary' => 'Catalogue languages',
'description' => 'Distinct language values present in the catalogue, each with a book count, for building the app language filter from real data rather than a hardcoded list (#282).',
'operationId' => 'getCatalogLanguages',
'security' => [['bearerAuth' => []]],
'responses' => [
'200' => ['description' => 'Languages.', 'content' => ['application/json' => ['schema' => [
'allOf' => [['$ref' => '#/components/schemas/Envelope']],
'properties' => ['data' => ['type' => 'array', 'items' => [
'type' => 'object',
'properties' => [
'language' => ['type' => 'string'],
'count' => ['type' => 'integer'],
],
]]],
]]]],
'401' => ['$ref' => '#/components/responses/Unauthorized'],
'500' => ['$ref' => '#/components/responses/InternalError'],
],
],
],

'/me/loans' => [
'get' => [
'tags' => ['loans'],
Expand Down
Loading
Loading