fix: before/after works for compose (source fallback), hidden for text-only; add reuse result as new Studio input
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6
This commit is contained in:
@@ -53,6 +53,8 @@ export default function LibraryApp() {
|
|||||||
if (r.ok) notify('Alternative wird erzeugt — erscheint gleich hier.'); else notify(r.error || 'Fehler.');
|
if (r.ok) notify('Alternative wird erzeugt — erscheint gleich hier.'); else notify(r.error || 'Fehler.');
|
||||||
setTimeout(load, 2500);
|
setTimeout(load, 2500);
|
||||||
};
|
};
|
||||||
|
// Ergebnis als neue Vorlage ins Studio übernehmen und weiterbearbeiten.
|
||||||
|
const reuse = (id: string) => { window.location.href = `/?reuse=${id}`; };
|
||||||
|
|
||||||
const newFolder = async () => {
|
const newFolder = async () => {
|
||||||
const name = prompt('Name des Ordners?'); if (!name) return;
|
const name = prompt('Name des Ordners?'); if (!name) return;
|
||||||
@@ -143,8 +145,9 @@ export default function LibraryApp() {
|
|||||||
{v.delivery_status === 'failed' && <span className="dbadge err">Picdrop ✕</span>}
|
{v.delivery_status === 'failed' && <span className="dbadge err">Picdrop ✕</span>}
|
||||||
</div>
|
</div>
|
||||||
<div className="reihe knapp">
|
<div className="reihe knapp">
|
||||||
<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>
|
<a className="mini" href={`/api/items/${v.id}/file?download=1`}>Laden</a>
|
||||||
|
<button className="mini stark2" onClick={() => reuse(v.id)} title="Ergebnis im Studio weiterbearbeiten">Weiterbearbeiten</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">+ Alternative</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>
|
||||||
@@ -166,7 +169,7 @@ export default function LibraryApp() {
|
|||||||
<input type="range" min={0} max={100} defaultValue={50}
|
<input type="range" min={0} max={100} defaultValue={50}
|
||||||
onChange={(e) => { const o = document.getElementById('ov'); if (o) o.style.clipPath = `inset(0 0 0 ${e.target.value}%)`; }} />
|
onChange={(e) => { const o = document.getElementById('ov'); if (o) o.style.clipPath = `inset(0 0 0 ${e.target.value}%)`; }} />
|
||||||
</div>
|
</div>
|
||||||
<div className="lupe-fuss">{compare.filename} · {compare.output_px}px — Regler: links 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>
|
||||||
)}
|
)}
|
||||||
{toast && <div className="toast">{toast}</div>}
|
{toast && <div className="toast">{toast}</div>}
|
||||||
@@ -208,6 +211,7 @@ function LibStyles() {
|
|||||||
.name{margin-top:8px;width:100%;font-size:12.5px;padding:6px 8px;font-family:var(--font-mono);border:1px solid var(--line);border-radius:3px;background:#fff;}
|
.name{margin-top:8px;width:100%;font-size:12.5px;padding:6px 8px;font-family:var(--font-mono);border:1px solid var(--line);border-radius:3px;background:#fff;}
|
||||||
.meta{font-size:11px;color:var(--soft);margin-top:5px;}
|
.meta{font-size:11px;color:var(--soft);margin-top:5px;}
|
||||||
.mini.stark{background:var(--accent);border-color:var(--accent);color:#fff;font-weight:600;}
|
.mini.stark{background:var(--accent);border-color:var(--accent);color:#fff;font-weight:600;}
|
||||||
|
.mini.stark2{border-color:var(--accent);color:var(--accent);font-weight:600;}
|
||||||
.dbadge{display:inline-block;margin-left:6px;font-family:var(--font-mono);font-size:9px;letter-spacing:.08em;text-transform:uppercase;background:var(--paper);color:var(--soft);padding:1px 6px;border-radius:20px;}
|
.dbadge{display:inline-block;margin-left:6px;font-family:var(--font-mono);font-size:9px;letter-spacing:.08em;text-transform:uppercase;background:var(--paper);color:var(--soft);padding:1px 6px;border-radius:20px;}
|
||||||
.dbadge.ok{background:var(--accent-bg);color:var(--accent);}
|
.dbadge.ok{background:var(--accent-bg);color:var(--accent);}
|
||||||
.dbadge.err{color:var(--err);}
|
.dbadge.err{color:var(--err);}
|
||||||
|
|||||||
@@ -115,6 +115,25 @@ export default function StudioApp({ recipes }: { recipes: any[] }) {
|
|||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Ergebnis aus der Bibliothek übernehmen: ?reuse=<itemId> lädt es als Vorlage.
|
||||||
|
useEffect(() => {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const reuseId = params.get('reuse');
|
||||||
|
if (!reuseId) return;
|
||||||
|
(async () => {
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
setPics((p) => [...p, { id, src: `/api/items/${reuseId}/file?thumb=1`, name: 'weiterbearbeiten.png', uploading: true }]);
|
||||||
|
try {
|
||||||
|
const j = await fetch(`/api/items/${reuseId}/reuse`, { method: 'POST' }).then((r) => r.json());
|
||||||
|
setPics((p) => p.map((x) => x.id === id
|
||||||
|
? { ...x, uploading: false, source_path: j.source_path, name: j.filename || x.name, error: j.error } : x));
|
||||||
|
} catch {
|
||||||
|
setPics((p) => p.map((x) => x.id === id ? { ...x, uploading: false, error: 'Konnte Bild nicht laden' } : x));
|
||||||
|
}
|
||||||
|
history.replaceState(null, '', '/');
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
function toggleTask(id: string) {
|
function toggleTask(id: string) {
|
||||||
setTasks((t) => {
|
setTasks((t) => {
|
||||||
let next = t.includes(id) ? t.filter((x) => x !== id) : [...t, id];
|
let next = t.includes(id) ? t.filter((x) => x !== id) : [...t, id];
|
||||||
|
|||||||
@@ -11,9 +11,10 @@ export const GET: APIRoute = async ({ params, url, locals }) => {
|
|||||||
const wantThumb = q.get('thumb') === '1';
|
const wantThumb = q.get('thumb') === '1';
|
||||||
const wantSrc = q.get('src') === '1';
|
const wantSrc = q.get('src') === '1';
|
||||||
const item = await one<any>(
|
const item = await one<any>(
|
||||||
`SELECT source_path, result_path, thumb_path, filename, has_alpha FROM items WHERE id=$1`, [params.id]);
|
`SELECT source_path, source_paths, result_path, thumb_path, filename, has_alpha FROM items WHERE id=$1`, [params.id]);
|
||||||
// Vorschau: Thumbnail bevorzugen, sonst Ergebnis. Quelle nur explizit.
|
// Quelle: Einzelquelle, sonst erste Kombinier-Quelle (für Vorher/Nachher).
|
||||||
const path = wantSrc ? item?.source_path
|
const firstSource = item?.source_path || (Array.isArray(item?.source_paths) ? item.source_paths[0] : null);
|
||||||
|
const path = wantSrc ? firstSource
|
||||||
: wantThumb ? (item?.thumb_path || item?.result_path)
|
: wantThumb ? (item?.thumb_path || item?.result_path)
|
||||||
: item?.result_path;
|
: item?.result_path;
|
||||||
if (!path) return new Response('Nicht gefunden', { status: 404 });
|
if (!path) return new Response('Nicht gefunden', { status: 404 });
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import type { APIRoute } from 'astro';
|
||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
|
import { one } from '../../../../lib/db';
|
||||||
|
import { getObject, putObject, sourceKey } from '../../../../lib/storage';
|
||||||
|
|
||||||
|
export const prerender = false;
|
||||||
|
const json = (b: unknown, s = 200) =>
|
||||||
|
new Response(JSON.stringify(b), { status: s, headers: { 'Content-Type': 'application/json' } });
|
||||||
|
|
||||||
|
/** Kopiert das Ergebnis eines Items in eine neue Quelle, damit es im Studio
|
||||||
|
* als Vorlage weiterbearbeitet werden kann. */
|
||||||
|
export const POST: APIRoute = async ({ params, locals }) => {
|
||||||
|
if (!locals.user) return new Response('Unauthorized', { status: 401 });
|
||||||
|
const it = await one<{ result_path: string | null; filename: string | null }>(
|
||||||
|
'SELECT result_path, filename FROM items WHERE id=$1', [params.id]);
|
||||||
|
if (!it?.result_path) return json({ error: 'Kein Ergebnis vorhanden.' }, 404);
|
||||||
|
try {
|
||||||
|
const buf = await getObject(it.result_path);
|
||||||
|
const key = sourceKey(randomUUID(), 'png');
|
||||||
|
await putObject(key, buf, 'image/png');
|
||||||
|
const name = (it.filename || 'bild').replace(/\.[^.]+$/, '') + '.png';
|
||||||
|
return json({ source_path: key, filename: name });
|
||||||
|
} catch (e: any) {
|
||||||
|
return json({ error: e?.message || 'Konnte Ergebnis nicht laden.' }, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user