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:
2026-07-23 20:01:17 +00:00
parent 063903c60a
commit 5105c7d529
4 changed files with 55 additions and 5 deletions
+6 -2
View File
@@ -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);}
+19
View File
@@ -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];
+4 -3
View File
@@ -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 });
+26
View File
@@ -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);
}
};