Compare commits
5 Commits
163dc3698c
...
39e9f35acb
| Author | SHA1 | Date | |
|---|---|---|---|
| 39e9f35acb | |||
| 36987f59b9 | |||
| 931d0e06f4 | |||
| 741a4da878 | |||
| e28b78d0e6 |
20
.gitea/workflows/deploy.yml
Normal file
20
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
name: Deploy with Docker Compose
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main # adapte la branche que tu veux déployer
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: mac-orbstack-runner # le nom que tu as donné au runner
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Deploy stack
|
||||||
|
env:
|
||||||
|
DOCKER_BUILDKIT: 1
|
||||||
|
COMPOSE_DOCKER_CLI_BUILD: 1
|
||||||
|
run: |
|
||||||
|
BUILDKIT_PROGRESS=plain docker pull julienfroidefond32/stripstream-backoffice && docker pull julienfroidefond32/stripstream-api && docker pull julienfroidefond32/stripstream-indexer && ./scripts/stack.sh up stripstream
|
||||||
8
Cargo.lock
generated
8
Cargo.lock
generated
@@ -64,7 +64,7 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "api"
|
name = "api"
|
||||||
version = "1.7.0"
|
version = "1.8.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argon2",
|
"argon2",
|
||||||
@@ -1232,7 +1232,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexer"
|
name = "indexer"
|
||||||
version = "1.7.0"
|
version = "1.8.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -1771,7 +1771,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parsers"
|
name = "parsers"
|
||||||
version = "1.7.0"
|
version = "1.8.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"flate2",
|
"flate2",
|
||||||
@@ -2906,7 +2906,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stripstream-core"
|
name = "stripstream-core"
|
||||||
version = "1.7.0"
|
version = "1.8.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ resolver = "2"
|
|||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
version = "1.7.0"
|
version = "1.8.1"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
|||||||
@@ -513,6 +513,15 @@ async fn get_series_books_impl(
|
|||||||
|
|
||||||
// External book ID from album URL (e.g. "...-1063.html")
|
// External book ID from album URL (e.g. "...-1063.html")
|
||||||
let album_url = title_el.and_then(|el| el.value().attr("href")).unwrap_or("");
|
let album_url = title_el.and_then(|el| el.value().attr("href")).unwrap_or("");
|
||||||
|
|
||||||
|
// Only keep main tomes — their URLs contain "Tome-{N}-"
|
||||||
|
// Skip hors-série (HS), intégrales (INT/INTFL), romans, coffrets, etc.
|
||||||
|
if let Ok(re) = regex::Regex::new(r"(?i)-Tome-\d+-") {
|
||||||
|
if !re.is_match(album_url) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let external_book_id = regex::Regex::new(r"-(\d+)\.html")
|
let external_book_id = regex::Regex::new(r"-(\d+)\.html")
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|re| re.captures(album_url))
|
.and_then(|re| re.captures(album_url))
|
||||||
|
|||||||
@@ -114,10 +114,10 @@ export default async function BooksPage({
|
|||||||
<LiveSearchForm
|
<LiveSearchForm
|
||||||
basePath="/books"
|
basePath="/books"
|
||||||
fields={[
|
fields={[
|
||||||
{ name: "q", type: "text", label: t("common.search"), placeholder: t("books.searchPlaceholder"), className: "flex-1 w-full" },
|
{ name: "q", type: "text", label: t("common.search"), placeholder: t("books.searchPlaceholder") },
|
||||||
{ name: "library", type: "select", label: t("books.library"), options: libraryOptions, className: "w-full sm:w-48" },
|
{ name: "library", type: "select", label: t("books.library"), options: libraryOptions },
|
||||||
{ name: "status", type: "select", label: t("books.status"), options: statusOptions, className: "w-full sm:w-40" },
|
{ name: "status", type: "select", label: t("books.status"), options: statusOptions },
|
||||||
{ name: "sort", type: "select", label: t("books.sort"), options: sortOptions, className: "w-full sm:w-40" },
|
{ name: "sort", type: "select", label: t("books.sort"), options: sortOptions },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -4,6 +4,22 @@ import { useRef, useCallback, useEffect } from "react";
|
|||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useTranslation } from "../../lib/i18n/context";
|
import { useTranslation } from "../../lib/i18n/context";
|
||||||
|
|
||||||
|
// SVG path data for filter icons, keyed by field name
|
||||||
|
const FILTER_ICONS: Record<string, string> = {
|
||||||
|
// Library - building/collection
|
||||||
|
library: "M8 14v3m4-3v3m4-3v3M3 21h18M3 10h18M3 7l9-4 9 4M4 10h16v11H4V10z",
|
||||||
|
// Reading status - open book
|
||||||
|
status: "M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253",
|
||||||
|
// Series status - signal/activity
|
||||||
|
series_status: "M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z",
|
||||||
|
// Missing books - warning triangle
|
||||||
|
has_missing: "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z",
|
||||||
|
// Metadata provider - tag
|
||||||
|
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: "M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12",
|
||||||
|
};
|
||||||
|
|
||||||
interface FieldDef {
|
interface FieldDef {
|
||||||
name: string;
|
name: string;
|
||||||
type: "text" | "select";
|
type: "text" | "select";
|
||||||
@@ -60,6 +76,9 @@ export function LiveSearchForm({ fields, basePath, debounceMs = 300 }: LiveSearc
|
|||||||
return val && val.trim() !== "";
|
return val && val.trim() !== "";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const textFields = fields.filter((f) => f.type === "text");
|
||||||
|
const selectFields = fields.filter((f) => f.type === "select");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
ref={formRef}
|
ref={formRef}
|
||||||
@@ -68,33 +87,52 @@ export function LiveSearchForm({ fields, basePath, debounceMs = 300 }: LiveSearc
|
|||||||
if (timerRef.current) clearTimeout(timerRef.current);
|
if (timerRef.current) clearTimeout(timerRef.current);
|
||||||
router.replace(buildUrl() as any);
|
router.replace(buildUrl() as any);
|
||||||
}}
|
}}
|
||||||
className="flex flex-col sm:flex-row sm:flex-wrap gap-3 items-start sm:items-end"
|
className="space-y-4"
|
||||||
>
|
>
|
||||||
{fields.map((field) =>
|
{/* Search input with icon */}
|
||||||
field.type === "text" ? (
|
{textFields.map((field) => (
|
||||||
<div key={field.name} className={field.className || "flex-1 w-full"}>
|
<div key={field.name} className="relative">
|
||||||
<label className="block text-sm font-medium text-foreground mb-1.5">
|
<svg
|
||||||
{field.label}
|
className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-muted-foreground pointer-events-none"
|
||||||
</label>
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
|
</svg>
|
||||||
<input
|
<input
|
||||||
name={field.name}
|
name={field.name}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={field.placeholder}
|
placeholder={field.placeholder}
|
||||||
defaultValue={searchParams.get(field.name) || ""}
|
defaultValue={searchParams.get(field.name) || ""}
|
||||||
onChange={() => navigate(false)}
|
onChange={() => navigate(false)}
|
||||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
className="flex h-11 w-full rounded-lg border border-input bg-background pl-10 pr-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
))}
|
||||||
<div key={field.name} className={field.className || "w-full sm:w-48"}>
|
|
||||||
<label className="block text-sm font-medium text-foreground mb-1.5">
|
{/* Filters row */}
|
||||||
|
{selectFields.length > 0 && (
|
||||||
|
<>
|
||||||
|
{textFields.length > 0 && (
|
||||||
|
<div className="border-t border-border/60" />
|
||||||
|
)}
|
||||||
|
<div className="flex flex-wrap gap-3 items-center">
|
||||||
|
{selectFields.map((field) => (
|
||||||
|
<div key={field.name} className="flex items-center gap-1.5">
|
||||||
|
{FILTER_ICONS[field.name] && (
|
||||||
|
<svg className="w-3.5 h-3.5 text-muted-foreground shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={FILTER_ICONS[field.name]} />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
<label className="text-xs font-medium text-muted-foreground whitespace-nowrap">
|
||||||
{field.label}
|
{field.label}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
name={field.name}
|
name={field.name}
|
||||||
defaultValue={searchParams.get(field.name) || ""}
|
defaultValue={searchParams.get(field.name) || ""}
|
||||||
onChange={() => navigate(true)}
|
onChange={() => navigate(true)}
|
||||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
className="h-8 rounded-md border border-input bg-background px-2 py-1 text-xs ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
{field.options?.map((opt) => (
|
{field.options?.map((opt) => (
|
||||||
<option key={opt.value} value={opt.value}>
|
<option key={opt.value} value={opt.value}>
|
||||||
@@ -103,28 +141,30 @@ export function LiveSearchForm({ fields, basePath, debounceMs = 300 }: LiveSearc
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
)
|
))}
|
||||||
)}
|
|
||||||
{hasFilters && (
|
{hasFilters && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => router.replace(basePath as any)}
|
onClick={() => router.replace(basePath as any)}
|
||||||
className="
|
className="
|
||||||
inline-flex items-center justify-center
|
inline-flex items-center gap-1
|
||||||
h-10 px-4
|
h-8 px-2.5
|
||||||
border border-input
|
text-xs font-medium
|
||||||
text-sm font-medium
|
|
||||||
text-muted-foreground
|
text-muted-foreground
|
||||||
bg-background
|
|
||||||
rounded-md
|
rounded-md
|
||||||
hover:bg-accent hover:text-accent-foreground
|
hover:bg-accent hover:text-accent-foreground
|
||||||
transition-colors duration-200
|
transition-colors duration-200
|
||||||
w-full sm:w-auto
|
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
|
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
{t("common.clear")}
|
{t("common.clear")}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { revalidatePath } from "next/cache";
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { listJobs, fetchLibraries, rebuildIndex, rebuildThumbnails, regenerateThumbnails, startMetadataBatch, startMetadataRefresh, IndexJobDto, LibraryDto } from "../../lib/api";
|
import { listJobs, fetchLibraries, rebuildIndex, rebuildThumbnails, regenerateThumbnails, startMetadataBatch, startMetadataRefresh, IndexJobDto, LibraryDto } from "../../lib/api";
|
||||||
import { JobsList } from "../components/JobsList";
|
import { JobsList } from "../components/JobsList";
|
||||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent, Button, FormField, FormSelect, FormRow } from "../components/ui";
|
import { Card, CardHeader, CardTitle, CardDescription, CardContent, FormField, FormSelect } from "../components/ui";
|
||||||
import { getServerTranslations } from "../../lib/i18n/server";
|
import { getServerTranslations } from "../../lib/i18n/server";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
@@ -85,8 +85,8 @@ export default async function JobsPage({ searchParams }: { searchParams: Promise
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<form>
|
<form>
|
||||||
<FormRow>
|
<div className="mb-6">
|
||||||
<FormField className="flex-1 max-w-xs">
|
<FormField className="max-w-xs">
|
||||||
<FormSelect name="library_id" defaultValue="">
|
<FormSelect name="library_id" defaultValue="">
|
||||||
<option value="">{t("jobs.allLibraries")}</option>
|
<option value="">{t("jobs.allLibraries")}</option>
|
||||||
{libraries.map((lib) => (
|
{libraries.map((lib) => (
|
||||||
@@ -94,123 +94,108 @@ export default async function JobsPage({ searchParams }: { searchParams: Promise
|
|||||||
))}
|
))}
|
||||||
</FormSelect>
|
</FormSelect>
|
||||||
</FormField>
|
</FormField>
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
<Button type="submit" formAction={triggerRebuild}>
|
|
||||||
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
|
||||||
</svg>
|
|
||||||
{t("jobs.rebuild")}
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" formAction={triggerFullRebuild} variant="warning">
|
|
||||||
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
|
||||||
</svg>
|
|
||||||
{t("jobs.fullRebuild")}
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" formAction={triggerThumbnailsRebuild} variant="secondary">
|
|
||||||
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
||||||
</svg>
|
|
||||||
{t("jobs.generateThumbnails")}
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" formAction={triggerThumbnailsRegenerate} variant="warning">
|
|
||||||
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
|
||||||
</svg>
|
|
||||||
{t("jobs.regenerateThumbnails")}
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" formAction={triggerMetadataBatch} variant="secondary">
|
|
||||||
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
||||||
</svg>
|
|
||||||
{t("jobs.batchMetadata")}
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" formAction={triggerMetadataRefresh} variant="secondary">
|
|
||||||
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
|
||||||
</svg>
|
|
||||||
{t("jobs.refreshMetadata")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</FormRow>
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
</form>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Job types legend */}
|
{/* Indexation group */}
|
||||||
<Card className="mb-6">
|
<div className="space-y-3">
|
||||||
<CardHeader>
|
<div className="flex items-center gap-2 text-sm font-semibold text-foreground">
|
||||||
<CardTitle className="text-base">{t("jobs.referenceTitle")}</CardTitle>
|
<svg className="w-4 h-4 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
</CardHeader>
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
||||||
<CardContent>
|
</svg>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
{t("jobs.groupIndexation")}
|
||||||
<div className="flex gap-3">
|
</div>
|
||||||
<div className="shrink-0 mt-0.5">
|
<div className="space-y-2">
|
||||||
<svg className="w-5 h-5 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<button type="submit" formAction={triggerRebuild}
|
||||||
|
className="w-full text-left rounded-lg border border-input bg-background p-3 hover:bg-accent/50 transition-colors group cursor-pointer">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<svg className="w-4 h-4 text-primary shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||||
</svg>
|
</svg>
|
||||||
|
<span className="font-medium text-sm text-foreground">{t("jobs.rebuild")}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<p className="text-xs text-muted-foreground mt-1 ml-6">{t("jobs.rebuildShort")}</p>
|
||||||
<span className="font-medium text-foreground">{t("jobs.rebuild")}</span>
|
</button>
|
||||||
<p className="text-muted-foreground text-xs mt-0.5" dangerouslySetInnerHTML={{ __html: t("jobs.rebuildDescription") }} />
|
<button type="submit" formAction={triggerFullRebuild}
|
||||||
</div>
|
className="w-full text-left rounded-lg border border-warning/30 bg-warning/5 p-3 hover:bg-warning/10 transition-colors group cursor-pointer">
|
||||||
</div>
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex gap-3">
|
<svg className="w-4 h-4 text-warning shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<div className="shrink-0 mt-0.5">
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||||
<svg className="w-5 h-5 text-warning" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
|
||||||
</svg>
|
</svg>
|
||||||
|
<span className="font-medium text-sm text-warning">{t("jobs.fullRebuild")}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<p className="text-xs text-muted-foreground mt-1 ml-6">{t("jobs.fullRebuildShort")}</p>
|
||||||
<span className="font-medium text-foreground">{t("jobs.fullRebuild")}</span>
|
</button>
|
||||||
<p className="text-muted-foreground text-xs mt-0.5" dangerouslySetInnerHTML={{ __html: t("jobs.fullRebuildDescription") }} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3">
|
|
||||||
<div className="shrink-0 mt-0.5">
|
{/* Thumbnails group */}
|
||||||
<svg className="w-5 h-5 text-muted-foreground" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center gap-2 text-sm font-semibold text-foreground">
|
||||||
|
<svg className="w-4 h-4 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||||
</svg>
|
</svg>
|
||||||
|
{t("jobs.groupThumbnails")}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<span className="font-medium text-foreground">{t("jobs.generateThumbnails")}</span>
|
<button type="submit" formAction={triggerThumbnailsRebuild}
|
||||||
<p className="text-muted-foreground text-xs mt-0.5" dangerouslySetInnerHTML={{ __html: t("jobs.generateThumbnailsDescription") }} />
|
className="w-full text-left rounded-lg border border-input bg-background p-3 hover:bg-accent/50 transition-colors group cursor-pointer">
|
||||||
</div>
|
<div className="flex items-center gap-2">
|
||||||
</div>
|
<svg className="w-4 h-4 text-primary shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<div className="flex gap-3">
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||||
<div className="shrink-0 mt-0.5">
|
|
||||||
<svg className="w-5 h-5 text-warning" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
|
||||||
</svg>
|
</svg>
|
||||||
|
<span className="font-medium text-sm text-foreground">{t("jobs.generateThumbnails")}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<p className="text-xs text-muted-foreground mt-1 ml-6">{t("jobs.generateThumbnailsShort")}</p>
|
||||||
<span className="font-medium text-foreground">{t("jobs.regenerateThumbnails")}</span>
|
</button>
|
||||||
<p className="text-muted-foreground text-xs mt-0.5" dangerouslySetInnerHTML={{ __html: t("jobs.regenerateThumbnailsDescription") }} />
|
<button type="submit" formAction={triggerThumbnailsRegenerate}
|
||||||
|
className="w-full text-left rounded-lg border border-warning/30 bg-warning/5 p-3 hover:bg-warning/10 transition-colors group cursor-pointer">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<svg className="w-4 h-4 text-warning shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||||
|
</svg>
|
||||||
|
<span className="font-medium text-sm text-warning">{t("jobs.regenerateThumbnails")}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1 ml-6">{t("jobs.regenerateThumbnailsShort")}</p>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3">
|
|
||||||
<div className="shrink-0 mt-0.5">
|
{/* Metadata group */}
|
||||||
<svg className="w-5 h-5 text-muted-foreground" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center gap-2 text-sm font-semibold text-foreground">
|
||||||
|
<svg className="w-4 h-4 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="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" />
|
||||||
|
</svg>
|
||||||
|
{t("jobs.groupMetadata")}
|
||||||
|
<span className="text-xs font-normal text-muted-foreground">({t("jobs.requiresLibrary")})</span>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<button type="submit" formAction={triggerMetadataBatch}
|
||||||
|
className="w-full text-left rounded-lg border border-input bg-background p-3 hover:bg-accent/50 transition-colors group cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-background">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<svg className="w-4 h-4 text-primary shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
|
<span className="font-medium text-sm text-foreground">{t("jobs.batchMetadata")}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<p className="text-xs text-muted-foreground mt-1 ml-6">{t("jobs.batchMetadataShort")}</p>
|
||||||
<span className="font-medium text-foreground">{t("jobs.batchMetadata")}</span>
|
</button>
|
||||||
<p className="text-muted-foreground text-xs mt-0.5" dangerouslySetInnerHTML={{ __html: t("jobs.batchMetadataDescription") }} />
|
<button type="submit" formAction={triggerMetadataRefresh}
|
||||||
</div>
|
className="w-full text-left rounded-lg border border-input bg-background p-3 hover:bg-accent/50 transition-colors group cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-background">
|
||||||
</div>
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex gap-3">
|
<svg className="w-4 h-4 text-primary shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<div className="shrink-0 mt-0.5">
|
|
||||||
<svg className="w-5 h-5 text-muted-foreground" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||||
</svg>
|
</svg>
|
||||||
|
<span className="font-medium text-sm text-foreground">{t("jobs.refreshMetadata")}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<p className="text-xs text-muted-foreground mt-1 ml-6">{t("jobs.refreshMetadataShort")}</p>
|
||||||
<span className="font-medium text-foreground">{t("jobs.refreshMetadata")}</span>
|
</button>
|
||||||
<p className="text-muted-foreground text-xs mt-0.5" dangerouslySetInnerHTML={{ __html: t("jobs.refreshMetadataDescription") }} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
@@ -99,13 +99,13 @@ export default async function SeriesPage({
|
|||||||
<LiveSearchForm
|
<LiveSearchForm
|
||||||
basePath="/series"
|
basePath="/series"
|
||||||
fields={[
|
fields={[
|
||||||
{ name: "q", type: "text", label: t("common.search"), placeholder: t("series.searchPlaceholder"), className: "flex-1 w-full" },
|
{ name: "q", type: "text", label: t("common.search"), placeholder: t("series.searchPlaceholder") },
|
||||||
{ name: "library", type: "select", label: t("books.library"), options: libraryOptions, className: "w-full sm:w-44" },
|
{ name: "library", type: "select", label: t("books.library"), options: libraryOptions },
|
||||||
{ name: "status", type: "select", label: t("series.reading"), options: statusOptions, className: "w-full sm:w-32" },
|
{ name: "status", type: "select", label: t("series.reading"), options: statusOptions },
|
||||||
{ name: "series_status", type: "select", label: t("editSeries.status"), options: seriesStatusOptions, className: "w-full sm:w-36" },
|
{ name: "series_status", type: "select", label: t("editSeries.status"), options: seriesStatusOptions },
|
||||||
{ name: "has_missing", type: "select", label: t("series.missing"), options: missingOptions, className: "w-full sm:w-36" },
|
{ name: "has_missing", type: "select", label: t("series.missing"), options: missingOptions },
|
||||||
{ name: "metadata_provider", type: "select", label: t("series.metadata"), options: metadataOptions, className: "w-full sm:w-36" },
|
{ name: "metadata_provider", type: "select", label: t("series.metadata"), options: metadataOptions },
|
||||||
{ name: "sort", type: "select", label: t("books.sort"), options: sortOptions, className: "w-full sm:w-32" },
|
{ name: "sort", type: "select", label: t("books.sort"), options: sortOptions },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -176,6 +176,16 @@ const en: Record<TranslationKey, string> = {
|
|||||||
"jobs.refreshMetadata": "Refresh metadata",
|
"jobs.refreshMetadata": "Refresh metadata",
|
||||||
"jobs.refreshMetadataDescription": "Refreshes metadata for all series already linked to an external provider. Re-downloads information from the provider and updates series and books in the database (respecting locked fields). Series without an approved link are ignored. <strong>Requires a specific library</strong> (does not work on \"All libraries\").",
|
"jobs.refreshMetadataDescription": "Refreshes metadata for all series already linked to an external provider. Re-downloads information from the provider and updates series and books in the database (respecting locked fields). Series without an approved link are ignored. <strong>Requires a specific library</strong> (does not work on \"All libraries\").",
|
||||||
"jobs.referenceTitle": "Job types reference",
|
"jobs.referenceTitle": "Job types reference",
|
||||||
|
"jobs.groupIndexation": "Indexation",
|
||||||
|
"jobs.groupThumbnails": "Thumbnails",
|
||||||
|
"jobs.groupMetadata": "Metadata",
|
||||||
|
"jobs.requiresLibrary": "Requires a specific library",
|
||||||
|
"jobs.rebuildShort": "Scan new & modified files",
|
||||||
|
"jobs.fullRebuildShort": "Delete all & re-scan from scratch",
|
||||||
|
"jobs.generateThumbnailsShort": "Missing thumbnails only",
|
||||||
|
"jobs.regenerateThumbnailsShort": "Recreate all thumbnails",
|
||||||
|
"jobs.batchMetadataShort": "Auto-match unlinked series",
|
||||||
|
"jobs.refreshMetadataShort": "Update existing linked series",
|
||||||
"jobs.rebuildDescription": "Incremental scan: detects files added, modified, or deleted since the last scan, indexes them, and generates missing thumbnails. Existing unmodified data is preserved. This is the most common and fastest action.",
|
"jobs.rebuildDescription": "Incremental scan: detects files added, modified, or deleted since the last scan, indexes them, and generates missing thumbnails. Existing unmodified data is preserved. This is the most common and fastest action.",
|
||||||
"jobs.fullRebuildDescription": "Deletes all indexed data (books, series, thumbnails) then performs a full scan from scratch. Useful if the database is out of sync or corrupted. Long and destructive operation: reading statuses and manual metadata will be lost.",
|
"jobs.fullRebuildDescription": "Deletes all indexed data (books, series, thumbnails) then performs a full scan from scratch. Useful if the database is out of sync or corrupted. Long and destructive operation: reading statuses and manual metadata will be lost.",
|
||||||
"jobs.generateThumbnailsDescription": "Generates thumbnails only for books that don't have one yet. Existing thumbnails are not affected. Useful after an import or if some thumbnails are missing.",
|
"jobs.generateThumbnailsDescription": "Generates thumbnails only for books that don't have one yet. Existing thumbnails are not affected. Useful after an import or if some thumbnails are missing.",
|
||||||
|
|||||||
@@ -174,6 +174,16 @@ const fr = {
|
|||||||
"jobs.refreshMetadata": "Rafraîchir métadonnées",
|
"jobs.refreshMetadata": "Rafraîchir métadonnées",
|
||||||
"jobs.refreshMetadataDescription": "Rafraîchit les métadonnées de toutes les séries déjà liées à un fournisseur externe. Re-télécharge les informations depuis le fournisseur et met à jour les séries et livres en base (en respectant les champs verrouillés). Les séries sans lien approuvé sont ignorées. <strong>Requiert une bibliothèque spécifique</strong> (ne fonctionne pas sur \u00ab Toutes les bibliothèques \u00bb).",
|
"jobs.refreshMetadataDescription": "Rafraîchit les métadonnées de toutes les séries déjà liées à un fournisseur externe. Re-télécharge les informations depuis le fournisseur et met à jour les séries et livres en base (en respectant les champs verrouillés). Les séries sans lien approuvé sont ignorées. <strong>Requiert une bibliothèque spécifique</strong> (ne fonctionne pas sur \u00ab Toutes les bibliothèques \u00bb).",
|
||||||
"jobs.referenceTitle": "Référence des types de tâches",
|
"jobs.referenceTitle": "Référence des types de tâches",
|
||||||
|
"jobs.groupIndexation": "Indexation",
|
||||||
|
"jobs.groupThumbnails": "Miniatures",
|
||||||
|
"jobs.groupMetadata": "Métadonnées",
|
||||||
|
"jobs.requiresLibrary": "Requiert une bibliothèque spécifique",
|
||||||
|
"jobs.rebuildShort": "Scanner les fichiers nouveaux et modifiés",
|
||||||
|
"jobs.fullRebuildShort": "Tout supprimer et re-scanner depuis zéro",
|
||||||
|
"jobs.generateThumbnailsShort": "Miniatures manquantes uniquement",
|
||||||
|
"jobs.regenerateThumbnailsShort": "Recréer toutes les miniatures",
|
||||||
|
"jobs.batchMetadataShort": "Lier automatiquement les séries non liées",
|
||||||
|
"jobs.refreshMetadataShort": "Mettre à jour les séries déjà liées",
|
||||||
"jobs.rebuildDescription": "Scan incrémental : détecte les fichiers ajoutés, modifiés ou supprimés depuis le dernier scan, les indexe et génère les miniatures manquantes. Les données existantes non modifiées sont conservées. C'est l'action la plus courante et la plus rapide.",
|
"jobs.rebuildDescription": "Scan incrémental : détecte les fichiers ajoutés, modifiés ou supprimés depuis le dernier scan, les indexe et génère les miniatures manquantes. Les données existantes non modifiées sont conservées. C'est l'action la plus courante et la plus rapide.",
|
||||||
"jobs.fullRebuildDescription": "Supprime toutes les données indexées (livres, séries, miniatures) puis effectue un scan complet depuis zéro. Utile si la base de données est désynchronisée ou corrompue. Opération longue et destructive : les statuts de lecture et les métadonnées manuelles seront perdus.",
|
"jobs.fullRebuildDescription": "Supprime toutes les données indexées (livres, séries, miniatures) puis effectue un scan complet depuis zéro. Utile si la base de données est désynchronisée ou corrompue. Opération longue et destructive : les statuts de lecture et les métadonnées manuelles seront perdus.",
|
||||||
"jobs.generateThumbnailsDescription": "Génère les miniatures uniquement pour les livres qui n'en ont pas encore. Les miniatures existantes ne sont pas touchées. Utile après un import ou si certaines miniatures sont manquantes.",
|
"jobs.generateThumbnailsDescription": "Génère les miniatures uniquement pour les livres qui n'en ont pas encore. Les miniatures existantes ne sont pas touchées. Utile après un import ou si certaines miniatures sont manquantes.",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "stripstream-backoffice",
|
"name": "stripstream-backoffice",
|
||||||
"version": "1.7.0",
|
"version": "1.8.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -p 7082",
|
"dev": "next dev -p 7082",
|
||||||
|
|||||||
Reference in New Issue
Block a user