feat: Picdrop-style color tags (Rot/Orange/Gruen/Final) in library + in .md metadata; tap-to-open fullscreen for iPhone Save-to-Photos
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
-- Farbmarkierungen wie bei Picdrop (Rot / Orange / Grün / Final).
|
||||||
|
ALTER TABLE items ADD COLUMN IF NOT EXISTS color_tag text
|
||||||
|
CHECK (color_tag IN ('red','orange','green','final'));
|
||||||
|
CREATE INDEX IF NOT EXISTS items_color_tag_idx ON items(color_tag);
|
||||||
@@ -2,7 +2,16 @@ import React, { useState, useEffect, useCallback } from 'react';
|
|||||||
|
|
||||||
interface Item { id: string; filename: string; output_px: string; has_alpha: boolean;
|
interface Item { id: string; filename: string; output_px: string; has_alpha: boolean;
|
||||||
folder_id: string | null; by_name: string; tasks: string[]; delivery_status: string; nas_status: string;
|
folder_id: string | null; by_name: string; tasks: string[]; delivery_status: string; nas_status: string;
|
||||||
model_used: string; variant_of: string | null; mode: string; thumb_path: string | null; prompt_used: string | null; created_at: string; }
|
model_used: string; variant_of: string | null; mode: string; thumb_path: string | null; prompt_used: string | null;
|
||||||
|
color_tag: string | null; created_at: string; }
|
||||||
|
|
||||||
|
const TAGS = [
|
||||||
|
{ id: 'red', label: 'Rot', color: '#E5484D' },
|
||||||
|
{ id: 'orange', label: 'Orange', color: '#E8830C' },
|
||||||
|
{ id: 'green', label: 'Grün', color: '#46A758' },
|
||||||
|
{ id: 'final', label: 'Final', color: '#3A3733' },
|
||||||
|
];
|
||||||
|
const tagColor = (t: string | null) => TAGS.find((x) => x.id === t)?.color || null;
|
||||||
|
|
||||||
export default function LibraryApp() {
|
export default function LibraryApp() {
|
||||||
const [items, setItems] = useState<Item[]>([]);
|
const [items, setItems] = useState<Item[]>([]);
|
||||||
@@ -13,6 +22,9 @@ export default function LibraryApp() {
|
|||||||
const [variantSel, setVariantSel] = useState<Record<string, string>>({});
|
const [variantSel, setVariantSel] = useState<Record<string, string>>({});
|
||||||
const [compare, setCompare] = useState<Item | null>(null);
|
const [compare, setCompare] = useState<Item | null>(null);
|
||||||
const [promptOf, setPromptOf] = useState<Item | null>(null);
|
const [promptOf, setPromptOf] = useState<Item | null>(null);
|
||||||
|
const [saveView, setSaveView] = useState<Item | null>(null);
|
||||||
|
const [tagMenu, setTagMenu] = useState<string | null>(null);
|
||||||
|
const [tagFilter, setTagFilter] = useState<string | null>(null);
|
||||||
const [toast, setToast] = useState<string | null>(null);
|
const [toast, setToast] = useState<string | null>(null);
|
||||||
const notify = (t: string) => { setToast(t); setTimeout(() => setToast(null), 2400); };
|
const notify = (t: string) => { setToast(t); setTimeout(() => setToast(null), 2400); };
|
||||||
|
|
||||||
@@ -36,6 +48,11 @@ export default function LibraryApp() {
|
|||||||
|
|
||||||
const rename = async (id: string, filename: string) =>
|
const rename = async (id: string, filename: string) =>
|
||||||
fetch(`/api/items/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ filename }) });
|
fetch(`/api/items/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ filename }) });
|
||||||
|
const setTag = async (id: string, color_tag: string | null) => {
|
||||||
|
setItems((x) => x.map((y) => y.id === id ? { ...y, color_tag } : y));
|
||||||
|
setTagMenu(null);
|
||||||
|
await fetch(`/api/items/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ color_tag }) });
|
||||||
|
};
|
||||||
const setFolder = async (id: string, folder_id: string | null) => {
|
const setFolder = async (id: string, folder_id: string | null) => {
|
||||||
await fetch(`/api/items/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ folder_id }) });
|
await fetch(`/api/items/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ folder_id }) });
|
||||||
load();
|
load();
|
||||||
@@ -90,8 +107,10 @@ export default function LibraryApp() {
|
|||||||
load();
|
load();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Nach Ordner filtern, dann Alternativen unter ihrem Ursprung gruppieren.
|
// Nach Ordner + Markierung filtern, dann Alternativen unter ihrem Ursprung gruppieren.
|
||||||
const filtered = folderFilter ? items.filter((v) => v.folder_id === folderFilter) : items;
|
const filtered = items
|
||||||
|
.filter((v) => !folderFilter || v.folder_id === folderFilter)
|
||||||
|
.filter((v) => !tagFilter || v.color_tag === tagFilter);
|
||||||
const groupMap = new Map<string, Item[]>();
|
const groupMap = new Map<string, Item[]>();
|
||||||
for (const it of filtered) {
|
for (const it of filtered) {
|
||||||
const root = it.variant_of || it.id;
|
const root = it.variant_of || it.id;
|
||||||
@@ -139,6 +158,16 @@ export default function LibraryApp() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="fein hinweis">Ordner gruppieren die Bibliothek und können je Ordner eine eigene Picdrop-Galerie bekommen (Pfeil = eigene Galerie hinterlegt).</div>
|
<div className="fein hinweis">Ordner gruppieren die Bibliothek und können je Ordner eine eigene Picdrop-Galerie bekommen (Pfeil = eigene Galerie hinterlegt).</div>
|
||||||
|
|
||||||
|
{/* Farbmarkierungen filtern */}
|
||||||
|
<div className="ordnerleiste tagbar">
|
||||||
|
<button className={`chip ${!tagFilter ? 'an' : ''}`} onClick={() => setTagFilter(null)}>Alle Markierungen</button>
|
||||||
|
{TAGS.map((t) => (
|
||||||
|
<button key={t.id} className={`chip flagchip ${tagFilter === t.id ? 'an' : ''}`} onClick={() => setTagFilter(tagFilter === t.id ? null : t.id)}>
|
||||||
|
<span className="flag" style={{ color: t.color }}>⚑</span> {t.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
{filtered.length === 0 ? (
|
{filtered.length === 0 ? (
|
||||||
<div className="leer"><span className="regmark" /><h3>{folderFilter ? 'Ordner ist leer' : 'Noch nichts erstellt'}</h3>
|
<div className="leer"><span className="regmark" /><h3>{folderFilter ? 'Ordner ist leer' : 'Noch nichts erstellt'}</h3>
|
||||||
<p>{folderFilter ? 'Weise Bildern über das Ordner-Menü diesen Ordner zu.' : 'Lege im Studio los — die Ergebnisse sammeln sich hier.'}</p></div>
|
<p>{folderFilter ? 'Weise Bildern über das Ordner-Menü diesen Ordner zu.' : 'Lege im Studio los — die Ergebnisse sammeln sich hier.'}</p></div>
|
||||||
@@ -154,6 +183,7 @@ export default function LibraryApp() {
|
|||||||
<span className="haken">{pick.has(v.id) ? '✓' : ''}</span>
|
<span className="haken">{pick.has(v.id) ? '✓' : ''}</span>
|
||||||
{(v.mode === 'compose' || v.mode === 'generate') &&
|
{(v.mode === 'compose' || v.mode === 'generate') &&
|
||||||
<span className="modetag">{v.mode === 'compose' ? 'kombiniert' : 'erzeugt'}</span>}
|
<span className="modetag">{v.mode === 'compose' ? 'kombiniert' : 'erzeugt'}</span>}
|
||||||
|
{v.color_tag && <span className="flagbadge" style={{ color: tagColor(v.color_tag)! }}>⚑</span>}
|
||||||
{multi && <span className="fassung">Fassung {g.indexOf(v) + 1}/{g.length}</span>}
|
{multi && <span className="fassung">Fassung {g.indexOf(v) + 1}/{g.length}</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -177,11 +207,23 @@ export default function LibraryApp() {
|
|||||||
{v.nas_status === 'failed' && <span className="dbadge err">NAS ✕</span>}
|
{v.nas_status === 'failed' && <span className="dbadge err">NAS ✕</span>}
|
||||||
</div>
|
</div>
|
||||||
<div className="reihe knapp">
|
<div className="reihe knapp">
|
||||||
|
<div className="tagwrap">
|
||||||
|
<button className="mini flagbtn" style={v.color_tag ? { color: tagColor(v.color_tag)!, borderColor: tagColor(v.color_tag)! } : {}}
|
||||||
|
onClick={() => setTagMenu(tagMenu === v.id ? null : v.id)} title="Markierung">⚑</button>
|
||||||
|
{tagMenu === v.id && (
|
||||||
|
<div className="tagmenu" onMouseLeave={() => setTagMenu(null)}>
|
||||||
|
<button onClick={() => setTag(v.id, null)}><span className="flag none">⚑</span> Keine</button>
|
||||||
|
{TAGS.map((t) => (
|
||||||
|
<button key={t.id} onClick={() => setTag(v.id, t.id)}><span className="flag" style={{ color: t.color }}>⚑</span> {t.label}</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{v.prompt_used && <button className="mini" onClick={() => setPromptOf(v)}>Prompt</button>}
|
{v.prompt_used && <button className="mini" onClick={() => setPromptOf(v)}>Prompt</button>}
|
||||||
{v.mode !== 'generate' && <button className="mini" onClick={() => setCompare(v)}>Vorher/Nachher</button>}
|
{v.mode !== 'generate' && <button className="mini" onClick={() => setCompare(v)}>Vorher/Nachher</button>}
|
||||||
<a className="mini" href={`/api/items/${v.id}/file?download=1`}>Laden</a>
|
<button className="mini stark2" onClick={() => setSaveView(v)} title="Groß öffnen, dann sichern (iPhone: In Fotos sichern)">Laden</button>
|
||||||
<button className="mini stark2" onClick={() => reuse(v.id)} title="Ergebnis im Studio weiterbearbeiten">Weiterbearbeiten</button>
|
<button className="mini" onClick={() => reuse(v.id)} title="Ergebnis im Studio weiterbearbeiten">Weiter</button>
|
||||||
<button className="mini" onClick={() => alternative(v.id)} title="Weitere Fassung erzeugen">+ Alternative</button>
|
<button className="mini" onClick={() => alternative(v.id)} title="Weitere Fassung erzeugen">+ Alt.</button>
|
||||||
<select className="mini-select" value={v.folder_id || ''} onChange={(e) => setFolder(v.id, e.target.value || null)}>
|
<select className="mini-select" value={v.folder_id || ''} onChange={(e) => setFolder(v.id, e.target.value || null)}>
|
||||||
<option value="">Kein Ordner</option>
|
<option value="">Kein Ordner</option>
|
||||||
{folders.map((o) => <option key={o.id} value={o.id}>{o.name}</option>)}
|
{folders.map((o) => <option key={o.id} value={o.id}>{o.name}</option>)}
|
||||||
@@ -205,6 +247,16 @@ export default function LibraryApp() {
|
|||||||
<div className="lupe-fuss">{compare.filename} · {compare.output_px}px — Regler: links {compare.mode === 'compose' ? 'Vorlage' : 'Original'}, rechts Ergebnis</div>
|
<div className="lupe-fuss">{compare.filename} · {compare.output_px}px — Regler: links {compare.mode === 'compose' ? 'Vorlage' : 'Original'}, rechts Ergebnis</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{saveView && (
|
||||||
|
<div className="lupe" onClick={() => setSaveView(null)}>
|
||||||
|
<img className="saveimg" src={`/api/items/${saveView.id}/file`} alt={saveView.filename || ''} onClick={(e) => e.stopPropagation()} />
|
||||||
|
<div className="save-fuss" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<span>📱 iPhone/iPad: lange auf das Bild tippen → <b>„In Fotos sichern"</b>.</span>
|
||||||
|
<a className="mini stark" href={`/api/items/${saveView.id}/file?download=1`}>Als Datei herunterladen</a>
|
||||||
|
<button className="mini" onClick={() => setSaveView(null)}>Schließen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{promptOf && (
|
{promptOf && (
|
||||||
<div className="lupe" onClick={() => setPromptOf(null)}>
|
<div className="lupe" onClick={() => setPromptOf(null)}>
|
||||||
<div className="promptbox" onClick={(e) => e.stopPropagation()}>
|
<div className="promptbox" onClick={(e) => e.stopPropagation()}>
|
||||||
@@ -247,6 +299,18 @@ function LibStyles() {
|
|||||||
.haken{position:absolute;top:6px;left:6px;width:21px;height:21px;border-radius:50%;border:1.5px solid #fff;background:rgba(22,21,15,.35);color:#fff;display:flex;align-items:center;justify-content:center;font-size:12px;}
|
.haken{position:absolute;top:6px;left:6px;width:21px;height:21px;border-radius:50%;border:1.5px solid #fff;background:rgba(22,21,15,.35);color:#fff;display:flex;align-items:center;justify-content:center;font-size:12px;}
|
||||||
.kachel.gewaehlt .haken{background:var(--accent);}
|
.kachel.gewaehlt .haken{background:var(--accent);}
|
||||||
.modetag{position:absolute;top:6px;right:6px;background:rgba(59,91,219,.92);color:#fff;font-family:var(--font-mono);font-size:9px;letter-spacing:.06em;text-transform:uppercase;padding:2px 7px;border-radius:20px;}
|
.modetag{position:absolute;top:6px;right:6px;background:rgba(59,91,219,.92);color:#fff;font-family:var(--font-mono);font-size:9px;letter-spacing:.06em;text-transform:uppercase;padding:2px 7px;border-radius:20px;}
|
||||||
|
.flagbadge{position:absolute;top:5px;left:6px;font-size:19px;line-height:1;text-shadow:0 1px 2px rgba(0,0,0,.35);}
|
||||||
|
.tagbar{margin:0 0 12px;}
|
||||||
|
.flagchip .flag{font-size:13px;vertical-align:-1px;}
|
||||||
|
.flag.none{color:var(--soft);}
|
||||||
|
.tagwrap{position:relative;display:inline-block;}
|
||||||
|
.flagbtn{font-size:14px;line-height:1;padding:5px 9px;}
|
||||||
|
.tagmenu{position:absolute;bottom:calc(100% + 4px);left:0;z-index:20;background:var(--card);border:1px solid var(--line);border-radius:6px;box-shadow:0 8px 24px rgba(0,0,0,.16);padding:5px;display:flex;flex-direction:column;min-width:130px;}
|
||||||
|
.tagmenu button{display:flex;align-items:center;gap:8px;background:none;border:none;text-align:left;padding:7px 9px;cursor:pointer;font-family:inherit;font-size:13px;border-radius:4px;color:var(--ink);}
|
||||||
|
.tagmenu button:hover{background:var(--paper);}
|
||||||
|
.tagmenu .flag{font-size:15px;}
|
||||||
|
.saveimg{max-width:94vw;max-height:78vh;border-radius:4px;display:block;}
|
||||||
|
.save-fuss{display:flex;flex-direction:column;align-items:center;gap:10px;color:#EAEAE3;font-size:13.5px;text-align:center;max-width:90vw;}
|
||||||
.fassung{position:absolute;bottom:6px;right:6px;background:rgba(22,21,15,.7);color:#fff;font-family:var(--font-mono);font-size:9px;padding:2px 7px;border-radius:20px;}
|
.fassung{position:absolute;bottom:6px;right:6px;background:rgba(22,21,15,.7);color:#fff;font-family:var(--font-mono);font-size:9px;padding:2px 7px;border-radius:20px;}
|
||||||
.varianten{display:flex;gap:5px;margin-top:6px;overflow-x:auto;}
|
.varianten{display:flex;gap:5px;margin-top:6px;overflow-x:auto;}
|
||||||
.vthumb{flex:0 0 auto;width:40px;height:40px;border:1px solid var(--line);border-radius:3px;overflow:hidden;padding:0;background:none;cursor:pointer;}
|
.vthumb{flex:0 0 auto;width:40px;height:40px;border:1px solid var(--line);border-radius:3px;overflow:hidden;padding:0;background:none;cursor:pointer;}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export default function QueueApp({ jobId }: { jobId: string | null }) {
|
|||||||
const [job, setJob] = useState<any>(null);
|
const [job, setJob] = useState<any>(null);
|
||||||
const [items, setItems] = useState<any[]>([]);
|
const [items, setItems] = useState<any[]>([]);
|
||||||
const [jobs, setJobs] = useState<any[]>([]);
|
const [jobs, setJobs] = useState<any[]>([]);
|
||||||
|
const [saveView, setSaveView] = useState<any | null>(null);
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
if (jobId) {
|
if (jobId) {
|
||||||
@@ -87,12 +88,22 @@ export default function QueueApp({ jobId }: { jobId: string | null }) {
|
|||||||
{it.error_message && <span className="fein err">{it.error_message}</span>}
|
{it.error_message && <span className="fein err">{it.error_message}</span>}
|
||||||
</div>
|
</div>
|
||||||
<div className="iact">
|
<div className="iact">
|
||||||
{it.status === 'done' && <a className="mini" href={`/api/items/${it.id}/file?download=1`}>Laden</a>}
|
{it.status === 'done' && <button className="mini" onClick={() => setSaveView(it)}>Laden</button>}
|
||||||
{it.status === 'failed' && <button className="mini" onClick={() => act(`/api/items/${it.id}/retry`)}>Erneut</button>}
|
{it.status === 'failed' && <button className="mini" onClick={() => act(`/api/items/${it.id}/retry`)}>Erneut</button>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{saveView && (
|
||||||
|
<div className="lupe" onClick={() => setSaveView(null)}>
|
||||||
|
<img className="saveimg" src={`/api/items/${saveView.id}/file`} alt="" onClick={(e) => e.stopPropagation()} />
|
||||||
|
<div className="save-fuss" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<span>📱 iPhone/iPad: lange auf das Bild tippen → „In Fotos sichern".</span>
|
||||||
|
<a className="mini" href={`/api/items/${saveView.id}/file?download=1`}>Als Datei herunterladen</a>
|
||||||
|
<button className="mini" onClick={() => setSaveView(null)}>Schließen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<QStyles />
|
<QStyles />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -122,6 +133,10 @@ function QStyles() {
|
|||||||
.punkt.done{background:var(--ok);}
|
.punkt.done{background:var(--ok);}
|
||||||
.punkt.failed{background:var(--err);}
|
.punkt.failed{background:var(--err);}
|
||||||
.punkt.queued{background:var(--mark);}
|
.punkt.queued{background:var(--mark);}
|
||||||
|
.lupe{position:fixed;inset:0;background:rgba(22,21,15,.9);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;padding:20px;z-index:50;}
|
||||||
|
.saveimg{max-width:94vw;max-height:78vh;border-radius:4px;display:block;}
|
||||||
|
.save-fuss{display:flex;flex-direction:column;align-items:center;gap:10px;color:#EAEAE3;font-size:13.5px;text-align:center;}
|
||||||
|
.save-fuss .mini{text-decoration:none;}
|
||||||
.joblist{display:flex;flex-direction:column;}
|
.joblist{display:flex;flex-direction:column;}
|
||||||
.jobrow{display:flex;align-items:center;gap:10px;padding:11px 16px;border-bottom:1px solid var(--line);text-decoration:none;color:var(--ink);}
|
.jobrow{display:flex;align-items:center;gap:10px;padding:11px 16px;border-bottom:1px solid var(--line);text-decoration:none;color:var(--ink);}
|
||||||
p.fein{padding:16px;}
|
p.fein{padding:16px;}
|
||||||
|
|||||||
+3
-1
@@ -4,11 +4,12 @@ import { one } from './db';
|
|||||||
export async function buildMetadataMd(itemId: string): Promise<string> {
|
export async function buildMetadataMd(itemId: string): Promise<string> {
|
||||||
const it = await one<any>(
|
const it = await one<any>(
|
||||||
`SELECT i.filename, i.output_px, i.has_alpha, i.model_used, i.prompt_used, i.cost,
|
`SELECT i.filename, i.output_px, i.has_alpha, i.model_used, i.prompt_used, i.cost,
|
||||||
i.created_at, i.delivery_status, j.mode, j.recipe_snapshot
|
i.created_at, i.delivery_status, i.color_tag, j.mode, j.recipe_snapshot
|
||||||
FROM items i JOIN jobs j ON j.id=i.job_id WHERE i.id=$1`, [itemId]);
|
FROM items i JOIN jobs j ON j.id=i.job_id WHERE i.id=$1`, [itemId]);
|
||||||
if (!it) return '';
|
if (!it) return '';
|
||||||
const r = it.recipe_snapshot || {};
|
const r = it.recipe_snapshot || {};
|
||||||
const tasks = Array.isArray(r.tasks) ? r.tasks.join(', ') : '';
|
const tasks = Array.isArray(r.tasks) ? r.tasks.join(', ') : '';
|
||||||
|
const TAG: Record<string, string> = { red: 'Rot', orange: 'Orange', green: 'Grün', final: 'Final' };
|
||||||
const lines = [
|
const lines = [
|
||||||
`# ${it.filename || itemId}`,
|
`# ${it.filename || itemId}`,
|
||||||
'',
|
'',
|
||||||
@@ -19,6 +20,7 @@ export async function buildMetadataMd(itemId: string): Promise<string> {
|
|||||||
it.output_px ? `- **Auflösung:** ${it.output_px} px${r.dpi ? ` @ ${r.dpi} dpi` : ''}` : '',
|
it.output_px ? `- **Auflösung:** ${it.output_px} px${r.dpi ? ` @ ${r.dpi} dpi` : ''}` : '',
|
||||||
`- **Transparenz:** ${it.has_alpha ? 'ja' : 'nein'}`,
|
`- **Transparenz:** ${it.has_alpha ? 'ja' : 'nein'}`,
|
||||||
it.model_used ? `- **Modell:** ${it.model_used}` : '',
|
it.model_used ? `- **Modell:** ${it.model_used}` : '',
|
||||||
|
it.color_tag ? `- **Markierung:** ${TAG[it.color_tag] || it.color_tag}` : '',
|
||||||
it.cost != null ? `- **Kosten:** $${Number(it.cost).toFixed(4)}` : '',
|
it.cost != null ? `- **Kosten:** $${Number(it.cost).toFixed(4)}` : '',
|
||||||
'',
|
'',
|
||||||
'## Prompt',
|
'## Prompt',
|
||||||
|
|||||||
@@ -12,9 +12,13 @@ export const PATCH: APIRoute = async ({ params, request, locals }) => {
|
|||||||
const sets: string[] = []; const args: any[] = [];
|
const sets: string[] = []; const args: any[] = [];
|
||||||
if (typeof b.filename === 'string') { args.push(b.filename.trim()); sets.push(`filename=$${args.length}`); }
|
if (typeof b.filename === 'string') { args.push(b.filename.trim()); sets.push(`filename=$${args.length}`); }
|
||||||
if ('folder_id' in b) { args.push(b.folder_id || null); sets.push(`folder_id=$${args.length}`); }
|
if ('folder_id' in b) { args.push(b.folder_id || null); sets.push(`folder_id=$${args.length}`); }
|
||||||
|
if ('color_tag' in b) {
|
||||||
|
const t = ['red', 'orange', 'green', 'final'].includes(b.color_tag) ? b.color_tag : null;
|
||||||
|
args.push(t); sets.push(`color_tag=$${args.length}`);
|
||||||
|
}
|
||||||
if (!sets.length) return json({ error: 'Nichts zu ändern.' }, 400);
|
if (!sets.length) return json({ error: 'Nichts zu ändern.' }, 400);
|
||||||
args.push(params.id);
|
args.push(params.id);
|
||||||
const row = await one(`UPDATE items SET ${sets.join(',')} WHERE id=$${args.length} RETURNING id, filename, folder_id`, args);
|
const row = await one(`UPDATE items SET ${sets.join(',')} WHERE id=$${args.length} RETURNING id, filename, folder_id, color_tag`, args);
|
||||||
return json({ item: row });
|
return json({ item: row });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export const GET: APIRoute = async ({ url, locals }) => {
|
|||||||
if (!isAdmin && !shared) add('j.created_by = ?', locals.user.uid);
|
if (!isAdmin && !shared) add('j.created_by = ?', locals.user.uid);
|
||||||
|
|
||||||
if (q.get('folder')) add('i.folder_id = ?', q.get('folder'));
|
if (q.get('folder')) add('i.folder_id = ?', q.get('folder'));
|
||||||
|
if (q.get('tag')) add('i.color_tag = ?', q.get('tag'));
|
||||||
if (q.get('delivery')) add('i.delivery_status = ?', q.get('delivery'));
|
if (q.get('delivery')) add('i.delivery_status = ?', q.get('delivery'));
|
||||||
if (q.get('by')) add('j.created_by = ?', q.get('by'));
|
if (q.get('by')) add('j.created_by = ?', q.get('by'));
|
||||||
if (q.get('format')) add('i.output_px = ?', q.get('format'));
|
if (q.get('format')) add('i.output_px = ?', q.get('format'));
|
||||||
@@ -28,7 +29,7 @@ export const GET: APIRoute = async ({ url, locals }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const rows = await query(
|
const rows = await query(
|
||||||
`SELECT i.id, i.filename, i.output_px, i.has_alpha, i.result_path, i.thumb_path, i.folder_id,
|
`SELECT i.id, i.filename, i.output_px, i.has_alpha, i.result_path, i.thumb_path, i.folder_id, i.color_tag,
|
||||||
i.delivery_status, i.nas_status, i.prompt_used, i.created_at, i.model_used, i.variant_of, j.mode,
|
i.delivery_status, i.nas_status, i.prompt_used, i.created_at, i.model_used, i.variant_of, j.mode,
|
||||||
${(s?.anonymous_generations && !isAdmin) ? 'NULL' : 'u.display_name'} AS by_name,
|
${(s?.anonymous_generations && !isAdmin) ? 'NULL' : 'u.display_name'} AS by_name,
|
||||||
j.recipe_snapshot->'tasks' AS tasks
|
j.recipe_snapshot->'tasks' AS tasks
|
||||||
|
|||||||
Reference in New Issue
Block a user