Compare commits
5 Commits
b9e54cbfd8
...
41c77fca2e
| Author | SHA1 | Date | |
|---|---|---|---|
| 41c77fca2e | |||
| 49621f3fb1 | |||
| 6df743b2e6 | |||
| edfefc0128 | |||
| b0185abefe |
8
Cargo.lock
generated
8
Cargo.lock
generated
@@ -64,7 +64,7 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "api"
|
name = "api"
|
||||||
version = "1.19.1"
|
version = "1.20.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argon2",
|
"argon2",
|
||||||
@@ -1232,7 +1232,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexer"
|
name = "indexer"
|
||||||
version = "1.19.1"
|
version = "1.20.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -1771,7 +1771,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parsers"
|
name = "parsers"
|
||||||
version = "1.19.1"
|
version = "1.20.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"flate2",
|
"flate2",
|
||||||
@@ -2906,7 +2906,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stripstream-core"
|
name = "stripstream-core"
|
||||||
version = "1.19.1"
|
version = "1.20.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ resolver = "2"
|
|||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
version = "1.19.1"
|
version = "1.20.0"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export default async function AuthorDetailPage({
|
|||||||
alt={s.name}
|
alt={s.name}
|
||||||
fill
|
fill
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
unoptimized
|
sizes="(max-width: 640px) 50vw, (max-width: 768px) 33vw, (max-width: 1024px) 25vw, 16vw"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
|
|||||||
@@ -2,11 +2,15 @@ import { fetchLibraries, getBookCoverUrl, BookDto, apiFetch, ReadingStatus } fro
|
|||||||
import { BookPreview } from "../../components/BookPreview";
|
import { BookPreview } from "../../components/BookPreview";
|
||||||
import { ConvertButton } from "../../components/ConvertButton";
|
import { ConvertButton } from "../../components/ConvertButton";
|
||||||
import { MarkBookReadButton } from "../../components/MarkBookReadButton";
|
import { MarkBookReadButton } from "../../components/MarkBookReadButton";
|
||||||
import { EditBookForm } from "../../components/EditBookForm";
|
import nextDynamic from "next/dynamic";
|
||||||
import { SafeHtml } from "../../components/SafeHtml";
|
import { SafeHtml } from "../../components/SafeHtml";
|
||||||
import { getServerTranslations } from "../../../lib/i18n/server";
|
import { getServerTranslations } from "../../../lib/i18n/server";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
|
const EditBookForm = nextDynamic(
|
||||||
|
() => import("../../components/EditBookForm").then(m => m.EditBookForm)
|
||||||
|
);
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
@@ -95,7 +99,7 @@ export default async function BookDetailPage({
|
|||||||
alt={t("bookDetail.coverOf", { title: book.title })}
|
alt={t("bookDetail.coverOf", { title: book.title })}
|
||||||
fill
|
fill
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
unoptimized
|
sizes="192px"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ export default async function BooksPage({
|
|||||||
alt={t("books.coverOf", { name: s.name })}
|
alt={t("books.coverOf", { name: s.name })}
|
||||||
fill
|
fill
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
unoptimized
|
sizes="(max-width: 640px) 50vw, (max-width: 768px) 33vw, (max-width: 1024px) 25vw, 16vw"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { memo, useState } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { BookDto, ReadingStatus } from "../../lib/api";
|
import { BookDto, ReadingStatus } from "../../lib/api";
|
||||||
@@ -17,7 +17,7 @@ interface BookCardProps {
|
|||||||
readingStatus?: ReadingStatus;
|
readingStatus?: ReadingStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
function BookImage({ src, alt }: { src: string; alt: string }) {
|
const BookImage = memo(function BookImage({ src, alt }: { src: string; alt: string }) {
|
||||||
const [isLoaded, setIsLoaded] = useState(false);
|
const [isLoaded, setIsLoaded] = useState(false);
|
||||||
const [hasError, setHasError] = useState(false);
|
const [hasError, setHasError] = useState(false);
|
||||||
|
|
||||||
@@ -51,13 +51,12 @@ function BookImage({ src, alt }: { src: string; alt: string }) {
|
|||||||
sizes="(max-width: 640px) 50vw, (max-width: 768px) 33vw, (max-width: 1024px) 25vw, 16vw"
|
sizes="(max-width: 640px) 50vw, (max-width: 768px) 33vw, (max-width: 1024px) 25vw, 16vw"
|
||||||
onLoad={() => setIsLoaded(true)}
|
onLoad={() => setIsLoaded(true)}
|
||||||
onError={() => setHasError(true)}
|
onError={() => setHasError(true)}
|
||||||
unoptimized
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
export function BookCard({ book, readingStatus }: BookCardProps) {
|
export const BookCard = memo(function BookCard({ book, readingStatus }: BookCardProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const coverUrl = book.coverUrl || `/api/books/${book.id}/thumbnail`;
|
const coverUrl = book.coverUrl || `/api/books/${book.id}/thumbnail`;
|
||||||
const status = readingStatus ?? book.reading_status;
|
const status = readingStatus ?? book.reading_status;
|
||||||
@@ -129,7 +128,7 @@ export function BookCard({ book, readingStatus }: BookCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
interface BooksGridProps {
|
interface BooksGridProps {
|
||||||
books: (BookDto & { coverUrl?: string })[];
|
books: (BookDto & { coverUrl?: string })[];
|
||||||
|
|||||||
@@ -54,21 +54,46 @@ export function JobsIndicator() {
|
|||||||
const [popinStyle, setPopinStyle] = useState<React.CSSProperties>({});
|
const [popinStyle, setPopinStyle] = useState<React.CSSProperties>({});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
let intervalId: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
const fetchActiveJobs = async () => {
|
const fetchActiveJobs = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/jobs/active");
|
const response = await fetch("/api/jobs/active");
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const jobs = await response.json();
|
const jobs: Job[] = await response.json();
|
||||||
setActiveJobs(jobs);
|
setActiveJobs(jobs);
|
||||||
|
// Adapt polling interval: 2s when jobs are active, 30s when idle
|
||||||
|
restartInterval(jobs.length > 0 ? 2000 : 30000);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch jobs:", error);
|
console.error("Failed to fetch jobs:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const restartInterval = (ms: number) => {
|
||||||
|
if (intervalId !== null) clearInterval(intervalId);
|
||||||
|
intervalId = setInterval(fetchActiveJobs, ms);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVisibilityChange = () => {
|
||||||
|
if (document.hidden) {
|
||||||
|
if (intervalId !== null) {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
intervalId = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Refetch immediately when tab becomes visible, then resume polling
|
||||||
fetchActiveJobs();
|
fetchActiveJobs();
|
||||||
const interval = setInterval(fetchActiveJobs, 2000);
|
}
|
||||||
return () => clearInterval(interval);
|
};
|
||||||
|
|
||||||
|
fetchActiveJobs();
|
||||||
|
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (intervalId !== null) clearInterval(intervalId);
|
||||||
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Position the popin relative to the button
|
// Position the popin relative to the button
|
||||||
|
|||||||
@@ -2,13 +2,21 @@ import { fetchLibraries, fetchBooks, fetchSeriesMetadata, getBookCoverUrl, getMe
|
|||||||
import { BooksGrid, EmptyState } from "../../../../components/BookCard";
|
import { BooksGrid, EmptyState } from "../../../../components/BookCard";
|
||||||
import { MarkSeriesReadButton } from "../../../../components/MarkSeriesReadButton";
|
import { MarkSeriesReadButton } from "../../../../components/MarkSeriesReadButton";
|
||||||
import { MarkBookReadButton } from "../../../../components/MarkBookReadButton";
|
import { MarkBookReadButton } from "../../../../components/MarkBookReadButton";
|
||||||
import { EditSeriesForm } from "../../../../components/EditSeriesForm";
|
import nextDynamic from "next/dynamic";
|
||||||
import { MetadataSearchModal } from "../../../../components/MetadataSearchModal";
|
|
||||||
import { ProwlarrSearchModal } from "../../../../components/ProwlarrSearchModal";
|
|
||||||
import { OffsetPagination } from "../../../../components/ui";
|
import { OffsetPagination } from "../../../../components/ui";
|
||||||
import { SafeHtml } from "../../../../components/SafeHtml";
|
import { SafeHtml } from "../../../../components/SafeHtml";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
|
const EditSeriesForm = nextDynamic(
|
||||||
|
() => import("../../../../components/EditSeriesForm").then(m => m.EditSeriesForm)
|
||||||
|
);
|
||||||
|
const MetadataSearchModal = nextDynamic(
|
||||||
|
() => import("../../../../components/MetadataSearchModal").then(m => m.MetadataSearchModal)
|
||||||
|
);
|
||||||
|
const ProwlarrSearchModal = nextDynamic(
|
||||||
|
() => import("../../../../components/ProwlarrSearchModal").then(m => m.ProwlarrSearchModal)
|
||||||
|
);
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { getServerTranslations } from "../../../../../lib/i18n/server";
|
import { getServerTranslations } from "../../../../../lib/i18n/server";
|
||||||
|
|
||||||
@@ -94,7 +102,7 @@ export default async function SeriesDetailPage({
|
|||||||
alt={t("books.coverOf", { name: displayName })}
|
alt={t("books.coverOf", { name: displayName })}
|
||||||
fill
|
fill
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
unoptimized
|
sizes="160px"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export default async function LibrarySeriesPage({
|
|||||||
alt={t("books.coverOf", { name: s.name })}
|
alt={t("books.coverOf", { name: s.name })}
|
||||||
fill
|
fill
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
unoptimized
|
sizes="(max-width: 640px) 50vw, (max-width: 768px) 33vw, (max-width: 1024px) 25vw, 20vw"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { listFolders, createLibrary, deleteLibrary, fetchLibraries, getBookCoverUrl, LibraryDto, FolderItem } from "../../lib/api";
|
import { listFolders, createLibrary, deleteLibrary, fetchLibraries, getBookCoverUrl, LibraryDto, FolderItem } from "../../lib/api";
|
||||||
import type { TranslationKey } from "../../lib/i18n/fr";
|
import type { TranslationKey } from "../../lib/i18n/fr";
|
||||||
@@ -88,10 +89,12 @@ export default async function LibrariesPage() {
|
|||||||
{/* Thumbnail fan */}
|
{/* Thumbnail fan */}
|
||||||
{thumbnails.length > 0 ? (
|
{thumbnails.length > 0 ? (
|
||||||
<Link href={`/libraries/${lib.id}/series`} className="block relative h-48 overflow-hidden bg-muted/10">
|
<Link href={`/libraries/${lib.id}/series`} className="block relative h-48 overflow-hidden bg-muted/10">
|
||||||
<img
|
<Image
|
||||||
src={thumbnails[0]}
|
src={thumbnails[0]}
|
||||||
alt=""
|
alt=""
|
||||||
className="absolute inset-0 w-full h-full object-cover blur-xl scale-110 opacity-40"
|
fill
|
||||||
|
className="object-cover blur-xl scale-110 opacity-40"
|
||||||
|
sizes="(max-width: 768px) 100vw, 33vw"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 flex items-end justify-center">
|
<div className="absolute inset-0 flex items-end justify-center">
|
||||||
@@ -104,17 +107,20 @@ export default async function LibrariesPage() {
|
|||||||
const cx = Math.cos(rad) * radius;
|
const cx = Math.cos(rad) * radius;
|
||||||
const cy = Math.sin(rad) * radius;
|
const cy = Math.sin(rad) * radius;
|
||||||
return (
|
return (
|
||||||
<img
|
<Image
|
||||||
key={i}
|
key={i}
|
||||||
src={url}
|
src={url}
|
||||||
alt=""
|
alt=""
|
||||||
className="absolute w-24 h-36 object-cover shadow-lg"
|
width={96}
|
||||||
|
height={144}
|
||||||
|
className="absolute object-cover shadow-lg"
|
||||||
style={{
|
style={{
|
||||||
transform: `translate(${cx}px, ${cy}px) rotate(${angle}deg)`,
|
transform: `translate(${cx}px, ${cy}px) rotate(${angle}deg)`,
|
||||||
transformOrigin: 'bottom center',
|
transformOrigin: 'bottom center',
|
||||||
zIndex: count - Math.abs(Math.round(i - mid)),
|
zIndex: count - Math.abs(Math.round(i - mid)),
|
||||||
bottom: '-185px',
|
bottom: '-185px',
|
||||||
}}
|
}}
|
||||||
|
sizes="96px"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ export default async function SeriesPage({
|
|||||||
alt={t("books.coverOf", { name: s.name })}
|
alt={t("books.coverOf", { name: s.name })}
|
||||||
fill
|
fill
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
unoptimized
|
sizes="(max-width: 640px) 50vw, (max-width: 768px) 33vw, (max-width: 1024px) 25vw, 16vw"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
typedRoutes: true
|
typedRoutes: true,
|
||||||
|
images: {
|
||||||
|
minimumCacheTTL: 86400,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "stripstream-backoffice",
|
"name": "stripstream-backoffice",
|
||||||
"version": "1.19.1",
|
"version": "1.20.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -p 7082",
|
"dev": "next dev -p 7082",
|
||||||
|
|||||||
Reference in New Issue
Block a user