Feature: Tagebuch Cover-Bild (Favorit-Funktion) für diary_media

- Migration: diary_media.is_cover (INTEGER DEFAULT 0)
- Upload: erstes Item eines Eintrags automatisch is_cover=1
- Neuer Endpoint: PATCH /diary/{id}/media/{mid}/cover
- GET-Endpoints geben is_cover + cover_url zurück
- Frontend: Stern-Button () in Gallery-Detail und Edit-Formular
- Timeline-Karte verwendet cover_url als Vorschaubild
- SW by-v212, APP_VER 186
This commit is contained in:
rene 2026-04-18 19:07:37 +02:00
parent 63ab092f5e
commit fa0fcbf8c9
7 changed files with 196 additions and 21 deletions

View file

@ -87,12 +87,12 @@ def _set_dog_ids(conn, entry_id: int, dog_ids: list[int]):
def _fetch_media_items(conn, entry_ids: list[int]) -> dict:
"""Gibt {entry_id: [{url, media_type, sort_order, id}, ...]} zurück."""
"""Gibt {entry_id: [{url, media_type, sort_order, id, is_cover}, ...]} zurück."""
if not entry_ids:
return {}
ph = ",".join("?" * len(entry_ids))
rows = conn.execute(
f"SELECT id, diary_id, url, media_type, sort_order FROM diary_media "
f"SELECT id, diary_id, url, media_type, sort_order, is_cover FROM diary_media "
f"WHERE diary_id IN ({ph}) ORDER BY diary_id, sort_order",
entry_ids
).fetchall()
@ -100,7 +100,8 @@ def _fetch_media_items(conn, entry_ids: list[int]) -> dict:
for r in rows:
result.setdefault(r["diary_id"], []).append({
"id": r["id"], "url": r["url"],
"media_type": r["media_type"], "sort_order": r["sort_order"]
"media_type": r["media_type"], "sort_order": r["sort_order"],
"is_cover": r["is_cover"],
})
return result
@ -109,7 +110,11 @@ def _entry_dict(row, dog_ids_map: dict, media_map: dict = None) -> dict:
e = dict(row)
e["tags"] = json.loads(e["tags"]) if e["tags"] else []
e["dog_ids"] = dog_ids_map.get(e["id"], [e["dog_id"]])
e["media_items"] = (media_map or {}).get(e["id"], [])
items = (media_map or {}).get(e["id"], [])
e["media_items"] = items
# cover_url: Item mit is_cover=1, Fallback auf erstes Item
cover = next((m for m in items if m.get("is_cover")), items[0] if items else None)
e["cover_url"] = cover["url"] if cover else None
return e
@ -437,16 +442,19 @@ async def upload_media(dog_id: int, entry_id: int,
"SELECT COALESCE(MAX(sort_order), -1) FROM diary_media WHERE diary_id=?",
(entry_id,)
).fetchone()[0]
# Erstes Item eines Eintrags wird automatisch Cover
is_cover = 1 if max_order == -1 else 0
conn.execute(
"INSERT INTO diary_media (diary_id, url, media_type, sort_order) VALUES (?,?,?,?)",
(entry_id, media_url, media_type, max_order + 1)
"INSERT INTO diary_media (diary_id, url, media_type, sort_order, is_cover) VALUES (?,?,?,?,?)",
(entry_id, media_url, media_type, max_order + 1, is_cover)
)
new_id = conn.execute(
"SELECT id FROM diary_media WHERE diary_id=? ORDER BY id DESC LIMIT 1",
(entry_id,)
).fetchone()["id"]
return {"id": new_id, "url": media_url, "media_type": media_type, "sort_order": max_order + 1}
return {"id": new_id, "url": media_url, "media_type": media_type,
"sort_order": max_order + 1, "is_cover": is_cover}
@router.delete("/{dog_id}/diary/{entry_id}/media/{media_id}", status_code=204)
@ -484,3 +492,24 @@ async def delete_media_legacy(dog_id: int, entry_id: int, user=Depends(get_curre
try: os.remove(path)
except OSError: pass
conn.execute("UPDATE diary SET media_url=NULL WHERE id=?", (entry_id,))
@router.patch("/{dog_id}/diary/{entry_id}/media/{media_id}/cover", status_code=200)
async def set_cover_media(dog_id: int, entry_id: int, media_id: int,
user=Depends(get_current_user)):
"""Setzt ein Medium als Cover-Bild (is_cover=1), alle anderen auf 0."""
with db() as conn:
_own_dog(dog_id, user["id"], conn)
row = conn.execute(
"SELECT dm.id FROM diary_media dm "
"JOIN diary d ON d.id = dm.diary_id "
"LEFT JOIN diary_dogs dd ON dd.diary_id = d.id "
"WHERE dm.id=? AND dm.diary_id=? AND (d.dog_id=? OR dd.dog_id=?)",
(media_id, entry_id, dog_id, dog_id)
).fetchone()
if not row:
raise HTTPException(404, "Medium nicht gefunden.")
# Alle Items dieses Eintrags auf is_cover=0, dann das gewählte auf 1
conn.execute("UPDATE diary_media SET is_cover=0 WHERE diary_id=?", (entry_id,))
conn.execute("UPDATE diary_media SET is_cover=1 WHERE id=?", (media_id,))
return {"ok": True}