Compare commits
3 Commits
4972a403df
...
fc8856c83f
| Author | SHA1 | Date | |
|---|---|---|---|
| fc8856c83f | |||
| bd09f3d943 | |||
| 1f434c3d67 |
8
Cargo.lock
generated
8
Cargo.lock
generated
@@ -64,7 +64,7 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "api"
|
name = "api"
|
||||||
version = "1.18.0"
|
version = "1.19.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argon2",
|
"argon2",
|
||||||
@@ -1232,7 +1232,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexer"
|
name = "indexer"
|
||||||
version = "1.18.0"
|
version = "1.19.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -1771,7 +1771,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parsers"
|
name = "parsers"
|
||||||
version = "1.18.0"
|
version = "1.19.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"flate2",
|
"flate2",
|
||||||
@@ -2906,7 +2906,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stripstream-core"
|
name = "stripstream-core"
|
||||||
version = "1.18.0"
|
version = "1.19.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ resolver = "2"
|
|||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
version = "1.18.0"
|
version = "1.19.0"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ pub struct ListBooksQuery {
|
|||||||
/// Sort order: "title" (default) or "latest" (most recently added first)
|
/// Sort order: "title" (default) or "latest" (most recently added first)
|
||||||
#[schema(value_type = Option<String>, example = "latest")]
|
#[schema(value_type = Option<String>, example = "latest")]
|
||||||
pub sort: Option<String>,
|
pub sort: Option<String>,
|
||||||
|
/// Filter by metadata provider: "linked" (any provider), "unlinked" (no provider), or a specific provider name
|
||||||
|
#[schema(value_type = Option<String>, example = "linked")]
|
||||||
|
pub metadata_provider: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, ToSchema)]
|
#[derive(Serialize, ToSchema)]
|
||||||
@@ -108,6 +111,7 @@ pub struct BookDetails {
|
|||||||
("page" = Option<i64>, Query, description = "Page number (1-indexed, default 1)"),
|
("page" = Option<i64>, Query, description = "Page number (1-indexed, default 1)"),
|
||||||
("limit" = Option<i64>, Query, description = "Items per page (max 200, default 50)"),
|
("limit" = Option<i64>, Query, description = "Items per page (max 200, default 50)"),
|
||||||
("sort" = Option<String>, Query, description = "Sort order: 'title' (default) or 'latest' (most recently added first)"),
|
("sort" = Option<String>, Query, description = "Sort order: 'title' (default) or 'latest' (most recently added first)"),
|
||||||
|
("metadata_provider" = Option<String>, Query, description = "Filter by metadata provider: 'linked' (any provider), 'unlinked' (no provider), or a specific provider name"),
|
||||||
),
|
),
|
||||||
responses(
|
responses(
|
||||||
(status = 200, body = BooksPage),
|
(status = 200, body = BooksPage),
|
||||||
@@ -141,16 +145,34 @@ pub async fn list_books(
|
|||||||
let author_cond = if query.author.is_some() {
|
let author_cond = if query.author.is_some() {
|
||||||
p += 1; format!("AND (${p} = ANY(COALESCE(NULLIF(b.authors, '{{}}'), CASE WHEN b.author IS NOT NULL AND b.author != '' THEN ARRAY[b.author] ELSE ARRAY[]::text[] END)))")
|
p += 1; format!("AND (${p} = ANY(COALESCE(NULLIF(b.authors, '{{}}'), CASE WHEN b.author IS NOT NULL AND b.author != '' THEN ARRAY[b.author] ELSE ARRAY[]::text[] END)))")
|
||||||
} else { String::new() };
|
} else { String::new() };
|
||||||
|
let metadata_cond = match query.metadata_provider.as_deref() {
|
||||||
|
Some("unlinked") => "AND eml.id IS NULL".to_string(),
|
||||||
|
Some("linked") => "AND eml.id IS NOT NULL".to_string(),
|
||||||
|
Some(_) => { p += 1; format!("AND eml.provider = ${p}") },
|
||||||
|
None => String::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let metadata_links_cte = r#"
|
||||||
|
metadata_links AS (
|
||||||
|
SELECT DISTINCT ON (eml.series_name, eml.library_id)
|
||||||
|
eml.series_name, eml.library_id, eml.provider, eml.id
|
||||||
|
FROM external_metadata_links eml
|
||||||
|
WHERE eml.status = 'approved'
|
||||||
|
ORDER BY eml.series_name, eml.library_id, eml.created_at DESC
|
||||||
|
)"#;
|
||||||
|
|
||||||
let count_sql = format!(
|
let count_sql = format!(
|
||||||
r#"SELECT COUNT(*) FROM books b
|
r#"WITH {metadata_links_cte}
|
||||||
|
SELECT COUNT(*) FROM books b
|
||||||
LEFT JOIN book_reading_progress brp ON brp.book_id = b.id
|
LEFT JOIN book_reading_progress brp ON brp.book_id = b.id
|
||||||
|
LEFT JOIN metadata_links eml ON eml.series_name = b.series AND eml.library_id = b.library_id
|
||||||
WHERE ($1::uuid IS NULL OR b.library_id = $1)
|
WHERE ($1::uuid IS NULL OR b.library_id = $1)
|
||||||
AND ($2::text IS NULL OR b.kind = $2)
|
AND ($2::text IS NULL OR b.kind = $2)
|
||||||
AND ($3::text IS NULL OR b.format = $3)
|
AND ($3::text IS NULL OR b.format = $3)
|
||||||
{series_cond}
|
{series_cond}
|
||||||
{rs_cond}
|
{rs_cond}
|
||||||
{author_cond}"#
|
{author_cond}
|
||||||
|
{metadata_cond}"#
|
||||||
);
|
);
|
||||||
|
|
||||||
let order_clause = if query.sort.as_deref() == Some("latest") {
|
let order_clause = if query.sort.as_deref() == Some("latest") {
|
||||||
@@ -164,18 +186,21 @@ pub async fn list_books(
|
|||||||
let offset_p = p + 2;
|
let offset_p = p + 2;
|
||||||
let data_sql = format!(
|
let data_sql = format!(
|
||||||
r#"
|
r#"
|
||||||
|
WITH {metadata_links_cte}
|
||||||
SELECT b.id, b.library_id, b.kind, b.format, b.title, b.author, b.authors, b.series, b.volume, b.language, b.page_count, b.thumbnail_path, b.updated_at,
|
SELECT b.id, b.library_id, b.kind, b.format, b.title, b.author, b.authors, b.series, b.volume, b.language, b.page_count, b.thumbnail_path, b.updated_at,
|
||||||
COALESCE(brp.status, 'unread') AS reading_status,
|
COALESCE(brp.status, 'unread') AS reading_status,
|
||||||
brp.current_page AS reading_current_page,
|
brp.current_page AS reading_current_page,
|
||||||
brp.last_read_at AS reading_last_read_at
|
brp.last_read_at AS reading_last_read_at
|
||||||
FROM books b
|
FROM books b
|
||||||
LEFT JOIN book_reading_progress brp ON brp.book_id = b.id
|
LEFT JOIN book_reading_progress brp ON brp.book_id = b.id
|
||||||
|
LEFT JOIN metadata_links eml ON eml.series_name = b.series AND eml.library_id = b.library_id
|
||||||
WHERE ($1::uuid IS NULL OR b.library_id = $1)
|
WHERE ($1::uuid IS NULL OR b.library_id = $1)
|
||||||
AND ($2::text IS NULL OR b.kind = $2)
|
AND ($2::text IS NULL OR b.kind = $2)
|
||||||
AND ($3::text IS NULL OR b.format = $3)
|
AND ($3::text IS NULL OR b.format = $3)
|
||||||
{series_cond}
|
{series_cond}
|
||||||
{rs_cond}
|
{rs_cond}
|
||||||
{author_cond}
|
{author_cond}
|
||||||
|
{metadata_cond}
|
||||||
ORDER BY {order_clause}
|
ORDER BY {order_clause}
|
||||||
LIMIT ${limit_p} OFFSET ${offset_p}
|
LIMIT ${limit_p} OFFSET ${offset_p}
|
||||||
"#
|
"#
|
||||||
@@ -204,6 +229,12 @@ pub async fn list_books(
|
|||||||
count_builder = count_builder.bind(author.clone());
|
count_builder = count_builder.bind(author.clone());
|
||||||
data_builder = data_builder.bind(author.clone());
|
data_builder = data_builder.bind(author.clone());
|
||||||
}
|
}
|
||||||
|
if let Some(ref mp) = query.metadata_provider {
|
||||||
|
if mp != "linked" && mp != "unlinked" {
|
||||||
|
count_builder = count_builder.bind(mp.clone());
|
||||||
|
data_builder = data_builder.bind(mp.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data_builder = data_builder.bind(limit).bind(offset);
|
data_builder = data_builder.bind(limit).bind(offset);
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ export default async function BooksPage({
|
|||||||
const libraryId = typeof searchParamsAwaited.library === "string" ? searchParamsAwaited.library : undefined;
|
const libraryId = typeof searchParamsAwaited.library === "string" ? searchParamsAwaited.library : undefined;
|
||||||
const searchQuery = typeof searchParamsAwaited.q === "string" ? searchParamsAwaited.q : "";
|
const searchQuery = typeof searchParamsAwaited.q === "string" ? searchParamsAwaited.q : "";
|
||||||
const readingStatus = typeof searchParamsAwaited.status === "string" ? searchParamsAwaited.status : undefined;
|
const readingStatus = typeof searchParamsAwaited.status === "string" ? searchParamsAwaited.status : undefined;
|
||||||
|
const format = typeof searchParamsAwaited.format === "string" ? searchParamsAwaited.format : undefined;
|
||||||
|
const metadataProvider = typeof searchParamsAwaited.metadata === "string" ? searchParamsAwaited.metadata : undefined;
|
||||||
const sort = typeof searchParamsAwaited.sort === "string" ? searchParamsAwaited.sort : undefined;
|
const sort = typeof searchParamsAwaited.sort === "string" ? searchParamsAwaited.sort : undefined;
|
||||||
const page = typeof searchParamsAwaited.page === "string" ? parseInt(searchParamsAwaited.page) : 1;
|
const page = typeof searchParamsAwaited.page === "string" ? parseInt(searchParamsAwaited.page) : 1;
|
||||||
const limit = typeof searchParamsAwaited.limit === "string" ? parseInt(searchParamsAwaited.limit) : 20;
|
const limit = typeof searchParamsAwaited.limit === "string" ? parseInt(searchParamsAwaited.limit) : 20;
|
||||||
@@ -62,7 +64,7 @@ export default async function BooksPage({
|
|||||||
totalHits = searchResponse.estimated_total_hits;
|
totalHits = searchResponse.estimated_total_hits;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const booksPage = await fetchBooks(libraryId, undefined, page, limit, readingStatus, sort).catch(() => ({
|
const booksPage = await fetchBooks(libraryId, undefined, page, limit, readingStatus, sort, undefined, format, metadataProvider).catch(() => ({
|
||||||
items: [] as BookDto[],
|
items: [] as BookDto[],
|
||||||
total: 0,
|
total: 0,
|
||||||
page: 1,
|
page: 1,
|
||||||
@@ -91,12 +93,26 @@ export default async function BooksPage({
|
|||||||
{ value: "read", label: t("status.read") },
|
{ value: "read", label: t("status.read") },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const formatOptions = [
|
||||||
|
{ value: "", label: t("books.allFormats") },
|
||||||
|
{ value: "cbz", label: "CBZ" },
|
||||||
|
{ value: "cbr", label: "CBR" },
|
||||||
|
{ value: "pdf", label: "PDF" },
|
||||||
|
{ value: "epub", label: "EPUB" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const metadataOptions = [
|
||||||
|
{ value: "", label: t("series.metadataAll") },
|
||||||
|
{ value: "linked", label: t("series.metadataLinked") },
|
||||||
|
{ value: "unlinked", label: t("series.metadataUnlinked") },
|
||||||
|
];
|
||||||
|
|
||||||
const sortOptions = [
|
const sortOptions = [
|
||||||
{ value: "", label: t("books.sortTitle") },
|
{ value: "", label: t("books.sortTitle") },
|
||||||
{ value: "latest", label: t("books.sortLatest") },
|
{ value: "latest", label: t("books.sortLatest") },
|
||||||
];
|
];
|
||||||
|
|
||||||
const hasFilters = searchQuery || libraryId || readingStatus || sort;
|
const hasFilters = searchQuery || libraryId || readingStatus || format || metadataProvider || sort;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -117,6 +133,8 @@ export default async function BooksPage({
|
|||||||
{ name: "q", type: "text", label: t("common.search"), placeholder: t("books.searchPlaceholder") },
|
{ name: "q", type: "text", label: t("common.search"), placeholder: t("books.searchPlaceholder") },
|
||||||
{ name: "library", type: "select", label: t("books.library"), options: libraryOptions },
|
{ name: "library", type: "select", label: t("books.library"), options: libraryOptions },
|
||||||
{ name: "status", type: "select", label: t("books.status"), options: statusOptions },
|
{ name: "status", type: "select", label: t("books.status"), options: statusOptions },
|
||||||
|
{ name: "format", type: "select", label: t("books.format"), options: formatOptions },
|
||||||
|
{ name: "metadata", type: "select", label: t("series.metadata"), options: metadataOptions },
|
||||||
{ name: "sort", type: "select", label: t("books.sort"), options: sortOptions },
|
{ name: "sort", type: "select", label: t("books.sort"), options: sortOptions },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ const FILTER_ICONS: Record<string, string> = {
|
|||||||
metadata_provider: "M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z",
|
metadata_provider: "M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z",
|
||||||
// Sort - arrows up/down
|
// Sort - arrows up/down
|
||||||
sort: "M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12",
|
sort: "M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12",
|
||||||
|
// Format - document/file
|
||||||
|
format: "M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z",
|
||||||
|
// Metadata - link/chain
|
||||||
|
metadata: "M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1",
|
||||||
};
|
};
|
||||||
|
|
||||||
interface FieldDef {
|
interface FieldDef {
|
||||||
@@ -35,12 +39,17 @@ interface LiveSearchFormProps {
|
|||||||
debounceMs?: number;
|
debounceMs?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const STORAGE_KEY_PREFIX = "filters:";
|
||||||
|
|
||||||
export function LiveSearchForm({ fields, basePath, debounceMs = 300 }: LiveSearchFormProps) {
|
export function LiveSearchForm({ fields, basePath, debounceMs = 300 }: LiveSearchFormProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const formRef = useRef<HTMLFormElement>(null);
|
const formRef = useRef<HTMLFormElement>(null);
|
||||||
|
const restoredRef = useRef(false);
|
||||||
|
|
||||||
|
const storageKey = `${STORAGE_KEY_PREFIX}${basePath}`;
|
||||||
|
|
||||||
const buildUrl = useCallback((): string => {
|
const buildUrl = useCallback((): string => {
|
||||||
if (!formRef.current) return basePath;
|
if (!formRef.current) return basePath;
|
||||||
@@ -54,16 +63,58 @@ export function LiveSearchForm({ fields, basePath, debounceMs = 300 }: LiveSearc
|
|||||||
return qs ? `${basePath}?${qs}` : basePath;
|
return qs ? `${basePath}?${qs}` : basePath;
|
||||||
}, [basePath]);
|
}, [basePath]);
|
||||||
|
|
||||||
|
const saveFilters = useCallback(() => {
|
||||||
|
if (!formRef.current) return;
|
||||||
|
const formData = new FormData(formRef.current);
|
||||||
|
const filters: Record<string, string> = {};
|
||||||
|
for (const [key, value] of formData.entries()) {
|
||||||
|
const str = value.toString().trim();
|
||||||
|
if (str) filters[key] = str;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
localStorage.setItem(storageKey, JSON.stringify(filters));
|
||||||
|
} catch {}
|
||||||
|
}, [storageKey]);
|
||||||
|
|
||||||
const navigate = useCallback((immediate: boolean) => {
|
const navigate = useCallback((immediate: boolean) => {
|
||||||
if (timerRef.current) clearTimeout(timerRef.current);
|
if (timerRef.current) clearTimeout(timerRef.current);
|
||||||
if (immediate) {
|
if (immediate) {
|
||||||
|
saveFilters();
|
||||||
router.replace(buildUrl() as any);
|
router.replace(buildUrl() as any);
|
||||||
} else {
|
} else {
|
||||||
timerRef.current = setTimeout(() => {
|
timerRef.current = setTimeout(() => {
|
||||||
|
saveFilters();
|
||||||
router.replace(buildUrl() as any);
|
router.replace(buildUrl() as any);
|
||||||
}, debounceMs);
|
}, debounceMs);
|
||||||
}
|
}
|
||||||
}, [router, buildUrl, debounceMs]);
|
}, [router, buildUrl, debounceMs, saveFilters]);
|
||||||
|
|
||||||
|
// Restore filters from localStorage on mount if URL has no filters
|
||||||
|
useEffect(() => {
|
||||||
|
if (restoredRef.current) return;
|
||||||
|
restoredRef.current = true;
|
||||||
|
|
||||||
|
const hasUrlFilters = fields.some((f) => {
|
||||||
|
const val = searchParams.get(f.name);
|
||||||
|
return val && val.trim() !== "";
|
||||||
|
});
|
||||||
|
if (hasUrlFilters) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem(storageKey);
|
||||||
|
if (!saved) return;
|
||||||
|
const filters: Record<string, string> = JSON.parse(saved);
|
||||||
|
const fieldNames = new Set(fields.map((f) => f.name));
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
for (const [key, value] of Object.entries(filters)) {
|
||||||
|
if (fieldNames.has(key) && value) params.set(key, value);
|
||||||
|
}
|
||||||
|
const qs = params.toString();
|
||||||
|
if (qs) {
|
||||||
|
router.replace(`${basePath}?${qs}` as any);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@@ -85,6 +136,7 @@ export function LiveSearchForm({ fields, basePath, debounceMs = 300 }: LiveSearc
|
|||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (timerRef.current) clearTimeout(timerRef.current);
|
if (timerRef.current) clearTimeout(timerRef.current);
|
||||||
|
saveFilters();
|
||||||
router.replace(buildUrl() as any);
|
router.replace(buildUrl() as any);
|
||||||
}}
|
}}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
@@ -145,7 +197,11 @@ export function LiveSearchForm({ fields, basePath, debounceMs = 300 }: LiveSearc
|
|||||||
{hasFilters && (
|
{hasFilters && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => router.replace(basePath as any)}
|
onClick={() => {
|
||||||
|
formRef.current?.reset();
|
||||||
|
try { localStorage.removeItem(storageKey); } catch {}
|
||||||
|
router.replace(basePath as any);
|
||||||
|
}}
|
||||||
className="
|
className="
|
||||||
inline-flex items-center gap-1
|
inline-flex items-center gap-1
|
||||||
h-8 px-2.5
|
h-8 px-2.5
|
||||||
|
|||||||
@@ -286,6 +286,8 @@ export async function fetchBooks(
|
|||||||
readingStatus?: string,
|
readingStatus?: string,
|
||||||
sort?: string,
|
sort?: string,
|
||||||
author?: string,
|
author?: string,
|
||||||
|
format?: string,
|
||||||
|
metadataProvider?: string,
|
||||||
): Promise<BooksPageDto> {
|
): Promise<BooksPageDto> {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (libraryId) params.set("library_id", libraryId);
|
if (libraryId) params.set("library_id", libraryId);
|
||||||
@@ -293,6 +295,8 @@ export async function fetchBooks(
|
|||||||
if (readingStatus) params.set("reading_status", readingStatus);
|
if (readingStatus) params.set("reading_status", readingStatus);
|
||||||
if (sort) params.set("sort", sort);
|
if (sort) params.set("sort", sort);
|
||||||
if (author) params.set("author", author);
|
if (author) params.set("author", author);
|
||||||
|
if (format) params.set("format", format);
|
||||||
|
if (metadataProvider) params.set("metadata_provider", metadataProvider);
|
||||||
params.set("page", page.toString());
|
params.set("page", page.toString());
|
||||||
params.set("limit", limit.toString());
|
params.set("limit", limit.toString());
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,8 @@ const en: Record<TranslationKey, string> = {
|
|||||||
"books.noResults": "No books found for \"{{query}}\"",
|
"books.noResults": "No books found for \"{{query}}\"",
|
||||||
"books.noBooks": "No books available",
|
"books.noBooks": "No books available",
|
||||||
"books.coverOf": "Cover of {{name}}",
|
"books.coverOf": "Cover of {{name}}",
|
||||||
|
"books.format": "Format",
|
||||||
|
"books.allFormats": "All formats",
|
||||||
|
|
||||||
// Series page
|
// Series page
|
||||||
"series.title": "Series",
|
"series.title": "Series",
|
||||||
|
|||||||
@@ -98,6 +98,8 @@ const fr = {
|
|||||||
"books.noResults": "Aucun livre trouvé pour \"{{query}}\"",
|
"books.noResults": "Aucun livre trouvé pour \"{{query}}\"",
|
||||||
"books.noBooks": "Aucun livre disponible",
|
"books.noBooks": "Aucun livre disponible",
|
||||||
"books.coverOf": "Couverture de {{name}}",
|
"books.coverOf": "Couverture de {{name}}",
|
||||||
|
"books.format": "Format",
|
||||||
|
"books.allFormats": "Tous les formats",
|
||||||
|
|
||||||
// Series page
|
// Series page
|
||||||
"series.title": "Séries",
|
"series.title": "Séries",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "stripstream-backoffice",
|
"name": "stripstream-backoffice",
|
||||||
"version": "1.18.0",
|
"version": "1.19.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -p 7082",
|
"dev": "next dev -p 7082",
|
||||||
|
|||||||
Reference in New Issue
Block a user