Feature: Tagebuch Multi-Medien (beliebig viele Fotos/Videos pro Eintrag)
- Backend: neue Tabelle diary_media (Migration), upload_media schreibt
jetzt in diary_media statt media_url; neuer DELETE-Endpoint
/diary/{id}/media/{media_id}; alle GET-Endpoints liefern media_items[].
- Frontend: Multi-Upload-Grid im Formular mit Vorschau und X-Button
zum Entfernen vor dem Speichern; bestehende Medien im Edit-Modus
einzeln löschbar; Detail-Ansicht zeigt horizontale Scroll-Galerie
bei mehreren Medien; Karten-Badge zeigt Anzahl bei > 1 Medium.
- Rückwärtskompatibilität: Einträge mit media_url werden weiterhin
korrekt angezeigt.
- SW by-v211, APP_VER 181
This commit is contained in:
parent
6581a9a88c
commit
63ab092f5e
7 changed files with 367 additions and 165 deletions
|
|
@ -466,6 +466,11 @@ def _migrate(conn_factory):
|
||||||
("dogs", "foto_offset_y", "REAL NOT NULL DEFAULT 0.0"),
|
("dogs", "foto_offset_y", "REAL NOT NULL DEFAULT 0.0"),
|
||||||
# Tagebuch: Ortsname (POI/Adresse)
|
# Tagebuch: Ortsname (POI/Adresse)
|
||||||
("diary", "location_name", "TEXT"),
|
("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:
|
with conn_factory() as conn:
|
||||||
for table, column, col_type in migrations:
|
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);
|
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)
|
# Walk-Einladungen (RSVP)
|
||||||
conn.executescript("""
|
conn.executescript("""
|
||||||
CREATE TABLE IF NOT EXISTS walk_invitations (
|
CREATE TABLE IF NOT EXISTS walk_invitations (
|
||||||
|
|
|
||||||
|
|
@ -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 = 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"], [])
|
||||||
return e
|
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)
|
(dog_id, dog_id, limit, offset)
|
||||||
).fetchall()
|
).fetchall()
|
||||||
ids = [r["id"] for r in rows]
|
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)
|
@router.post("/{dog_id}/diary", status_code=201)
|
||||||
|
|
@ -158,9 +179,10 @@ async def create_diary(dog_id: int, data: DiaryCreate,
|
||||||
(dog_id,)
|
(dog_id,)
|
||||||
).fetchone()
|
).fetchone()
|
||||||
_set_dog_ids(conn, entry["id"], all_dogs)
|
_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:
|
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()
|
).fetchone()
|
||||||
if not row:
|
if not row:
|
||||||
raise HTTPException(404, "Eintrag nicht gefunden.")
|
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}")
|
@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)
|
all_dogs = _validate_dog_ids(data.dog_ids, primary, user["id"], conn)
|
||||||
_set_dog_ids(conn, entry_id, all_dogs)
|
_set_dog_ids(conn, entry_id, all_dogs)
|
||||||
|
|
||||||
row = conn.execute("SELECT * FROM diary WHERE id=?", (entry_id,)).fetchone()
|
row = conn.execute("SELECT * FROM diary WHERE id=?", (entry_id,)).fetchone()
|
||||||
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.delete("/{dog_id}/diary/{entry_id}", status_code=204)
|
@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")
|
@router.post("/{dog_id}/diary/{entry_id}/media")
|
||||||
async def upload_media(dog_id: int, entry_id: int,
|
async def upload_media(dog_id: int, entry_id: int,
|
||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
|
|
@ -386,29 +420,58 @@ async def upload_media(dog_id: int, entry_id: int,
|
||||||
".mp4",".mov",".webm",".m4v"}:
|
".mp4",".mov",".webm",".m4v"}:
|
||||||
raise HTTPException(415, "Nur Bilder und Videos erlaubt.")
|
raise HTTPException(415, "Nur Bilder und Videos erlaubt.")
|
||||||
|
|
||||||
ext = os.path.splitext(file.filename or "")[1] or ".jpg"
|
ext = os.path.splitext(file.filename or "")[1] or ".jpg"
|
||||||
filename = f"diary_{entry_id}_{uuid.uuid4().hex[:8]}{ext}"
|
filename = f"diary_{entry_id}_{uuid.uuid4().hex[:8]}{ext}"
|
||||||
path = os.path.join(MEDIA_DIR, "diary", filename)
|
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)
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||||
|
|
||||||
with open(path, "wb") as f:
|
with open(path, "wb") as f:
|
||||||
f.write(await file.read())
|
f.write(await file.read())
|
||||||
|
|
||||||
# Altes Medium von Disk löschen wenn vorhanden
|
media_url = f"/media/diary/{filename}"
|
||||||
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))
|
|
||||||
|
|
||||||
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)
|
@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:
|
with db() as conn:
|
||||||
_own_dog(dog_id, user["id"], conn)
|
_own_dog(dog_id, user["id"], conn)
|
||||||
row = conn.execute(
|
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)
|
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,))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return unique
|
|
||||||
|
|
|
||||||
|
|
@ -1065,6 +1065,79 @@ html.modal-open {
|
||||||
}
|
}
|
||||||
.diary-media-pick-btn .ph-icon { font-size: 1.5rem; }
|
.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 {
|
.diary-card-video-thumb {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,9 @@ const API = (() => {
|
||||||
deleteMedia(dogId, id) {
|
deleteMedia(dogId, id) {
|
||||||
return del(`/dogs/${dogId}/diary/${id}/media`);
|
return del(`/dogs/${dogId}/diary/${id}/media`);
|
||||||
},
|
},
|
||||||
|
deleteMediaItem(dogId, entryId, mediaId) {
|
||||||
|
return del(`/dogs/${dogId}/diary/${entryId}/media/${mediaId}`);
|
||||||
|
},
|
||||||
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}`);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Router, State-Management, Navigation, Initialisierung.
|
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 = (() => {
|
const App = (() => {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,18 @@ window.Page_diary = (() => {
|
||||||
: `<img src="${url}" alt="Foto" style="width:100%;border-radius:var(--radius-md);${style}">`;
|
: `<img src="${url}" alt="Foto" style="width:100%;border-radius:var(--radius-md);${style}">`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 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 = {
|
const TYPEN = {
|
||||||
eintrag: { label: 'Eintrag', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#book-open"></use></svg>' },
|
eintrag: { label: 'Eintrag', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#book-open"></use></svg>' },
|
||||||
foto: { label: 'Foto', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#camera"></use></svg>' },
|
foto: { label: 'Foto', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#camera"></use></svg>' },
|
||||||
|
|
@ -292,11 +304,15 @@ 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 photo = e.media_url
|
const allMedia = _allMedia(e);
|
||||||
|
const firstMedia = allMedia[0] || null;
|
||||||
|
const mediaCount = allMedia.length;
|
||||||
|
const photo = firstMedia
|
||||||
? `<div class="diary-card-photo">
|
? `<div class="diary-card-photo">
|
||||||
${_isVideo(e.media_url)
|
${firstMedia.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="${e.media_url}" alt="Foto" loading="lazy">`}
|
: `<img src="${firstMedia.url}" alt="Foto" loading="lazy">`}
|
||||||
|
${mediaCount > 1 ? `<span class="diary-card-media-count">${mediaCount}</span>` : ''}
|
||||||
</div>`
|
</div>`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
|
|
@ -362,8 +378,16 @@ window.Page_diary = (() => {
|
||||||
const isMile = entry.is_milestone || entry.typ === 'meilenstein';
|
const isMile = entry.is_milestone || entry.typ === 'meilenstein';
|
||||||
const tags = (entry.tags || []);
|
const tags = (entry.tags || []);
|
||||||
|
|
||||||
const photo = entry.media_url
|
const allMedia = _allMedia(entry);
|
||||||
? _mediaHtml(entry.media_url, 'margin-bottom:var(--space-4)')
|
const photo = allMedia.length > 0
|
||||||
|
? (allMedia.length === 1
|
||||||
|
? _mediaHtml(allMedia[0].url, 'margin-bottom:var(--space-4)')
|
||||||
|
: `<div class="diary-gallery" style="margin-bottom:var(--space-4)">
|
||||||
|
${allMedia.map(m => m.media_type === 'video'
|
||||||
|
? `<video src="${m.url}" controls playsinline class="diary-gallery-item"></video>`
|
||||||
|
: `<img src="${m.url}" alt="Foto" class="diary-gallery-item">`
|
||||||
|
).join('')}
|
||||||
|
</div>`)
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
// Hunde-Anzeige wenn mehrere beteiligt
|
// Hunde-Anzeige wenn mehrere beteiligt
|
||||||
|
|
@ -506,9 +530,9 @@ window.Page_diary = (() => {
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;gap:var(--space-2);margin-top:var(--space-2)">
|
<div style="display:flex;gap:var(--space-2);margin-top:var(--space-2)">
|
||||||
<button type="button" class="btn btn-danger" id="diary-coords-clear">Ort entfernen</button>
|
<button type="button" class="btn btn-danger" id="diary-coords-clear">Ort entfernen</button>
|
||||||
<button type="button" class="btn btn-secondary flex-1" id="diary-location-btn">
|
<button type="button" class="btn btn-secondary btn-sm" id="diary-location-btn">
|
||||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#map-pin"></use></svg>
|
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#map-pin"></use></svg>
|
||||||
<span id="diary-location-btn-label">${entry?.gps_lat ? 'POI suchen' : 'GPS → POI suchen'}</span>
|
<span id="diary-location-btn-label">POI suchen</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="diary-location-suggestions" style="display:none;margin-top:var(--space-2)"></div>
|
<div id="diary-location-suggestions" style="display:none;margin-top:var(--space-2)"></div>
|
||||||
|
|
@ -525,24 +549,20 @@ window.Page_diary = (() => {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Foto / Video <span style="color:var(--c-text-secondary)">(optional)</span></label>
|
<label class="form-label">Fotos / Videos <span style="color:var(--c-text-secondary)">(optional)</span></label>
|
||||||
|
|
||||||
${isEdit && entry.media_url ? `
|
<!-- Bestehende Medien (Edit-Modus) -->
|
||||||
<div id="diary-current-media" style="position:relative;margin-bottom:var(--space-2)">
|
<div id="diary-existing-media"></div>
|
||||||
${_mediaHtml(entry.media_url, 'max-height:200px;object-fit:cover')}
|
|
||||||
<button type="button" class="btn btn-danger btn-sm" id="diary-media-delete"
|
<!-- Neue Medien: Vorschau-Grid -->
|
||||||
style="position:absolute;top:var(--space-2);right:var(--space-2)">
|
<div id="diary-new-media-grid" class="diary-media-grid" style="display:none"></div>
|
||||||
${UI.icon('trash')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
|
|
||||||
<!-- versteckte Inputs -->
|
<!-- versteckte Inputs -->
|
||||||
<input type="file" id="diary-media-input" accept="image/*,video/*" style="display:none">
|
<input type="file" id="diary-media-input" accept="image/*,video/*" style="display:none">
|
||||||
<input type="file" id="diary-camera-input" accept="image/*,video/*" capture="environment" style="display:none">
|
<input type="file" id="diary-camera-input" accept="image/*,video/*" capture="environment" style="display:none">
|
||||||
|
|
||||||
<!-- Auswahlbuttons — immer sichtbar -->
|
<!-- Auswahlbuttons — immer sichtbar -->
|
||||||
<div id="diary-media-btns" class="diary-media-picker">
|
<div class="diary-media-picker" style="margin-top:var(--space-2)">
|
||||||
<button type="button" class="diary-media-pick-btn" id="diary-btn-camera">
|
<button type="button" class="diary-media-pick-btn" id="diary-btn-camera">
|
||||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#camera"></use></svg>
|
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#camera"></use></svg>
|
||||||
Kamera
|
Kamera
|
||||||
|
|
@ -556,14 +576,6 @@ window.Page_diary = (() => {
|
||||||
Datei
|
Datei
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="diary-media-preview" style="display:none;margin-top:var(--space-2);position:relative">
|
|
||||||
<img id="diary-photo-preview" style="display:none;width:100%;max-height:200px;object-fit:cover;border-radius:var(--radius-md)">
|
|
||||||
<video id="diary-video-preview" style="display:none;width:100%;max-height:200px;border-radius:var(--radius-md)" controls playsinline></video>
|
|
||||||
<button type="button" id="diary-preview-clear"
|
|
||||||
style="position:absolute;top:var(--space-2);right:var(--space-2)"
|
|
||||||
class="btn btn-danger btn-sm">${UI.icon('x')}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
`;
|
`;
|
||||||
|
|
@ -587,108 +599,118 @@ window.Page_diary = (() => {
|
||||||
// Fokus auf Titel-Feld → öffnet Keyboard auf Mobile, zeigt dem User was zu tun ist
|
// Fokus auf Titel-Feld → öffnet Keyboard auf Mobile, zeigt dem User was zu tun ist
|
||||||
setTimeout(() => form?.querySelector('[name="titel"]')?.focus(), 150);
|
setTimeout(() => form?.querySelector('[name="titel"]')?.focus(), 150);
|
||||||
|
|
||||||
// Media-Inputs + Vorschau
|
// ---- Multi-Media-Verwaltung ----
|
||||||
const mediaInput = document.getElementById('diary-media-input');
|
const mediaInput = document.getElementById('diary-media-input');
|
||||||
const cameraInput = document.getElementById('diary-camera-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');
|
|
||||||
|
|
||||||
function _showPreview(file) {
|
// Neue Dateien die noch nicht hochgeladen wurden
|
||||||
if (!file) return;
|
const _newFiles = [];
|
||||||
previewWrap.style.display = '';
|
|
||||||
if (file.type.startsWith('video/')) {
|
function _renderNewGrid() {
|
||||||
photoPreview.style.display = 'none';
|
const grid = document.getElementById('diary-new-media-grid');
|
||||||
videoPreview.style.display = '';
|
if (!grid) return;
|
||||||
videoPreview.src = URL.createObjectURL(file);
|
if (_newFiles.length === 0) { grid.style.display = 'none'; grid.innerHTML = ''; return; }
|
||||||
} else {
|
grid.style.display = '';
|
||||||
videoPreview.style.display = 'none';
|
grid.innerHTML = _newFiles.map((f, i) => {
|
||||||
photoPreview.style.display = '';
|
const objUrl = URL.createObjectURL(f);
|
||||||
photoPreview.src = URL.createObjectURL(file);
|
const thumb = f.type.startsWith('video/')
|
||||||
}
|
? `<video src="${objUrl}" class="diary-media-thumb" muted playsinline></video>`
|
||||||
|
: `<img src="${objUrl}" alt="" class="diary-media-thumb">`;
|
||||||
|
return `<div class="diary-media-thumb-wrap" data-new-idx="${i}">
|
||||||
|
${thumb}
|
||||||
|
<button type="button" class="diary-media-thumb-del" data-new-idx="${i}"
|
||||||
|
aria-label="Entfernen">${UI.icon('x')}</button>
|
||||||
|
</div>`;
|
||||||
|
}).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) {
|
// Bestehende Medien im Edit-Modus rendern
|
||||||
// Vorherigen Button entfernen falls vorhanden
|
function _renderExistingMedia() {
|
||||||
document.getElementById('diary-save-album-btn')?.remove();
|
const wrap = document.getElementById('diary-existing-media');
|
||||||
// Nur anbieten wenn Share-API File-Support hat ODER als Download-Fallback
|
if (!wrap) return;
|
||||||
const canShare = navigator.canShare && navigator.canShare({ files: [file] });
|
const items = isEdit ? _allMedia(entry) : [];
|
||||||
const canDownload = true; // <a download> funktioniert immer als Fallback
|
if (items.length === 0) { wrap.innerHTML = ''; return; }
|
||||||
if (!canShare && !canDownload) return;
|
const grid = `<div class="diary-media-grid" style="margin-bottom:var(--space-2)">
|
||||||
const btn = document.createElement('button');
|
${items.map(m => `
|
||||||
btn.type = 'button';
|
<div class="diary-media-thumb-wrap" data-media-id="${m.id || ''}">
|
||||||
btn.id = 'diary-save-album-btn';
|
${m.media_type === 'video'
|
||||||
btn.className = 'btn btn-secondary btn-sm';
|
? `<video src="${m.url}" class="diary-media-thumb" muted playsinline></video>`
|
||||||
btn.style.cssText = 'display:flex;align-items:center;gap:var(--space-1);margin-top:var(--space-2);width:100%';
|
: `<img src="${m.url}" alt="" class="diary-media-thumb">`}
|
||||||
btn.innerHTML = `<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#images"></use></svg>
|
${m.id != null
|
||||||
<span>${canShare ? 'Zum Fotoalbum hinzufügen' : 'Foto herunterladen'}</span>`;
|
? `<button type="button" class="diary-media-thumb-del" data-media-id="${m.id}"
|
||||||
btn.addEventListener('click', () => UI.saveToAlbum(file));
|
aria-label="Entfernen">${UI.icon('x')}</button>`
|
||||||
previewWrap.after(btn);
|
: `<button type="button" class="diary-media-thumb-del" data-legacy="1"
|
||||||
|
aria-label="Entfernen">${UI.icon('x')}</button>`}
|
||||||
|
</div>`).join('')}
|
||||||
|
</div>`;
|
||||||
|
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', () => {
|
function _openPicker(opts = {}) {
|
||||||
_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
|
|
||||||
const tmp = document.createElement('input');
|
const tmp = document.createElement('input');
|
||||||
tmp.type = 'file'; tmp.accept = 'image/*,video/*'; tmp.style.display = 'none';
|
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', () => {
|
tmp.addEventListener('change', () => {
|
||||||
const dt = new DataTransfer();
|
_addFiles(tmp.files);
|
||||||
if (tmp.files[0]) dt.items.add(tmp.files[0]);
|
|
||||||
mediaInput.files = dt.files;
|
|
||||||
_showPreview(tmp.files[0]);
|
|
||||||
tmp.remove();
|
tmp.remove();
|
||||||
});
|
});
|
||||||
document.body.appendChild(tmp);
|
document.body.appendChild(tmp);
|
||||||
tmp.click();
|
tmp.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
cameraInput?.addEventListener('change', () => {
|
||||||
|
if (cameraInput.files.length) {
|
||||||
|
_addFiles(cameraInput.files);
|
||||||
|
cameraInput.value = '';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
document.getElementById('diary-btn-file')?.addEventListener('click', () => {
|
mediaInput?.addEventListener('change', () => {
|
||||||
mediaInput.removeAttribute('accept');
|
if (mediaInput.files.length) {
|
||||||
mediaInput.click();
|
_addFiles(mediaInput.files);
|
||||||
mediaInput.setAttribute('accept', 'image/*,video/*');
|
mediaInput.value = '';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('diary-preview-clear')?.addEventListener('click', () => {
|
document.getElementById('diary-btn-camera') ?.addEventListener('click', () => cameraInput.click());
|
||||||
previewWrap.style.display = 'none';
|
document.getElementById('diary-btn-library')?.addEventListener('click', () => _openPicker({}));
|
||||||
photoPreview.src = ''; videoPreview.src = '';
|
document.getElementById('diary-btn-file') ?.addEventListener('click', () => _openPicker({ noAccept: true }));
|
||||||
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-form-cancel')?.addEventListener('click', UI.modal.close);
|
document.getElementById('diary-form-cancel')?.addEventListener('click', UI.modal.close);
|
||||||
|
|
||||||
|
|
@ -752,7 +774,7 @@ window.Page_diary = (() => {
|
||||||
_locLat = null; _locLon = null; _locName = null;
|
_locLat = null; _locLon = null; _locName = null;
|
||||||
document.getElementById('diary-location-chip-wrap').style.display = 'none';
|
document.getElementById('diary-location-chip-wrap').style.display = 'none';
|
||||||
document.getElementById('diary-location-suggestions').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 (_miniMarker) { _miniMarker.remove(); _miniMarker = null; }
|
||||||
if (_miniMap) { _miniMap.setView([48.0, 11.9], 7); _setMapEditing(false); }
|
if (_miniMap) { _miniMap.setView([48.0, 11.9], 7); _setMapEditing(false); }
|
||||||
});
|
});
|
||||||
|
|
@ -882,33 +904,43 @@ window.Page_diary = (() => {
|
||||||
location_name: _locName,
|
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) {
|
if (isEdit) {
|
||||||
const updated = await API.diary.update(_appState.activeDog.id, entry.id, payload);
|
const updated = await API.diary.update(_appState.activeDog.id, entry.id, payload);
|
||||||
if (mediaFile) {
|
if (_newFiles.length > 0) {
|
||||||
try {
|
const uploaded = await _uploadNewFiles(entry.id);
|
||||||
const fd2 = new FormData();
|
if (!updated.media_items) updated.media_items = [];
|
||||||
fd2.append('file', mediaFile);
|
updated.media_items.push(...uploaded);
|
||||||
const media = await API.diary.uploadMedia(_appState.activeDog.id, entry.id, fd2);
|
} else {
|
||||||
updated.media_url = media.media_url;
|
// media_items aus dem aktuellen entry-State übernehmen (evtl. gelöscht via X-Button)
|
||||||
} catch {
|
updated.media_items = entry.media_items || updated.media_items || [];
|
||||||
UI.toast.warning('Gespeichert, Medium konnte nicht hochgeladen werden.');
|
updated.media_url = entry.media_url ?? updated.media_url;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_updateEntryInList(updated);
|
_updateEntryInList(updated);
|
||||||
UI.toast.success('Eintrag gespeichert.');
|
UI.toast.success('Eintrag gespeichert.');
|
||||||
} else {
|
} else {
|
||||||
const created = await API.diary.create(_appState.activeDog.id, payload);
|
const created = await API.diary.create(_appState.activeDog.id, payload);
|
||||||
if (mediaFile) {
|
if (_newFiles.length > 0) {
|
||||||
try {
|
const uploaded = await _uploadNewFiles(created.id);
|
||||||
const fd2 = new FormData();
|
created.media_items = uploaded;
|
||||||
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.');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_entries.unshift(created);
|
_entries.unshift(created);
|
||||||
UI.toast.success('Eintrag erstellt.');
|
UI.toast.success('Eintrag erstellt.');
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Offline-Cache + Push Notifications + Tile-Cache
|
Offline-Cache + Push Notifications + Tile-Cache
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const CACHE_VERSION = 'by-v210';
|
const CACHE_VERSION = 'by-v211';
|
||||||
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