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
|
|
@ -127,6 +127,9 @@ const API = (() => {
|
|||
deleteMediaItem(dogId, entryId, 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) {
|
||||
return get(`/dogs/${dogId}/diary/nearby?lat=${lat}&lon=${lon}`);
|
||||
},
|
||||
|
|
@ -149,6 +152,12 @@ const API = (() => {
|
|||
deleteDocument(dogId, id) {
|
||||
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) {
|
||||
return post(`/dogs/${dogId}/health/ki-zusammenfassung`);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
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 = (() => {
|
||||
|
||||
|
|
@ -420,7 +420,8 @@ const App = (() => {
|
|||
}
|
||||
|
||||
_updateNotifBadge();
|
||||
setInterval(_updateNotifBadge, 60_000);
|
||||
_updateChatBadge();
|
||||
setInterval(() => { _updateNotifBadge(); _updateChatBadge(); }, 30_000);
|
||||
|
||||
const pendingInvite = sessionStorage.getItem('pending_invite');
|
||||
if (pendingInvite) {
|
||||
|
|
@ -440,6 +441,18 @@ const App = (() => {
|
|||
} 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() {
|
||||
state.user = null;
|
||||
state.dogs = [];
|
||||
|
|
|
|||
|
|
@ -304,14 +304,14 @@ window.Page_diary = (() => {
|
|||
const dateStr = e.datum ? UI.time.format(e.datum + 'T00:00:00') : '';
|
||||
const tags = (e.tags || []).slice(0, 4);
|
||||
|
||||
const allMedia = _allMedia(e);
|
||||
const firstMedia = allMedia[0] || null;
|
||||
const allMedia = _allMedia(e);
|
||||
const coverMedia = allMedia.find(m => m.is_cover) || allMedia[0] || null;
|
||||
const mediaCount = allMedia.length;
|
||||
const photo = firstMedia
|
||||
const photo = coverMedia
|
||||
? `<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>`
|
||||
: `<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>` : ''}
|
||||
</div>`
|
||||
: '';
|
||||
|
|
@ -381,12 +381,21 @@ window.Page_diary = (() => {
|
|||
const allMedia = _allMedia(entry);
|
||||
const photo = allMedia.length > 0
|
||||
? (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)">
|
||||
${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('')}
|
||||
${allMedia.map(m => `
|
||||
<div class="diary-gallery-wrap" style="position:relative">
|
||||
${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">`}
|
||||
<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>`)
|
||||
: '';
|
||||
|
||||
|
|
@ -434,6 +443,33 @@ window.Page_diary = (() => {
|
|||
|
||||
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 () => {
|
||||
UI.modal.close();
|
||||
// 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">`}
|
||||
${m.id != null
|
||||
? `<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"
|
||||
aria-label="Entfernen">${UI.icon('x')}</button>`}
|
||||
</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();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue