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:
parent
63ab092f5e
commit
fa0fcbf8c9
7 changed files with 196 additions and 21 deletions
|
|
@ -6,18 +6,27 @@ SQLite mit WAL-Modus (bewährt von akku-werkstatt).
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
import unicodedata
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
DB_PATH = os.getenv("DB_PATH", "/data/banyaro.db")
|
DB_PATH = os.getenv("DB_PATH", "/data/banyaro.db")
|
||||||
|
|
||||||
|
|
||||||
|
def _norm(s):
|
||||||
|
"""Diakritika entfernen + lowercase — für akzentunabhängige Suche."""
|
||||||
|
if not s:
|
||||||
|
return ''
|
||||||
|
return unicodedata.normalize('NFKD', str(s)).encode('ascii', 'ignore').decode('ascii').lower()
|
||||||
|
|
||||||
|
|
||||||
def get_connection() -> sqlite3.Connection:
|
def get_connection() -> sqlite3.Connection:
|
||||||
conn = sqlite3.connect(DB_PATH, check_same_thread=False)
|
conn = sqlite3.connect(DB_PATH, check_same_thread=False)
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
conn.execute("PRAGMA journal_mode=WAL")
|
conn.execute("PRAGMA journal_mode=WAL")
|
||||||
conn.execute("PRAGMA foreign_keys=ON")
|
conn.execute("PRAGMA foreign_keys=ON")
|
||||||
conn.execute("PRAGMA busy_timeout=5000")
|
conn.execute("PRAGMA busy_timeout=5000")
|
||||||
|
conn.create_function('norm', 1, _norm)
|
||||||
return conn
|
return conn
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -471,6 +480,8 @@ def _migrate(conn_factory):
|
||||||
("walks", "anz_bewertungen", "INTEGER DEFAULT 0"),
|
("walks", "anz_bewertungen", "INTEGER DEFAULT 0"),
|
||||||
("sitters", "bewertung", "REAL DEFAULT 0"),
|
("sitters", "bewertung", "REAL DEFAULT 0"),
|
||||||
("sitters", "anz_bewertungen", "INTEGER DEFAULT 0"),
|
("sitters", "anz_bewertungen", "INTEGER DEFAULT 0"),
|
||||||
|
# Tagebuch-Medien: Cover-Bild markieren
|
||||||
|
("diary_media", "is_cover", "INTEGER NOT NULL DEFAULT 0"),
|
||||||
]
|
]
|
||||||
with conn_factory() as conn:
|
with conn_factory() as conn:
|
||||||
for table, column, col_type in migrations:
|
for table, column, col_type in migrations:
|
||||||
|
|
@ -774,6 +785,20 @@ def _migrate(conn_factory):
|
||||||
""")
|
""")
|
||||||
logger.info("Migration: diary_media Tabelle bereit.")
|
logger.info("Migration: diary_media Tabelle bereit.")
|
||||||
|
|
||||||
|
# Gesundheit: mehrere Mediendateien pro Eintrag
|
||||||
|
conn.executescript("""
|
||||||
|
CREATE TABLE IF NOT EXISTS health_media (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
health_id INTEGER NOT NULL REFERENCES health(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_health_media_entry ON health_media(health_id, sort_order);
|
||||||
|
""")
|
||||||
|
logger.info("Migration: health_media Tabelle bereit.")
|
||||||
|
|
||||||
# Walk-Einladungen (RSVP)
|
# Walk-Einladungen (RSVP)
|
||||||
conn.executescript("""
|
conn.executescript("""
|
||||||
CREATE TABLE IF NOT EXISTS walk_invitations (
|
CREATE TABLE IF NOT EXISTS walk_invitations (
|
||||||
|
|
|
||||||
|
|
@ -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:
|
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:
|
if not entry_ids:
|
||||||
return {}
|
return {}
|
||||||
ph = ",".join("?" * len(entry_ids))
|
ph = ",".join("?" * len(entry_ids))
|
||||||
rows = conn.execute(
|
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",
|
f"WHERE diary_id IN ({ph}) ORDER BY diary_id, sort_order",
|
||||||
entry_ids
|
entry_ids
|
||||||
).fetchall()
|
).fetchall()
|
||||||
|
|
@ -100,7 +100,8 @@ def _fetch_media_items(conn, entry_ids: list[int]) -> dict:
|
||||||
for r in rows:
|
for r in rows:
|
||||||
result.setdefault(r["diary_id"], []).append({
|
result.setdefault(r["diary_id"], []).append({
|
||||||
"id": r["id"], "url": r["url"],
|
"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
|
return result
|
||||||
|
|
||||||
|
|
@ -109,7 +110,11 @@ def _entry_dict(row, dog_ids_map: dict, media_map: dict = None) -> dict:
|
||||||
e = dict(row)
|
e = dict(row)
|
||||||
e["tags"] = json.loads(e["tags"]) if e["tags"] else []
|
e["tags"] = json.loads(e["tags"]) if e["tags"] else []
|
||||||
e["dog_ids"] = dog_ids_map.get(e["id"], [e["dog_id"]])
|
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
|
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=?",
|
"SELECT COALESCE(MAX(sort_order), -1) FROM diary_media WHERE diary_id=?",
|
||||||
(entry_id,)
|
(entry_id,)
|
||||||
).fetchone()[0]
|
).fetchone()[0]
|
||||||
|
# Erstes Item eines Eintrags wird automatisch Cover
|
||||||
|
is_cover = 1 if max_order == -1 else 0
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO diary_media (diary_id, url, media_type, sort_order) VALUES (?,?,?,?)",
|
"INSERT INTO diary_media (diary_id, url, media_type, sort_order, is_cover) VALUES (?,?,?,?,?)",
|
||||||
(entry_id, media_url, media_type, max_order + 1)
|
(entry_id, media_url, media_type, max_order + 1, is_cover)
|
||||||
)
|
)
|
||||||
new_id = conn.execute(
|
new_id = conn.execute(
|
||||||
"SELECT id FROM diary_media WHERE diary_id=? ORDER BY id DESC LIMIT 1",
|
"SELECT id FROM diary_media WHERE diary_id=? ORDER BY id DESC LIMIT 1",
|
||||||
(entry_id,)
|
(entry_id,)
|
||||||
).fetchone()["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)
|
@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)
|
try: os.remove(path)
|
||||||
except OSError: pass
|
except OSError: pass
|
||||||
conn.execute("UPDATE diary SET media_url=NULL WHERE id=?", (entry_id,))
|
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}
|
||||||
|
|
|
||||||
|
|
@ -1149,6 +1149,40 @@ html.modal-open {
|
||||||
font-size: 3rem;
|
font-size: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Cover-Stern-Button auf Gallery- und Thumbnail-Items */
|
||||||
|
.diary-gallery-wrap {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.diary-cover-btn {
|
||||||
|
position: absolute;
|
||||||
|
bottom: var(--space-1);
|
||||||
|
left: var(--space-1);
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: none;
|
||||||
|
background: rgba(0,0,0,.50);
|
||||||
|
color: rgba(255,255,255,.55);
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1;
|
||||||
|
z-index: 2;
|
||||||
|
transition: color .15s, background .15s;
|
||||||
|
}
|
||||||
|
.diary-cover-btn--active {
|
||||||
|
color: #f5c518;
|
||||||
|
background: rgba(0,0,0,.65);
|
||||||
|
}
|
||||||
|
.diary-cover-btn--form {
|
||||||
|
bottom: var(--space-1);
|
||||||
|
left: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
/* Card Body */
|
/* Card Body */
|
||||||
.diary-card-body {
|
.diary-card-body {
|
||||||
padding: var(--space-3) var(--space-4);
|
padding: var(--space-3) var(--space-4);
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,9 @@ const API = (() => {
|
||||||
deleteMediaItem(dogId, entryId, mediaId) {
|
deleteMediaItem(dogId, entryId, mediaId) {
|
||||||
return del(`/dogs/${dogId}/diary/${entryId}/media/${mediaId}`);
|
return del(`/dogs/${dogId}/diary/${entryId}/media/${mediaId}`);
|
||||||
},
|
},
|
||||||
|
setCover(dogId, entryId, mediaId) {
|
||||||
|
return patch(`/dogs/${dogId}/diary/${entryId}/media/${mediaId}/cover`, {});
|
||||||
|
},
|
||||||
nearby(dogId, lat, lon) {
|
nearby(dogId, lat, lon) {
|
||||||
return get(`/dogs/${dogId}/diary/nearby?lat=${lat}&lon=${lon}`);
|
return get(`/dogs/${dogId}/diary/nearby?lat=${lat}&lon=${lon}`);
|
||||||
},
|
},
|
||||||
|
|
@ -149,6 +152,12 @@ const API = (() => {
|
||||||
deleteDocument(dogId, id) {
|
deleteDocument(dogId, id) {
|
||||||
return del(`/dogs/${dogId}/health/${id}/dokument`);
|
return del(`/dogs/${dogId}/health/${id}/dokument`);
|
||||||
},
|
},
|
||||||
|
uploadMedia(dogId, entryId, formData) {
|
||||||
|
return upload(`/dogs/${dogId}/health/${entryId}/media`, formData);
|
||||||
|
},
|
||||||
|
deleteMedia(dogId, entryId, mediaId) {
|
||||||
|
return del(`/dogs/${dogId}/health/${entryId}/media/${mediaId}`);
|
||||||
|
},
|
||||||
kiZusammenfassung(dogId) {
|
kiZusammenfassung(dogId) {
|
||||||
return post(`/dogs/${dogId}/health/ki-zusammenfassung`);
|
return post(`/dogs/${dogId}/health/ki-zusammenfassung`);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Router, State-Management, Navigation, Initialisierung.
|
Router, State-Management, Navigation, Initialisierung.
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const APP_VER = '181'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
const APP_VER = '186'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||||
|
|
||||||
const App = (() => {
|
const App = (() => {
|
||||||
|
|
||||||
|
|
@ -420,7 +420,8 @@ const App = (() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateNotifBadge();
|
_updateNotifBadge();
|
||||||
setInterval(_updateNotifBadge, 60_000);
|
_updateChatBadge();
|
||||||
|
setInterval(() => { _updateNotifBadge(); _updateChatBadge(); }, 30_000);
|
||||||
|
|
||||||
const pendingInvite = sessionStorage.getItem('pending_invite');
|
const pendingInvite = sessionStorage.getItem('pending_invite');
|
||||||
if (pendingInvite) {
|
if (pendingInvite) {
|
||||||
|
|
@ -440,6 +441,18 @@ const App = (() => {
|
||||||
} catch { /* ignorieren */ }
|
} catch { /* ignorieren */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function _updateChatBadge() {
|
||||||
|
if (!state.user) return;
|
||||||
|
try {
|
||||||
|
const convs = await API.chat.conversations();
|
||||||
|
const total = convs.reduce((s, c) => s + (c.unread_count || 0), 0);
|
||||||
|
const badge = document.getElementById('chat-badge');
|
||||||
|
if (!badge) return;
|
||||||
|
badge.textContent = total;
|
||||||
|
badge.style.display = total > 0 ? '' : 'none';
|
||||||
|
} catch { /* ignorieren */ }
|
||||||
|
}
|
||||||
|
|
||||||
function _onLoggedOut() {
|
function _onLoggedOut() {
|
||||||
state.user = null;
|
state.user = null;
|
||||||
state.dogs = [];
|
state.dogs = [];
|
||||||
|
|
|
||||||
|
|
@ -304,14 +304,14 @@ window.Page_diary = (() => {
|
||||||
const dateStr = e.datum ? UI.time.format(e.datum + 'T00:00:00') : '';
|
const dateStr = e.datum ? UI.time.format(e.datum + 'T00:00:00') : '';
|
||||||
const tags = (e.tags || []).slice(0, 4);
|
const tags = (e.tags || []).slice(0, 4);
|
||||||
|
|
||||||
const allMedia = _allMedia(e);
|
const allMedia = _allMedia(e);
|
||||||
const firstMedia = allMedia[0] || null;
|
const coverMedia = allMedia.find(m => m.is_cover) || allMedia[0] || null;
|
||||||
const mediaCount = allMedia.length;
|
const mediaCount = allMedia.length;
|
||||||
const photo = firstMedia
|
const photo = coverMedia
|
||||||
? `<div class="diary-card-photo">
|
? `<div class="diary-card-photo">
|
||||||
${firstMedia.media_type === 'video'
|
${coverMedia.media_type === 'video'
|
||||||
? `<div class="diary-card-video-thumb"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#play-circle"></use></svg></div>`
|
? `<div class="diary-card-video-thumb"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#play-circle"></use></svg></div>`
|
||||||
: `<img src="${firstMedia.url}" alt="Foto" loading="lazy">`}
|
: `<img src="${e.cover_url || coverMedia.url}" alt="Foto" loading="lazy">`}
|
||||||
${mediaCount > 1 ? `<span class="diary-card-media-count">${mediaCount}</span>` : ''}
|
${mediaCount > 1 ? `<span class="diary-card-media-count">${mediaCount}</span>` : ''}
|
||||||
</div>`
|
</div>`
|
||||||
: '';
|
: '';
|
||||||
|
|
@ -381,12 +381,21 @@ window.Page_diary = (() => {
|
||||||
const allMedia = _allMedia(entry);
|
const allMedia = _allMedia(entry);
|
||||||
const photo = allMedia.length > 0
|
const photo = allMedia.length > 0
|
||||||
? (allMedia.length === 1
|
? (allMedia.length === 1
|
||||||
? _mediaHtml(allMedia[0].url, 'margin-bottom:var(--space-4)')
|
? `<div style="position:relative;margin-bottom:var(--space-4)">
|
||||||
|
${_mediaHtml(allMedia[0].url)}
|
||||||
|
</div>`
|
||||||
: `<div class="diary-gallery" style="margin-bottom:var(--space-4)">
|
: `<div class="diary-gallery" style="margin-bottom:var(--space-4)">
|
||||||
${allMedia.map(m => m.media_type === 'video'
|
${allMedia.map(m => `
|
||||||
? `<video src="${m.url}" controls playsinline class="diary-gallery-item"></video>`
|
<div class="diary-gallery-wrap" style="position:relative">
|
||||||
: `<img src="${m.url}" alt="Foto" class="diary-gallery-item">`
|
${m.media_type === 'video'
|
||||||
).join('')}
|
? `<video src="${m.url}" controls playsinline class="diary-gallery-item"></video>`
|
||||||
|
: `<img src="${m.url}" alt="Foto" class="diary-gallery-item">`}
|
||||||
|
<button type="button"
|
||||||
|
class="diary-cover-btn${m.is_cover ? ' diary-cover-btn--active' : ''}"
|
||||||
|
data-media-id="${m.id}"
|
||||||
|
aria-label="${m.is_cover ? 'Cover-Bild' : 'Als Cover setzen'}"
|
||||||
|
title="${m.is_cover ? 'Cover-Bild' : 'Als Cover setzen'}">⭐</button>
|
||||||
|
</div>`).join('')}
|
||||||
</div>`)
|
</div>`)
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
|
|
@ -434,6 +443,33 @@ window.Page_diary = (() => {
|
||||||
|
|
||||||
UI.modal.open({ title: entry.titel || typ.label, body });
|
UI.modal.open({ title: entry.titel || typ.label, body });
|
||||||
|
|
||||||
|
// Stern-Buttons: Cover-Bild setzen
|
||||||
|
document.querySelectorAll('.diary-cover-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async (ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const mediaId = parseInt(btn.dataset.mediaId);
|
||||||
|
try {
|
||||||
|
await API.diary.setCover(_appState.activeDog.id, entry.id, mediaId);
|
||||||
|
// Lokalen State aktualisieren
|
||||||
|
if (entry.media_items) {
|
||||||
|
entry.media_items.forEach(m => { m.is_cover = m.id === mediaId ? 1 : 0; });
|
||||||
|
}
|
||||||
|
entry.cover_url = (entry.media_items || []).find(m => m.id === mediaId)?.url || null;
|
||||||
|
_updateEntryInList(entry);
|
||||||
|
// Alle Sterne im Modal aktualisieren
|
||||||
|
document.querySelectorAll('.diary-cover-btn').forEach(b => {
|
||||||
|
const active = parseInt(b.dataset.mediaId) === mediaId;
|
||||||
|
b.classList.toggle('diary-cover-btn--active', active);
|
||||||
|
b.setAttribute('aria-label', active ? 'Cover-Bild' : 'Als Cover setzen');
|
||||||
|
b.setAttribute('title', active ? 'Cover-Bild' : 'Als Cover setzen');
|
||||||
|
});
|
||||||
|
UI.toast.success('Cover-Bild gesetzt.');
|
||||||
|
} catch {
|
||||||
|
UI.toast.error('Cover konnte nicht gesetzt werden.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById('detail-edit')?.addEventListener('click', async () => {
|
document.getElementById('detail-edit')?.addEventListener('click', async () => {
|
||||||
UI.modal.close();
|
UI.modal.close();
|
||||||
// Nur nachladen wenn location_name/gps_lat fehlen (älterer In-Memory-Eintrag)
|
// Nur nachladen wenn location_name/gps_lat fehlen (älterer In-Memory-Eintrag)
|
||||||
|
|
@ -645,7 +681,12 @@ window.Page_diary = (() => {
|
||||||
: `<img src="${m.url}" alt="" class="diary-media-thumb">`}
|
: `<img src="${m.url}" alt="" class="diary-media-thumb">`}
|
||||||
${m.id != null
|
${m.id != null
|
||||||
? `<button type="button" class="diary-media-thumb-del" data-media-id="${m.id}"
|
? `<button type="button" class="diary-media-thumb-del" data-media-id="${m.id}"
|
||||||
aria-label="Entfernen">${UI.icon('x')}</button>`
|
aria-label="Entfernen">${UI.icon('x')}</button>
|
||||||
|
<button type="button"
|
||||||
|
class="diary-cover-btn diary-cover-btn--form${m.is_cover ? ' diary-cover-btn--active' : ''}"
|
||||||
|
data-media-id="${m.id}"
|
||||||
|
aria-label="${m.is_cover ? 'Cover-Bild' : 'Als Cover setzen'}"
|
||||||
|
title="${m.is_cover ? 'Cover-Bild' : 'Als Cover setzen'}">⭐</button>`
|
||||||
: `<button type="button" class="diary-media-thumb-del" data-legacy="1"
|
: `<button type="button" class="diary-media-thumb-del" data-legacy="1"
|
||||||
aria-label="Entfernen">${UI.icon('x')}</button>`}
|
aria-label="Entfernen">${UI.icon('x')}</button>`}
|
||||||
</div>`).join('')}
|
</div>`).join('')}
|
||||||
|
|
@ -674,6 +715,30 @@ window.Page_diary = (() => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
// Stern-Buttons im Edit-Formular
|
||||||
|
wrap.querySelectorAll('.diary-cover-btn--form').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async () => {
|
||||||
|
const mediaId = parseInt(btn.dataset.mediaId);
|
||||||
|
try {
|
||||||
|
await API.diary.setCover(_appState.activeDog.id, entry.id, mediaId);
|
||||||
|
if (entry.media_items) {
|
||||||
|
entry.media_items.forEach(m => { m.is_cover = m.id === mediaId ? 1 : 0; });
|
||||||
|
}
|
||||||
|
entry.cover_url = (entry.media_items || []).find(m => m.id === mediaId)?.url || null;
|
||||||
|
_updateEntryInList(entry);
|
||||||
|
// Alle Sterne in diesem Formular aktualisieren
|
||||||
|
wrap.querySelectorAll('.diary-cover-btn--form').forEach(b => {
|
||||||
|
const active = parseInt(b.dataset.mediaId) === mediaId;
|
||||||
|
b.classList.toggle('diary-cover-btn--active', active);
|
||||||
|
b.setAttribute('aria-label', active ? 'Cover-Bild' : 'Als Cover setzen');
|
||||||
|
b.setAttribute('title', active ? 'Cover-Bild' : 'Als Cover setzen');
|
||||||
|
});
|
||||||
|
UI.toast.success('Cover-Bild gesetzt.');
|
||||||
|
} catch {
|
||||||
|
UI.toast.error('Cover konnte nicht gesetzt werden.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
_renderExistingMedia();
|
_renderExistingMedia();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Offline-Cache + Push Notifications + Tile-Cache
|
Offline-Cache + Push Notifications + Tile-Cache
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const CACHE_VERSION = 'by-v211';
|
const CACHE_VERSION = 'by-v212';
|
||||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue