feat: eigene Sammlung — „hab ich" mit Quelle
Streaming-Verfügbarkeit beantwortet nur die halbe Frage: manche Filme laufen nirgends mehr, die hat man gekauft und liegen zu Hause — in Jellyfin, Plex, Emby, auf dem NAS oder als Scheibe. Bewusst getrennt vom Feld „wo schaue ich das": das eine sagt, wo ein Titel gerade läuft, und das ändert sich ständig; das andere sagt, was einem gehört, und das bleibt. Auf der Detailseite steht die Sammlung deshalb vor der Verfügbarkeit — was man hat, ist die verlässlichere Antwort. Durchgezogen bis in Filter, Export, CSV-Import und REST-API.
This commit is contained in:
@@ -114,6 +114,8 @@ const detailsSchema = z.object({
|
||||
notes: z.string().max(5000).nullable().optional(),
|
||||
whereToWatch: z.string().max(120).nullable().optional(),
|
||||
favorite: z.boolean().optional(),
|
||||
owned: z.boolean().optional(),
|
||||
ownedSource: z.string().max(80).nullable().optional(),
|
||||
});
|
||||
|
||||
export async function updateEntryAction(entryId: string, data: unknown) {
|
||||
|
||||
@@ -36,6 +36,8 @@ export const GET = withApi(async (_request, user, context) => {
|
||||
rating: entry.rating,
|
||||
notes: entry.notes,
|
||||
whereToWatch: entry.whereToWatch,
|
||||
owned: entry.owned,
|
||||
ownedSource: entry.ownedSource,
|
||||
favorite: entry.favorite,
|
||||
currentRun: entry.currentRun,
|
||||
tags: entry.tags.map((link) => link.tag.name),
|
||||
@@ -58,6 +60,8 @@ const patchSchema = z.object({
|
||||
notes: z.string().max(5000).nullable().optional(),
|
||||
whereToWatch: z.string().max(120).nullable().optional(),
|
||||
favorite: z.boolean().optional(),
|
||||
owned: z.boolean().optional(),
|
||||
ownedSource: z.string().max(80).nullable().optional(),
|
||||
});
|
||||
|
||||
/** PATCH /api/v1/bibliothek/{id} — Status und Angaben ändern. */
|
||||
|
||||
@@ -22,6 +22,7 @@ export const GET = withApi(async (request, user) => {
|
||||
genre: url.searchParams.get("genre") ?? undefined,
|
||||
tag: url.searchParams.get("tag") ?? undefined,
|
||||
provider: url.searchParams.get("dienst") ?? undefined,
|
||||
owned: url.searchParams.get("sammlung") === "1" ? true : undefined,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -73,6 +73,12 @@ export function GET() {
|
||||
{ name: "genre", in: "query", schema: { type: "string" } },
|
||||
{ name: "tag", in: "query", schema: { type: "string" } },
|
||||
{ name: "dienst", in: "query", schema: { type: "string" } },
|
||||
{
|
||||
name: "sammlung",
|
||||
in: "query",
|
||||
schema: { type: "string", enum: ["1"] },
|
||||
description: "Nur Titel, die in der eigenen Sammlung liegen.",
|
||||
},
|
||||
{ name: "limit", in: "query", schema: { type: "integer", default: 50, maximum: 200 } },
|
||||
{ name: "offset", in: "query", schema: { type: "integer", default: 0 } },
|
||||
],
|
||||
@@ -123,6 +129,15 @@ export function GET() {
|
||||
notes: { type: "string", nullable: true },
|
||||
whereToWatch: { type: "string", nullable: true },
|
||||
favorite: { type: "boolean" },
|
||||
owned: {
|
||||
type: "boolean",
|
||||
description: "Liegt der Titel in der eigenen Sammlung?",
|
||||
},
|
||||
ownedSource: {
|
||||
type: "string",
|
||||
nullable: true,
|
||||
description: "Wo genau — Jellyfin, Plex, Blu-ray …",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -29,6 +29,7 @@ export default async function LibraryPage({
|
||||
genre: params.genre || undefined,
|
||||
tag: params.tag || undefined,
|
||||
provider: params.dienst || undefined,
|
||||
owned: params.sammlung === "1" ? true : undefined,
|
||||
sort: SORTS.includes(params.sortierung ?? "") ? (params.sortierung as LibrarySort) : undefined,
|
||||
};
|
||||
|
||||
|
||||
@@ -120,6 +120,19 @@ export default async function TitlePage({ params }: { params: Params }) {
|
||||
{year ? ` · ${year}` : ""}
|
||||
{title.status ? ` · ${title.status}` : ""}
|
||||
</p>
|
||||
{entry?.owned && (
|
||||
<span
|
||||
className="rounded-full px-2.5 py-0.5 text-xs font-medium text-[var(--fl-on-accent)]"
|
||||
style={{ backgroundImage: "var(--fl-gradient)" }}
|
||||
title={
|
||||
entry.ownedSource
|
||||
? `In deiner Sammlung: ${entry.ownedSource}`
|
||||
: "In deiner Sammlung"
|
||||
}
|
||||
>
|
||||
Hab ich{entry.ownedSource ? ` · ${entry.ownedSource}` : ""}
|
||||
</span>
|
||||
)}
|
||||
{entry && (
|
||||
<span
|
||||
className="rounded-full px-2.5 py-0.5 text-xs font-medium"
|
||||
@@ -225,6 +238,7 @@ export default async function TitlePage({ params }: { params: Params }) {
|
||||
<SeasonList seasons={seasons} entryId={entry.id} watchedIds={[...watchedIds]} />
|
||||
) : (
|
||||
<div className="fl-card p-4">
|
||||
<OwnedNotice owned={entry.owned} source={entry.ownedSource} />
|
||||
<ProviderList groups={providerGroups} region={user.region} />
|
||||
</div>
|
||||
)}
|
||||
@@ -245,6 +259,8 @@ export default async function TitlePage({ params }: { params: Params }) {
|
||||
whereToWatch={entry.whereToWatch}
|
||||
tags={entry.tags.map((link) => link.tag.name)}
|
||||
suggestion={suggestion}
|
||||
owned={entry.owned}
|
||||
ownedSource={entry.ownedSource}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -253,6 +269,7 @@ export default async function TitlePage({ params }: { params: Params }) {
|
||||
<h2 className="mb-3 text-sm font-semibold uppercase tracking-wide text-[var(--fl-faint)]">
|
||||
Wo läuft das?
|
||||
</h2>
|
||||
<OwnedNotice owned={entry.owned} source={entry.ownedSource} />
|
||||
<ProviderList groups={providerGroups} region={user.region} />
|
||||
</div>
|
||||
)}
|
||||
@@ -299,6 +316,34 @@ export default async function TitlePage({ params }: { params: Params }) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hinweis auf die eigene Sammlung — steht bewusst **vor** der
|
||||
* Streaming-Verfügbarkeit: was man hat, ist die verlässlichere Antwort auf
|
||||
* „wo kann ich das schauen".
|
||||
*/
|
||||
function OwnedNotice({ owned, source }: { owned: boolean; source: string | null }) {
|
||||
if (!owned) return null;
|
||||
|
||||
return (
|
||||
<p
|
||||
className="mb-3 flex items-center gap-2 rounded-xl border px-3 py-2 text-sm"
|
||||
style={{
|
||||
borderColor: "color-mix(in oklab, var(--fl-success) 45%, transparent)",
|
||||
backgroundColor: "color-mix(in oklab, var(--fl-success) 10%, transparent)",
|
||||
color: "var(--fl-success)",
|
||||
}}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" className="h-4 w-4 shrink-0" fill="none" stroke="currentColor" strokeWidth="2.5" aria-hidden>
|
||||
<path d="m5 13 4.5 4.5L19 7" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
<span>
|
||||
In deiner Sammlung
|
||||
{source ? <span className="font-medium"> · {source}</span> : null}
|
||||
</span>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
function Fact({ label, children }: { label: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState, useTransition } from "react";
|
||||
import { useRef, useState, useTransition } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { clsx } from "clsx";
|
||||
import { setTagsAction, updateEntryAction } from "@/app/actions/library";
|
||||
import { useToasts } from "@/components/Toasts";
|
||||
|
||||
@@ -17,6 +18,8 @@ export function EntryNotes({
|
||||
whereToWatch,
|
||||
tags,
|
||||
suggestion,
|
||||
owned,
|
||||
ownedSource,
|
||||
}: {
|
||||
entryId: string;
|
||||
notes: string | null;
|
||||
@@ -24,9 +27,12 @@ export function EntryNotes({
|
||||
tags: string[];
|
||||
/** Vorschlag aus der Streaming-Verfügbarkeit. */
|
||||
suggestion: string | null;
|
||||
owned: boolean;
|
||||
ownedSource: string | null;
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<OwnedEditor entryId={entryId} owned={owned} source={ownedSource} />
|
||||
<WhereToWatch entryId={entryId} value={whereToWatch} suggestion={suggestion} />
|
||||
<TagEditor entryId={entryId} tags={tags} />
|
||||
<Notes entryId={entryId} value={notes} />
|
||||
@@ -34,6 +40,94 @@ export function EntryNotes({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* „Hab ich" — der Titel liegt in der eigenen Sammlung.
|
||||
*
|
||||
* Bewusst getrennt von „wo schaue ich das": Streaming-Verfügbarkeit kommt und
|
||||
* geht, der gekaufte Film auf dem Heimserver bleibt. Genau die Titel, die
|
||||
* nirgends mehr laufen, will man ja wiederfinden.
|
||||
*/
|
||||
function OwnedEditor({
|
||||
entryId,
|
||||
owned,
|
||||
source,
|
||||
}: {
|
||||
entryId: string;
|
||||
owned: boolean;
|
||||
source: string | null;
|
||||
}) {
|
||||
const save = useAutosave(entryId);
|
||||
const [isOwned, setIsOwned] = useState(owned);
|
||||
const [text, setText] = useState(source ?? "");
|
||||
const initial = useRef(source ?? "");
|
||||
|
||||
return (
|
||||
<div className="space-y-2 rounded-xl border border-[var(--fl-border)] p-3">
|
||||
<label className="flex cursor-pointer items-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isOwned}
|
||||
onChange={(event) => {
|
||||
setIsOwned(event.target.checked);
|
||||
save({ owned: event.target.checked });
|
||||
}}
|
||||
className="sr-only"
|
||||
/>
|
||||
<span
|
||||
aria-hidden
|
||||
className={clsx(
|
||||
"mt-0.5 grid h-5 w-5 shrink-0 place-items-center rounded-md border transition-all duration-200",
|
||||
isOwned ? "border-transparent" : "border-[var(--fl-border-strong)]",
|
||||
)}
|
||||
style={isOwned ? { backgroundImage: "var(--fl-gradient)" } : undefined}
|
||||
>
|
||||
{isOwned && (
|
||||
<svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="var(--fl-on-accent)" strokeWidth="3.5">
|
||||
<path d="m5 13 4.5 4.5L19 7" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)}
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
<span className="block text-sm font-medium">Hab ich</span>
|
||||
<span className="block text-xs text-[var(--fl-faint)]">
|
||||
Liegt in der eigenen Sammlung — unabhängig davon, ob es gerade irgendwo läuft.
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
{isOwned && (
|
||||
<div className="pl-8">
|
||||
<label htmlFor="ownedSource" className="sr-only">
|
||||
Wo liegt der Titel?
|
||||
</label>
|
||||
<input
|
||||
id="ownedSource"
|
||||
value={text}
|
||||
onChange={(event) => setText(event.target.value)}
|
||||
onBlur={() => {
|
||||
if (text === initial.current) return;
|
||||
initial.current = text;
|
||||
save({ ownedSource: text });
|
||||
}}
|
||||
list="fl-owned-quellen"
|
||||
placeholder="Jellyfin, Plex, NAS, Blu-ray …"
|
||||
className="fl-input py-1.5 text-sm"
|
||||
/>
|
||||
<datalist id="fl-owned-quellen">
|
||||
<option value="Jellyfin" />
|
||||
<option value="Plex" />
|
||||
<option value="Emby" />
|
||||
<option value="NAS" />
|
||||
<option value="Blu-ray" />
|
||||
<option value="DVD" />
|
||||
<option value="Gekauft (digital)" />
|
||||
</datalist>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useAutosave(entryId: string) {
|
||||
const { report } = useToasts();
|
||||
const router = useRouter();
|
||||
|
||||
@@ -19,15 +19,31 @@ export function LibraryCard({ item }: { item: LibraryItem }) {
|
||||
<div className="relative">
|
||||
<Poster path={item.posterPath} alt={item.title} size="w342" />
|
||||
|
||||
<span
|
||||
className="absolute left-2 top-2 rounded-full px-2 py-0.5 text-[0.65rem] font-semibold backdrop-blur-md"
|
||||
style={{
|
||||
color: STATUS_COLOR[status],
|
||||
backgroundColor: `color-mix(in oklab, var(--fl-bg) 72%, transparent)`,
|
||||
boxShadow: `inset 0 0 0 1px color-mix(in oklab, ${STATUS_COLOR[status]} 45%, transparent)`,
|
||||
}}
|
||||
>
|
||||
{STATUS_LABEL[status]}
|
||||
{/* Status und Sammlung teilen sich die obere Zeile — unten sitzt bei
|
||||
den Postern oft der Titel, dort würden sie ihn verdecken. */}
|
||||
<span className="absolute left-2 top-2 flex flex-wrap gap-1">
|
||||
<span
|
||||
className="rounded-full px-2 py-0.5 text-[0.65rem] font-semibold backdrop-blur-md"
|
||||
style={{
|
||||
color: STATUS_COLOR[status],
|
||||
backgroundColor: `color-mix(in oklab, var(--fl-bg) 72%, transparent)`,
|
||||
boxShadow: `inset 0 0 0 1px color-mix(in oklab, ${STATUS_COLOR[status]} 45%, transparent)`,
|
||||
}}
|
||||
>
|
||||
{STATUS_LABEL[status]}
|
||||
</span>
|
||||
|
||||
{item.owned && (
|
||||
<span
|
||||
className="rounded-full px-2 py-0.5 text-[0.65rem] font-semibold text-[var(--fl-on-accent)]"
|
||||
style={{ backgroundImage: "var(--fl-gradient)" }}
|
||||
title={
|
||||
item.ownedSource ? `In deiner Sammlung: ${item.ownedSource}` : "In deiner Sammlung"
|
||||
}
|
||||
>
|
||||
Hab ich
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
|
||||
{item.rating != null && (
|
||||
@@ -51,7 +67,11 @@ export function LibraryCard({ item }: { item: LibraryItem }) {
|
||||
<p className="text-xs text-[var(--fl-faint)]">
|
||||
{item.type === "SERIES" ? "Serie" : "Film"}
|
||||
{item.year ? ` · ${item.year}` : ""}
|
||||
{item.whereToWatch ? ` · ${item.whereToWatch}` : ""}
|
||||
{item.owned && item.ownedSource
|
||||
? ` · ${item.ownedSource}`
|
||||
: item.whereToWatch
|
||||
? ` · ${item.whereToWatch}`
|
||||
: ""}
|
||||
</p>
|
||||
|
||||
{showProgress && (
|
||||
|
||||
@@ -56,7 +56,9 @@ export function LibraryFilters({ facets }: { facets: LibraryFacets }) {
|
||||
const status = params.get("status");
|
||||
const type = params.get("typ");
|
||||
const sort = params.get("sortierung") ?? "aktualisiert";
|
||||
const activeCount = ["genre", "tag", "dienst", "typ"].filter((key) => params.get(key)).length;
|
||||
const activeCount = ["genre", "tag", "dienst", "typ", "sammlung"].filter((key) =>
|
||||
params.get(key),
|
||||
).length;
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
@@ -78,6 +80,15 @@ export function LibraryFilters({ facets }: { facets: LibraryFacets }) {
|
||||
{STATUS_LABEL[key as StatusKey]} <Count value={facets.counts[key]} />
|
||||
</Chip>
|
||||
))}
|
||||
|
||||
{facets.owned > 0 && (
|
||||
<Chip
|
||||
active={params.get("sammlung") === "1"}
|
||||
onClick={() => apply({ sammlung: params.get("sammlung") === "1" ? null : "1" })}
|
||||
>
|
||||
Hab ich <Count value={facets.owned} />
|
||||
</Chip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
|
||||
+12
-1
@@ -110,7 +110,14 @@ export async function setStatus(userId: string, entryId: string, status: EntrySt
|
||||
export async function updateEntryDetails(
|
||||
userId: string,
|
||||
entryId: string,
|
||||
data: { rating?: number | null; notes?: string | null; whereToWatch?: string | null; favorite?: boolean },
|
||||
data: {
|
||||
rating?: number | null;
|
||||
notes?: string | null;
|
||||
whereToWatch?: string | null;
|
||||
favorite?: boolean;
|
||||
owned?: boolean;
|
||||
ownedSource?: string | null;
|
||||
},
|
||||
) {
|
||||
await requireEntry(userId, entryId);
|
||||
|
||||
@@ -127,6 +134,10 @@ export async function updateEntryDetails(
|
||||
? { whereToWatch: data.whereToWatch?.trim() || null }
|
||||
: {}),
|
||||
...(data.favorite !== undefined ? { favorite: data.favorite } : {}),
|
||||
...(data.owned !== undefined ? { owned: data.owned } : {}),
|
||||
...(data.ownedSource !== undefined
|
||||
? { ownedSource: data.ownedSource?.trim() || null }
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
+12
-1
@@ -19,6 +19,8 @@ export type LibraryFilters = {
|
||||
genre?: string;
|
||||
tag?: string;
|
||||
provider?: string;
|
||||
/** Nur Titel, die in der eigenen Sammlung liegen. */
|
||||
owned?: boolean;
|
||||
sort?: LibrarySort;
|
||||
};
|
||||
|
||||
@@ -42,6 +44,8 @@ export type LibraryItem = {
|
||||
favorite: boolean;
|
||||
currentRun: number;
|
||||
whereToWatch: string | null;
|
||||
owned: boolean;
|
||||
ownedSource: string | null;
|
||||
tags: string[];
|
||||
watched: number;
|
||||
total: number;
|
||||
@@ -56,6 +60,7 @@ export async function getLibrary(
|
||||
userId,
|
||||
deletedAt: null,
|
||||
...(filters.status ? { status: filters.status } : {}),
|
||||
...(filters.owned ? { owned: true } : {}),
|
||||
...(filters.tag ? { tags: { some: { tag: { name: filters.tag } } } } : {}),
|
||||
title: {
|
||||
...(filters.type ? { type: filters.type } : {}),
|
||||
@@ -116,6 +121,8 @@ export async function getLibrary(
|
||||
favorite: entry.favorite,
|
||||
currentRun: entry.currentRun,
|
||||
whereToWatch: entry.whereToWatch,
|
||||
owned: entry.owned,
|
||||
ownedSource: entry.ownedSource,
|
||||
tags: entry.tags.map((link) => link.tag.name),
|
||||
watched,
|
||||
total,
|
||||
@@ -162,11 +169,13 @@ export type LibraryFacets = {
|
||||
tags: string[];
|
||||
providers: string[];
|
||||
counts: Record<EntryStatus | "ALLE", number>;
|
||||
/** Wie viele Titel in der eigenen Sammlung liegen. */
|
||||
owned: number;
|
||||
};
|
||||
|
||||
/** Werte für die Filterleiste — nur das, was in der Bibliothek wirklich vorkommt. */
|
||||
export async function getLibraryFacets(userId: string): Promise<LibraryFacets> {
|
||||
const [genreRows, tagRows, providerRows, statusRows, total] = await Promise.all([
|
||||
const [genreRows, tagRows, providerRows, statusRows, total, owned] = await Promise.all([
|
||||
prisma.$queryRaw<{ genre: string }[]>`
|
||||
SELECT DISTINCT unnest(t."genres") AS genre
|
||||
FROM "LibraryEntry" e
|
||||
@@ -193,6 +202,7 @@ export async function getLibraryFacets(userId: string): Promise<LibraryFacets> {
|
||||
_count: { _all: true },
|
||||
}),
|
||||
prisma.libraryEntry.count({ where: { userId, deletedAt: null } }),
|
||||
prisma.libraryEntry.count({ where: { userId, deletedAt: null, owned: true } }),
|
||||
]);
|
||||
|
||||
const counts = {
|
||||
@@ -211,6 +221,7 @@ export async function getLibraryFacets(userId: string): Promise<LibraryFacets> {
|
||||
tags: tagRows.map((row) => row.name),
|
||||
providers: providerRows.map((row) => row.name),
|
||||
counts,
|
||||
owned,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+18
-1
@@ -135,6 +135,11 @@ const CSV_HEADERS: Record<string, string> = {
|
||||
notiz: "notes",
|
||||
notes: "notes",
|
||||
kommentar: "notes",
|
||||
habich: "owned",
|
||||
owned: "owned",
|
||||
besitz: "owned",
|
||||
sammlung: "ownedSource",
|
||||
quelle: "ownedSource",
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -185,6 +190,8 @@ export async function importCsv(userId: string, text: string): Promise<ImportSum
|
||||
rating: record.rating ? Number.parseInt(record.rating, 10) : null,
|
||||
year: record.year ? Number.parseInt(record.year, 10) : null,
|
||||
notes: record.notes || null,
|
||||
owned: /^(ja|yes|true|1|x)$/i.test(record.owned ?? ""),
|
||||
ownedSource: record.ownedSource || null,
|
||||
});
|
||||
if (result === "added") summary.added += 1;
|
||||
else summary.skipped += 1;
|
||||
@@ -299,6 +306,8 @@ type ImportRecord = {
|
||||
rating: number | null;
|
||||
year: number | null;
|
||||
notes: string | null;
|
||||
owned?: boolean;
|
||||
ownedSource?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -353,12 +362,14 @@ async function importOne(userId: string, record: ImportRecord): Promise<"added"
|
||||
|
||||
const entry = await addToLibrary(userId, titleId, record.status);
|
||||
|
||||
if (record.rating || record.notes) {
|
||||
if (record.rating || record.notes || record.owned || record.ownedSource) {
|
||||
await prisma.libraryEntry.update({
|
||||
where: { id: entry.id },
|
||||
data: {
|
||||
rating: record.rating && record.rating >= 1 && record.rating <= 10 ? record.rating : null,
|
||||
notes: record.notes,
|
||||
owned: record.owned ?? false,
|
||||
ownedSource: record.ownedSource ?? null,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -394,6 +405,8 @@ export async function exportCsv(userId: string): Promise<string> {
|
||||
"Status",
|
||||
"Bewertung",
|
||||
"Durchlaeufe",
|
||||
"HabIch",
|
||||
"Sammlung",
|
||||
"WoGeschaut",
|
||||
"Tags",
|
||||
"Notiz",
|
||||
@@ -410,6 +423,8 @@ export async function exportCsv(userId: string): Promise<string> {
|
||||
entry.status,
|
||||
entry.rating ?? "",
|
||||
entry.currentRun + 1,
|
||||
entry.owned ? "ja" : "nein",
|
||||
entry.ownedSource ?? "",
|
||||
entry.whereToWatch ?? "",
|
||||
entry.tags.map((link) => link.tag.name).join(" "),
|
||||
entry.notes?.replace(/\r?\n/g, " ") ?? "",
|
||||
@@ -479,6 +494,8 @@ export async function exportJson(userId: string): Promise<object> {
|
||||
rating: entry.rating,
|
||||
notes: entry.notes,
|
||||
whereToWatch: entry.whereToWatch,
|
||||
owned: entry.owned,
|
||||
ownedSource: entry.ownedSource,
|
||||
favorite: entry.favorite,
|
||||
currentRun: entry.currentRun,
|
||||
tags: entry.tags.map((link) => link.tag.name),
|
||||
|
||||
Reference in New Issue
Block a user