diff --git a/backend/database.py b/backend/database.py index 6136174..7c13a8f 100644 --- a/backend/database.py +++ b/backend/database.py @@ -466,6 +466,11 @@ def _migrate(conn_factory): ("dogs", "foto_offset_y", "REAL NOT NULL DEFAULT 0.0"), # Tagebuch: Ortsname (POI/Adresse) ("diary", "location_name", "TEXT"), + # Bewertungen: walks + sitters brauchen bewertung + anz_bewertungen + ("walks", "bewertung", "REAL DEFAULT 0"), + ("walks", "anz_bewertungen", "INTEGER DEFAULT 0"), + ("sitters", "bewertung", "REAL DEFAULT 0"), + ("sitters", "anz_bewertungen", "INTEGER DEFAULT 0"), ] with conn_factory() as conn: for table, column, col_type in migrations: @@ -739,6 +744,36 @@ def _migrate(conn_factory): CREATE INDEX IF NOT EXISTS idx_service_offers_user ON service_offers(user_id, type); """) + # Ratings — einheitliches Bewertungssystem + conn.executescript(""" + CREATE TABLE IF NOT EXISTS ratings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + target_type TEXT NOT NULL, + target_id INTEGER NOT NULL, + stars INTEGER NOT NULL, + kommentar TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + UNIQUE(user_id, target_type, target_id) + ); + CREATE INDEX IF NOT EXISTS idx_ratings_target ON ratings(target_type, target_id); + """) + logger.info("Migration: ratings Tabelle bereit.") + + # Tagebuch: mehrere Mediendateien pro Eintrag + conn.executescript(""" + CREATE TABLE IF NOT EXISTS diary_media ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + diary_id INTEGER NOT NULL REFERENCES diary(id) ON DELETE CASCADE, + url TEXT NOT NULL, + media_type TEXT NOT NULL DEFAULT 'image', + sort_order INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + CREATE INDEX IF NOT EXISTS idx_diary_media_entry ON diary_media(diary_id, sort_order); + """) + logger.info("Migration: diary_media Tabelle bereit.") + # Walk-Einladungen (RSVP) conn.executescript(""" CREATE TABLE IF NOT EXISTS walk_invitations ( diff --git a/backend/routes/diary.py b/backend/routes/diary.py index e99b161..b242426 100644 --- a/backend/routes/diary.py +++ b/backend/routes/diary.py @@ -86,10 +86,30 @@ def _set_dog_ids(conn, entry_id: int, dog_ids: list[int]): ) -def _entry_dict(row, dog_ids_map: dict) -> dict: +def _fetch_media_items(conn, entry_ids: list[int]) -> dict: + """Gibt {entry_id: [{url, media_type, sort_order, id}, ...]} 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"WHERE diary_id IN ({ph}) ORDER BY diary_id, sort_order", + entry_ids + ).fetchall() + result = {} + 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"] + }) + return result + + +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["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"], []) return e @@ -122,9 +142,10 @@ async def list_diary(dog_id: int, limit: int = 20, offset: int = 0, (dog_id, dog_id, limit, offset) ).fetchall() ids = [r["id"] for r in rows] - dogs_map = _fetch_dog_ids(conn, ids) + dogs_map = _fetch_dog_ids(conn, ids) + media_map = _fetch_media_items(conn, ids) - return [_entry_dict(r, dogs_map) for r in rows] + return [_entry_dict(r, dogs_map, media_map) for r in rows] @router.post("/{dog_id}/diary", status_code=201) @@ -158,9 +179,10 @@ async def create_diary(dog_id: int, data: DiaryCreate, (dog_id,) ).fetchone() _set_dog_ids(conn, entry["id"], all_dogs) - dogs_map = _fetch_dog_ids(conn, [entry["id"]]) + dogs_map = _fetch_dog_ids(conn, [entry["id"]]) + media_map = _fetch_media_items(conn, [entry["id"]]) - return _entry_dict(entry, dogs_map) + return _entry_dict(entry, dogs_map, media_map) def _haversine_km(lat1, lon1, lat2, lon2) -> float: @@ -302,9 +324,10 @@ async def get_diary(dog_id: int, entry_id: int, user=Depends(get_current_user)): ).fetchone() if not row: raise HTTPException(404, "Eintrag nicht gefunden.") - dogs_map = _fetch_dog_ids(conn, [entry_id]) + dogs_map = _fetch_dog_ids(conn, [entry_id]) + media_map = _fetch_media_items(conn, [entry_id]) - return _entry_dict(row, dogs_map) + return _entry_dict(row, dogs_map, media_map) @router.patch("/{dog_id}/diary/{entry_id}") @@ -345,10 +368,11 @@ async def update_diary(dog_id: int, entry_id: int, data: DiaryUpdate, all_dogs = _validate_dog_ids(data.dog_ids, primary, user["id"], conn) _set_dog_ids(conn, entry_id, all_dogs) - row = conn.execute("SELECT * FROM diary WHERE id=?", (entry_id,)).fetchone() - dogs_map = _fetch_dog_ids(conn, [entry_id]) + row = conn.execute("SELECT * FROM diary WHERE id=?", (entry_id,)).fetchone() + dogs_map = _fetch_dog_ids(conn, [entry_id]) + media_map = _fetch_media_items(conn, [entry_id]) - return _entry_dict(row, dogs_map) + return _entry_dict(row, dogs_map, media_map) @router.delete("/{dog_id}/diary/{entry_id}", status_code=204) @@ -360,6 +384,16 @@ async def delete_diary(dog_id: int, entry_id: int, user=Depends(get_current_user ) +def _guess_media_type(content_type: str, filename: str) -> str: + ct = (content_type or "").lower() + if ct.startswith("video/"): + return "video" + ext = os.path.splitext(filename or "")[1].lower() + if ext in {".mp4", ".mov", ".webm", ".m4v", ".avi"}: + return "video" + return "image" + + @router.post("/{dog_id}/diary/{entry_id}/media") async def upload_media(dog_id: int, entry_id: int, file: UploadFile = File(...), @@ -386,29 +420,58 @@ async def upload_media(dog_id: int, entry_id: int, ".mp4",".mov",".webm",".m4v"}: raise HTTPException(415, "Nur Bilder und Videos erlaubt.") - ext = os.path.splitext(file.filename or "")[1] or ".jpg" - filename = f"diary_{entry_id}_{uuid.uuid4().hex[:8]}{ext}" - path = os.path.join(MEDIA_DIR, "diary", filename) + ext = os.path.splitext(file.filename or "")[1] or ".jpg" + filename = f"diary_{entry_id}_{uuid.uuid4().hex[:8]}{ext}" + path = os.path.join(MEDIA_DIR, "diary", filename) + media_type = _guess_media_type(ct, file.filename or "") os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "wb") as f: f.write(await file.read()) - # Altes Medium von Disk löschen wenn vorhanden - with db() as conn: - old = conn.execute("SELECT media_url FROM diary WHERE id=?", (entry_id,)).fetchone() - if old and old["media_url"]: - old_path = os.path.join(MEDIA_DIR, old["media_url"].lstrip("/media/")) - try: os.remove(old_path) - except OSError: pass - media_url = f"/media/diary/{filename}" - conn.execute("UPDATE diary SET media_url=? WHERE id=?", (media_url, entry_id)) + media_url = f"/media/diary/{filename}" - return {"media_url": media_url} + with db() as conn: + # sort_order = nächste freie Position + max_order = conn.execute( + "SELECT COALESCE(MAX(sort_order), -1) FROM diary_media WHERE diary_id=?", + (entry_id,) + ).fetchone()[0] + conn.execute( + "INSERT INTO diary_media (diary_id, url, media_type, sort_order) VALUES (?,?,?,?)", + (entry_id, media_url, media_type, max_order + 1) + ) + 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} + + +@router.delete("/{dog_id}/diary/{entry_id}/media/{media_id}", status_code=204) +async def delete_media_item(dog_id: int, entry_id: int, media_id: int, + user=Depends(get_current_user)): + with db() as conn: + _own_dog(dog_id, user["id"], conn) + row = conn.execute( + "SELECT dm.id, dm.url 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.") + file_path = os.path.join(MEDIA_DIR, row["url"].lstrip("/media/")) + try: os.remove(file_path) + except OSError: pass + conn.execute("DELETE FROM diary_media WHERE id=?", (media_id,)) @router.delete("/{dog_id}/diary/{entry_id}/media", status_code=204) -async def delete_media(dog_id: int, entry_id: int, user=Depends(get_current_user)): +async def delete_media_legacy(dog_id: int, entry_id: int, user=Depends(get_current_user)): + """Legacy-Endpoint: löscht media_url aus dem diary-Datensatz (Rückwärtskompatibilität).""" with db() as conn: _own_dog(dog_id, user["id"], conn) row = conn.execute( @@ -421,7 +484,3 @@ async def delete_media(dog_id: int, entry_id: int, user=Depends(get_current_user try: os.remove(path) except OSError: pass conn.execute("UPDATE diary SET media_url=NULL WHERE id=?", (entry_id,)) - - - - return unique diff --git a/backend/static/css/components.css b/backend/static/css/components.css index b75b4b4..d29f76d 100644 --- a/backend/static/css/components.css +++ b/backend/static/css/components.css @@ -1065,6 +1065,79 @@ html.modal-open { } .diary-media-pick-btn .ph-icon { font-size: 1.5rem; } +/* Multi-Medien: Formular-Grid (Thumbnails beim Erstellen/Bearbeiten) */ +.diary-media-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(90px, 1fr)); + gap: var(--space-2); + margin-bottom: var(--space-2); +} +.diary-media-thumb-wrap { + position: relative; + aspect-ratio: 1; + border-radius: var(--radius-md); + overflow: hidden; + background: var(--c-surface-2); +} +.diary-media-thumb { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} +.diary-media-thumb-del { + position: absolute; + top: var(--space-1); + right: var(--space-1); + width: 24px; + height: 24px; + border-radius: 50%; + border: none; + background: rgba(0,0,0,.55); + color: #fff; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + line-height: 1; +} +.diary-media-thumb-del .ph-icon { font-size: .9rem; } + +/* Medien-Zähler-Badge auf der Karte */ +.diary-card-photo { position: relative; } +.diary-card-media-count { + position: absolute; + bottom: var(--space-1); + right: var(--space-1); + background: rgba(0,0,0,.55); + color: #fff; + font-size: var(--text-xs); + font-weight: var(--weight-semibold); + padding: 2px 6px; + border-radius: 12px; + pointer-events: none; +} + +/* Detail-Ansicht: horizontale Scroll-Galerie */ +.diary-gallery { + display: flex; + gap: var(--space-2); + overflow-x: auto; + -webkit-overflow-scrolling: touch; + scroll-snap-type: x mandatory; + border-radius: var(--radius-md); +} +.diary-gallery-item { + flex: 0 0 auto; + width: min(80vw, 320px); + max-height: 260px; + object-fit: cover; + border-radius: var(--radius-md); + scroll-snap-align: start; + display: block; +} + .diary-card-video-thumb { width: 100%; height: 100%; diff --git a/backend/static/js/api.js b/backend/static/js/api.js index eba9ac4..46f5ac3 100644 --- a/backend/static/js/api.js +++ b/backend/static/js/api.js @@ -124,6 +124,9 @@ const API = (() => { deleteMedia(dogId, id) { return del(`/dogs/${dogId}/diary/${id}/media`); }, + deleteMediaItem(dogId, entryId, mediaId) { + return del(`/dogs/${dogId}/diary/${entryId}/media/${mediaId}`); + }, nearby(dogId, lat, lon) { return get(`/dogs/${dogId}/diary/nearby?lat=${lat}&lon=${lon}`); }, diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 8ec5eb4..d84fbb9 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,7 +3,7 @@ Router, State-Management, Navigation, Initialisierung. ============================================================ */ -const APP_VER = '179'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '181'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const App = (() => { diff --git a/backend/static/js/pages/diary.js b/backend/static/js/pages/diary.js index f61e741..196eada 100644 --- a/backend/static/js/pages/diary.js +++ b/backend/static/js/pages/diary.js @@ -35,6 +35,18 @@ window.Page_diary = (() => { : `Foto`; } + /** Alle Mediendateien eines Eintrags normalisiert als Array zurückgeben. + * Rückwärtskompatibel: wenn media_items leer, aber media_url gesetzt → altes Format. */ + function _allMedia(entry) { + const items = entry.media_items || []; + if (items.length > 0) return items; + if (entry.media_url) { + return [{ id: null, url: entry.media_url, + media_type: _isVideo(entry.media_url) ? 'video' : 'image', sort_order: 0 }]; + } + return []; + } + const TYPEN = { eintrag: { label: 'Eintrag', icon: '' }, foto: { label: 'Foto', icon: '' }, @@ -292,11 +304,15 @@ window.Page_diary = (() => { const dateStr = e.datum ? UI.time.format(e.datum + 'T00:00:00') : ''; const tags = (e.tags || []).slice(0, 4); - const photo = e.media_url + const allMedia = _allMedia(e); + const firstMedia = allMedia[0] || null; + const mediaCount = allMedia.length; + const photo = firstMedia ? `
- ${_isVideo(e.media_url) + ${firstMedia.media_type === 'video' ? `
` - : `Foto`} + : `Foto`} + ${mediaCount > 1 ? `${mediaCount}` : ''}
` : ''; @@ -362,8 +378,16 @@ window.Page_diary = (() => { const isMile = entry.is_milestone || entry.typ === 'meilenstein'; const tags = (entry.tags || []); - const photo = entry.media_url - ? _mediaHtml(entry.media_url, 'margin-bottom:var(--space-4)') + const allMedia = _allMedia(entry); + const photo = allMedia.length > 0 + ? (allMedia.length === 1 + ? _mediaHtml(allMedia[0].url, 'margin-bottom:var(--space-4)') + : ``) : ''; // Hunde-Anzeige wenn mehrere beteiligt @@ -506,9 +530,9 @@ window.Page_diary = (() => {
-
@@ -525,24 +549,20 @@ window.Page_diary = (() => {
- + - ${isEdit && entry.media_url ? ` -
- ${_mediaHtml(entry.media_url, 'max-height:200px;object-fit:cover')} - -
- ` : ''} + +
+ + + -
+
- -
`; @@ -587,108 +599,118 @@ window.Page_diary = (() => { // Fokus auf Titel-Feld → öffnet Keyboard auf Mobile, zeigt dem User was zu tun ist setTimeout(() => form?.querySelector('[name="titel"]')?.focus(), 150); - // Media-Inputs + Vorschau - const mediaInput = document.getElementById('diary-media-input'); - const cameraInput = document.getElementById('diary-camera-input'); - const photoPreview = document.getElementById('diary-photo-preview'); - const videoPreview = document.getElementById('diary-video-preview'); - const previewWrap = document.getElementById('diary-media-preview'); - const mediaBtns = document.getElementById('diary-media-btns'); + // ---- Multi-Media-Verwaltung ---- + const mediaInput = document.getElementById('diary-media-input'); + const cameraInput = document.getElementById('diary-camera-input'); - function _showPreview(file) { - if (!file) return; - previewWrap.style.display = ''; - if (file.type.startsWith('video/')) { - photoPreview.style.display = 'none'; - videoPreview.style.display = ''; - videoPreview.src = URL.createObjectURL(file); - } else { - videoPreview.style.display = 'none'; - photoPreview.style.display = ''; - photoPreview.src = URL.createObjectURL(file); - } + // Neue Dateien die noch nicht hochgeladen wurden + const _newFiles = []; + + function _renderNewGrid() { + const grid = document.getElementById('diary-new-media-grid'); + if (!grid) return; + if (_newFiles.length === 0) { grid.style.display = 'none'; grid.innerHTML = ''; return; } + grid.style.display = ''; + grid.innerHTML = _newFiles.map((f, i) => { + const objUrl = URL.createObjectURL(f); + const thumb = f.type.startsWith('video/') + ? `` + : ``; + return `
+ ${thumb} + +
`; + }).join(''); + grid.querySelectorAll('.diary-media-thumb-del').forEach(btn => { + btn.addEventListener('click', () => { + const idx = parseInt(btn.dataset.newIdx); + _newFiles.splice(idx, 1); + _renderNewGrid(); + }); + }); } - function _showAlbumBtn(file) { - // Vorherigen Button entfernen falls vorhanden - document.getElementById('diary-save-album-btn')?.remove(); - // Nur anbieten wenn Share-API File-Support hat ODER als Download-Fallback - const canShare = navigator.canShare && navigator.canShare({ files: [file] }); - const canDownload = true; // funktioniert immer als Fallback - if (!canShare && !canDownload) return; - const btn = document.createElement('button'); - btn.type = 'button'; - btn.id = 'diary-save-album-btn'; - btn.className = 'btn btn-secondary btn-sm'; - btn.style.cssText = 'display:flex;align-items:center;gap:var(--space-1);margin-top:var(--space-2);width:100%'; - btn.innerHTML = ` - ${canShare ? 'Zum Fotoalbum hinzufügen' : 'Foto herunterladen'}`; - btn.addEventListener('click', () => UI.saveToAlbum(file)); - previewWrap.after(btn); + // Bestehende Medien im Edit-Modus rendern + function _renderExistingMedia() { + const wrap = document.getElementById('diary-existing-media'); + if (!wrap) return; + const items = isEdit ? _allMedia(entry) : []; + if (items.length === 0) { wrap.innerHTML = ''; return; } + const grid = `
+ ${items.map(m => ` +
+ ${m.media_type === 'video' + ? `` + : ``} + ${m.id != null + ? `` + : ``} +
`).join('')} +
`; + wrap.innerHTML = grid; + wrap.querySelectorAll('.diary-media-thumb-del').forEach(btn => { + btn.addEventListener('click', async () => { + const wrap2 = btn.closest('.diary-media-thumb-wrap'); + const mediaId = btn.dataset.mediaId ? parseInt(btn.dataset.mediaId) : null; + const isLegacy = !!btn.dataset.legacy; + btn.disabled = true; + try { + if (mediaId != null) { + await API.diary.deleteMediaItem(_appState.activeDog.id, entry.id, mediaId); + // aus entry.media_items entfernen + if (entry.media_items) entry.media_items = entry.media_items.filter(m => m.id !== mediaId); + } else if (isLegacy) { + await API.diary.deleteMedia(_appState.activeDog.id, entry.id); + entry.media_url = null; + } + wrap2.remove(); + UI.toast.success('Medium entfernt.'); + } catch (e) { + btn.disabled = false; + UI.toast.error(e.message || 'Fehler beim Löschen.'); + } + }); + }); + } + _renderExistingMedia(); + + function _addFiles(fileList) { + for (const f of fileList) _newFiles.push(f); + _renderNewGrid(); } - mediaInput?.addEventListener('change', () => { - _showPreview(mediaInput.files[0]); - // Kein Album-Button bei Mediathek-Picks (Fotos sind bereits dort) - document.getElementById('diary-save-album-btn')?.remove(); - }); - cameraInput?.addEventListener('change', () => { - // Auswahl in mediaInput spiegeln damit Submit-Handler nur einen Ort abfragt - const dt = new DataTransfer(); - if (cameraInput.files[0]) dt.items.add(cameraInput.files[0]); - mediaInput.files = dt.files; - _showPreview(cameraInput.files[0]); - // Album-Button nach Kamera-Aufnahme anzeigen - if (cameraInput.files[0]) _showAlbumBtn(cameraInput.files[0]); - }); - - document.getElementById('diary-btn-camera') ?.addEventListener('click', () => cameraInput.click()); - document.getElementById('diary-btn-library')?.addEventListener('click', () => { - // Kein capture → iOS zeigt Mediathek-Auswahl, Android zeigt Galerie + function _openPicker(opts = {}) { const tmp = document.createElement('input'); tmp.type = 'file'; tmp.accept = 'image/*,video/*'; tmp.style.display = 'none'; + if (opts.capture) tmp.setAttribute('capture', opts.capture); + if (opts.noAccept) tmp.removeAttribute('accept'); tmp.addEventListener('change', () => { - const dt = new DataTransfer(); - if (tmp.files[0]) dt.items.add(tmp.files[0]); - mediaInput.files = dt.files; - _showPreview(tmp.files[0]); + _addFiles(tmp.files); tmp.remove(); }); document.body.appendChild(tmp); tmp.click(); + } + + cameraInput?.addEventListener('change', () => { + if (cameraInput.files.length) { + _addFiles(cameraInput.files); + cameraInput.value = ''; + } }); - document.getElementById('diary-btn-file')?.addEventListener('click', () => { - mediaInput.removeAttribute('accept'); - mediaInput.click(); - mediaInput.setAttribute('accept', 'image/*,video/*'); + mediaInput?.addEventListener('change', () => { + if (mediaInput.files.length) { + _addFiles(mediaInput.files); + mediaInput.value = ''; + } }); - document.getElementById('diary-preview-clear')?.addEventListener('click', () => { - previewWrap.style.display = 'none'; - photoPreview.src = ''; videoPreview.src = ''; - mediaInput.value = ''; - document.getElementById('diary-save-album-btn')?.remove(); - }); - - // "Entfernen"-Button löscht Medium direkt - document.getElementById('diary-media-delete')?.addEventListener('click', async () => { - const ok = await UI.modal.confirm({ - title: `${_isVideo(entry.media_url) ? 'Video' : 'Foto'} entfernen?`, - message: 'Das Medium wird dauerhaft gelöscht.', - confirmText: 'Entfernen', danger: true, - }); - if (!ok) return; - try { - await API.diary.deleteMedia(_appState.activeDog.id, entry.id); - entry.media_url = null; - const mediaDiv = document.getElementById('diary-current-media'); - if (mediaDiv) mediaDiv.remove(); - const replaceBtn = document.getElementById('diary-media-replace'); - if (replaceBtn) replaceBtn.remove(); - mediaInput.style.display = ''; - UI.toast.success('Medium entfernt.'); - } catch (e) { UI.toast.error(e.message || 'Fehler.'); } - }); + document.getElementById('diary-btn-camera') ?.addEventListener('click', () => cameraInput.click()); + document.getElementById('diary-btn-library')?.addEventListener('click', () => _openPicker({})); + document.getElementById('diary-btn-file') ?.addEventListener('click', () => _openPicker({ noAccept: true })); document.getElementById('diary-form-cancel')?.addEventListener('click', UI.modal.close); @@ -752,7 +774,7 @@ window.Page_diary = (() => { _locLat = null; _locLon = null; _locName = null; document.getElementById('diary-location-chip-wrap').style.display = 'none'; document.getElementById('diary-location-suggestions').style.display = 'none'; - document.getElementById('diary-location-btn-label').textContent = 'GPS → POI suchen'; + document.getElementById('diary-location-btn-label').textContent = 'POI suchen'; if (_miniMarker) { _miniMarker.remove(); _miniMarker = null; } if (_miniMap) { _miniMap.setView([48.0, 11.9], 7); _setMapEditing(false); } }); @@ -882,33 +904,43 @@ window.Page_diary = (() => { location_name: _locName, }; - const mediaFile = mediaInput?.files[0]; + async function _uploadNewFiles(entryId) { + let failCount = 0; + const uploaded = []; + for (const file of _newFiles) { + try { + const formData = new FormData(); + formData.append('file', file); + const m = await API.diary.uploadMedia(_appState.activeDog.id, entryId, formData); + uploaded.push(m); + } catch { + failCount++; + } + } + if (failCount > 0) { + UI.toast.warning(`${failCount} Medium${failCount > 1 ? 'en' : ''} konnte${failCount > 1 ? 'n' : ''} nicht hochgeladen werden.`); + } + return uploaded; + } if (isEdit) { const updated = await API.diary.update(_appState.activeDog.id, entry.id, payload); - if (mediaFile) { - try { - const fd2 = new FormData(); - fd2.append('file', mediaFile); - const media = await API.diary.uploadMedia(_appState.activeDog.id, entry.id, fd2); - updated.media_url = media.media_url; - } catch { - UI.toast.warning('Gespeichert, Medium konnte nicht hochgeladen werden.'); - } + if (_newFiles.length > 0) { + const uploaded = await _uploadNewFiles(entry.id); + if (!updated.media_items) updated.media_items = []; + updated.media_items.push(...uploaded); + } else { + // media_items aus dem aktuellen entry-State übernehmen (evtl. gelöscht via X-Button) + updated.media_items = entry.media_items || updated.media_items || []; + updated.media_url = entry.media_url ?? updated.media_url; } _updateEntryInList(updated); UI.toast.success('Eintrag gespeichert.'); } else { const created = await API.diary.create(_appState.activeDog.id, payload); - if (mediaFile) { - try { - const fd2 = new FormData(); - fd2.append('file', mediaFile); - const media = await API.diary.uploadMedia(_appState.activeDog.id, created.id, fd2); - created.media_url = media.media_url; - } catch { - UI.toast.warning('Eintrag erstellt, Medium konnte nicht hochgeladen werden.'); - } + if (_newFiles.length > 0) { + const uploaded = await _uploadNewFiles(created.id); + created.media_items = uploaded; } _entries.unshift(created); UI.toast.success('Eintrag erstellt.'); diff --git a/backend/static/sw.js b/backend/static/sw.js index 6bee425..273e390 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -3,7 +3,7 @@ Offline-Cache + Push Notifications + Tile-Cache ============================================================ */ -const CACHE_VERSION = 'by-v210'; +const CACHE_VERSION = 'by-v211'; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten