diff --git a/app.py b/app.py index aec330e..b6e546d 100644 --- a/app.py +++ b/app.py @@ -113,6 +113,63 @@ def directus_object(obj_id): return jsonify(data), status +@app.route("/api/directus/pictures//words", methods=["GET", "POST"]) +def directus_picture_words(pic_id): + """Proxy: Safe-Words eines Bildes laden (GET) oder speichern (POST).""" + token = request.headers.get("Authorization", "") + + if request.method == "GET": + junc, _ = _directus( + "GET", + f"/items/words_pictures?filter[pictures_id][_eq]={pic_id}&fields=id,words_id&limit=500", + token, + ) + w_ids = [e["words_id"] for e in (junc.get("data") or []) if e.get("words_id")] + if not w_ids: + return jsonify({"data": []}) + ids_param = urllib.parse.quote(",".join(w_ids), safe="") + w_data, _ = _directus( + "GET", + f"/items/words?filter[id][_in]={ids_param}&filter[status][_neq]=archived&fields=id,title_de,level,status&limit=500", + token, + ) + junc_by_word = {e["words_id"]: e["id"] for e in (junc.get("data") or [])} + items = [ + { + "id": junc_by_word.get(w["id"], ""), + "word_id": w["id"], + "title_de": w["title_de"], + "level": w.get("level") or 50, + "status": w.get("status", ""), + } + for w in (w_data.get("data") or []) + ] + return jsonify({"data": items}) + + else: # POST + body = request.get_json(force=True, silent=True) or {} + words = body.get("words", []) + _ensure_junction("words_pictures", "words_id", "pictures_id", token) + saved = 0 + for entry in words: + title_de = (entry.get("title_de") or "").strip() + level = int(entry.get("level") or 50) + if not title_de: + continue + try: + wid, _ = _find_or_create_word(title_de, level, token) + _ensure_link( + "words_pictures", + {"words_id": wid, "pictures_id": pic_id}, + {"words_id": wid, "pictures_id": pic_id}, + token, + ) + saved += 1 + except Exception as e: + print(f"[picture_words] error for '{title_de}': {e}") + return jsonify({"ok": True, "saved": saved}) + + @app.route("/api/images", methods=["GET"]) def list_images(): """ diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 654d7b3..c4c1be3 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -1,4 +1,4 @@ -import type { ObjectMeta, Sentence } from './types' +import type { ObjectMeta, Sentence, PictureWord } from './types' const DIRECTUS_URL = 'https://db.hejyou.com' @@ -282,6 +282,30 @@ export async function deleteWord(wId: string, token: string): Promise { if (!res.ok) throw new Error('Fehler beim Löschen des Worts') } +export async function getPictureWords(pictureId: string, token: string): Promise { + const res = await fetch(`/api/directus/pictures/${pictureId}/words`, { + headers: { Authorization: `Bearer ${token}` }, + }) + const data = await res.json() + if (!res.ok) throw new Error('Fehler beim Laden der Bild-Wörter') + return data.data as PictureWord[] +} + +export async function savePictureWords( + pictureId: string, + words: { title_de: string; level: number }[], + token: string +): Promise<{ saved: number }> { + const res = await fetch(`/api/directus/pictures/${pictureId}/words`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, + body: JSON.stringify({ words }), + }) + const data = await res.json() + if (!res.ok) throw new Error(data.error || 'Fehler beim Speichern der Wörter') + return data +} + export async function purgeOrphans(objId: string, token: string): Promise<{ orphans_removed: number }> { const res = await fetch(`/api/object/${encodeURIComponent(objId)}/purge-orphans`, { method: 'POST', diff --git a/frontend/src/pages/DrawIt.tsx b/frontend/src/pages/DrawIt.tsx index 26539fc..40ee3c7 100644 --- a/frontend/src/pages/DrawIt.tsx +++ b/frontend/src/pages/DrawIt.tsx @@ -4,10 +4,10 @@ import Topbar from '../components/Topbar' import { getDirectusPictures, directusAssetUrl, type DirectusPicture, getDirectusObjects, createDirectusObject, updateDirectusObject, deleteDirectusObject, - updatePictureStatus, + updatePictureStatus, getPictureWords, savePictureWords, } from '../api' import { useAuth } from '../context/AuthContext' -import type { DirectusObject, Selection, CanvasObject } from '../types' +import type { DirectusObject, Selection, CanvasObject, PictureWord } from '../types' const ChevronLeftIcon = () => ( @@ -34,6 +34,13 @@ export default function DrawIt() { const [selectedObjectId, setSelectedObjectId] = useState(null) const [currentSelections, setCurrentSelections] = useState([]) const [userNotes, setUserNotes] = useState('') + const [safeWords, setSafeWords] = useState<{ title: string; level: number }[]>([]) + const [safeWordInput, setSafeWordInput] = useState('') + const [safeWordLevel, setSafeWordLevel] = useState(50) + const [safeWordInputVisible, setSafeWordInputVisible] = useState(false) + const safeWordInputRef = useRef(null) + const [pictureWords, setPictureWords] = useState([]) + const [savingWords, setSavingWords] = useState(false) const [parentId, setParentId] = useState(null) const [editingNotes, setEditingNotes] = useState<{ id: string; notes: string } | null>(null) const [mode, setMode] = useState<'rect' | 'polygon'>('polygon') @@ -45,6 +52,37 @@ export default function DrawIt() { const canvasRef = useRef(null) + useEffect(() => { + if (safeWordInputVisible) safeWordInputRef.current?.focus() + }, [safeWordInputVisible]) + + const addSafeWord = () => { + const title = safeWordInput.trim() + if (!title || safeWords.some(w => w.title === title) || pictureWords.some(w => w.title_de === title)) { + setSafeWordInput(''); return + } + setSafeWords(prev => [...prev, { title, level: safeWordLevel }]) + setSafeWordInput('') + setSafeWordLevel(50) + setSafeWordInputVisible(false) + } + + const saveSafeWords = async () => { + if (!currentPicture || !token || safeWords.length === 0) return + setSavingWords(true) + try { + await savePictureWords(currentPicture.id, safeWords.map(w => ({ title_de: w.title, level: w.level })), token) + const updated = await getPictureWords(currentPicture.id, token) + setPictureWords(updated) + setSafeWords([]) + showStatus('Wörter gespeichert.') + } catch (e) { + showStatus(e instanceof Error ? e.message : 'Fehler beim Speichern der Wörter.', true) + } finally { + setSavingWords(false) + } + } + const currentPicture: DirectusPicture | null = currentIndex >= 0 && currentIndex < pictureList.length ? pictureList[currentIndex] : null @@ -65,10 +103,17 @@ export default function DrawIt() { }, [token]) useEffect(() => { - if (!currentPicture || !token) { setObjects([]); setSelectedObjectId(null); return } + if (!currentPicture || !token) { + setObjects([]); setSelectedObjectId(null) + setPictureWords([]); setSafeWords([]) + return + } getDirectusObjects(currentPicture.id, token) .then(objs => { setObjects(objs.map(o => ({ ...o, visible: true }))); setSelectedObjectId(null) }) .catch(console.error) + getPictureWords(currentPicture.id, token) + .then(setPictureWords) + .catch(console.error) }, [currentPicture?.id, token]) const showStatus = (msg: string, isError = false) => { @@ -372,6 +417,123 @@ export default function DrawIt() { {status &&
{status}
} + + {/* Words sidebar */} + ) diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 860e368..6f256c2 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -45,6 +45,15 @@ export interface DirectusObject { visible?: boolean // local UI state only } +// Word linked to a picture (loaded from Directus via words_pictures junction) +export interface PictureWord { + id: string // junction row id + word_id: string + title_de: string + level: number + status: string +} + // Legacy — still used by GenerateIt export interface ObjectMeta { id: string