Feature: Foto-Editor im Hundeprofil — Zoom, Drag-to-pan, Löschen
This commit is contained in:
parent
cb8ac8cffd
commit
913cebcba1
6 changed files with 234 additions and 35 deletions
|
|
@ -93,6 +93,10 @@ const API = (() => {
|
|||
update(id, data) { return patch(`/dogs/${id}`, data); },
|
||||
delete(id) { return del(`/dogs/${id}`); },
|
||||
uploadPhoto(id, formData) { return upload(`/dogs/${id}/photo`, formData); },
|
||||
updatePhotoPosition(id, zoom, offsetX, offsetY) {
|
||||
return patch(`/dogs/${id}/photo-position`, { zoom, offset_x: offsetX, offset_y: offsetY });
|
||||
},
|
||||
deletePhoto(id) { return del(`/dogs/${id}/photo`); },
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -79,22 +79,14 @@ window.Page_dog_profile = (() => {
|
|||
<!-- Profilfoto mit Upload-Button -->
|
||||
<div style="position:relative;display:inline-block;margin-bottom:var(--space-4)">
|
||||
${dog.foto_url
|
||||
? `<img src="${dog.foto_url}" alt="${_esc(dog.name)}"
|
||||
style="width:120px;height:120px;border-radius:50%;object-fit:cover;
|
||||
border:3px solid var(--c-primary)">`
|
||||
: `<div style="width:120px;height:120px;border-radius:50%;
|
||||
background:var(--c-surface-2);display:flex;
|
||||
align-items:center;justify-content:center;
|
||||
font-size:3.5rem;border:3px solid var(--c-border)">${UI.icon('dog')}</div>`}
|
||||
<label style="position:absolute;bottom:4px;right:4px;
|
||||
background:var(--c-primary);color:#fff;border-radius:50%;
|
||||
width:30px;height:30px;display:flex;align-items:center;
|
||||
justify-content:center;cursor:pointer;font-size:14px"
|
||||
title="Foto ändern">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#camera"></use></svg>
|
||||
<input type="file" id="dp-photo-input" accept="image/*"
|
||||
style="display:none">
|
||||
</label>
|
||||
? `<div class="dp-avatar-ring">
|
||||
<img src="${dog.foto_url}" alt="${_esc(dog.name)}" class="dp-avatar-img"
|
||||
style="transform:scale(${dog.foto_zoom||1}) translate(${dog.foto_offset_x||0}px,${dog.foto_offset_y||0}px)">
|
||||
</div>`
|
||||
: `<div class="dp-avatar-ring dp-avatar-empty">${UI.icon('dog')}</div>`}
|
||||
<button class="dp-avatar-edit-btn" id="dp-photo-edit-btn" title="Foto bearbeiten">
|
||||
${UI.icon('camera')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Name + Rasse -->
|
||||
|
|
@ -206,24 +198,9 @@ window.Page_dog_profile = (() => {
|
|||
</div>
|
||||
`;
|
||||
|
||||
// Foto hochladen
|
||||
document.getElementById('dp-photo-input')?.addEventListener('change', async e => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
try {
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
const result = await API.dogs.uploadPhoto(dog.id, fd);
|
||||
dog.foto_url = result.foto_url;
|
||||
_appState.activeDog = { ..._appState.activeDog, foto_url: result.foto_url };
|
||||
_appState.dogs = _appState.dogs.map(d =>
|
||||
d.id === dog.id ? _appState.activeDog : d
|
||||
);
|
||||
UI.toast.success('Foto gespeichert.');
|
||||
_renderProfile(_appState.activeDog);
|
||||
} catch (err) {
|
||||
UI.toast.error(err.message || 'Fehler beim Hochladen.');
|
||||
}
|
||||
// Foto-Editor öffnen
|
||||
document.getElementById('dp-photo-edit-btn')?.addEventListener('click', () => {
|
||||
_showPhotoEditor(dog);
|
||||
});
|
||||
// NFC-Link kopieren
|
||||
document.getElementById('dp-copy-link-btn')?.addEventListener('click', async () => {
|
||||
|
|
@ -293,6 +270,131 @@ window.Page_dog_profile = (() => {
|
|||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// FOTO-EDITOR
|
||||
// ----------------------------------------------------------
|
||||
function _showPhotoEditor(dog) {
|
||||
const hasPhoto = !!dog.foto_url;
|
||||
const zoom = dog.foto_zoom || 1.0;
|
||||
const ox = dog.foto_offset_x || 0.0;
|
||||
const oy = dog.foto_offset_y || 0.0;
|
||||
|
||||
const body = `
|
||||
<div class="photo-editor">
|
||||
<div class="photo-editor-preview" id="pe-preview">
|
||||
${hasPhoto
|
||||
? `<img src="${UI.escape(dog.foto_url)}" id="pe-img" draggable="false"
|
||||
style="transform:scale(${zoom}) translate(${ox}px,${oy}px)">`
|
||||
: `<div class="photo-editor-empty">${UI.icon('dog')}</div>`}
|
||||
</div>
|
||||
${hasPhoto ? `
|
||||
<div class="photo-editor-controls">
|
||||
<label class="form-label">Zoom</label>
|
||||
<input type="range" id="pe-zoom" min="1" max="3" step="0.05" value="${zoom}"
|
||||
style="width:100%">
|
||||
</div>
|
||||
` : ''}
|
||||
<label class="btn btn-secondary" style="cursor:pointer">
|
||||
${UI.icon('upload-simple')} Neues Foto wählen
|
||||
<input type="file" id="pe-file-input" accept="image/*" style="display:none">
|
||||
</label>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const footer = `
|
||||
${hasPhoto ? `<button class="btn btn-danger" id="pe-delete-btn">${UI.icon('trash')} Löschen</button>` : ''}
|
||||
<button class="btn btn-ghost" onclick="UI.modal.close()">Abbrechen</button>
|
||||
${hasPhoto ? `<button class="btn btn-primary" id="pe-save-btn">Speichern</button>` : ''}
|
||||
`;
|
||||
|
||||
UI.modal.open({ title: 'Foto bearbeiten', body, footer });
|
||||
|
||||
// State für Drag
|
||||
let _zoom = zoom, _ox = ox, _oy = oy;
|
||||
const img = document.getElementById('pe-img');
|
||||
const zoomSlider = document.getElementById('pe-zoom');
|
||||
|
||||
function _applyTransform() {
|
||||
if (img) img.style.transform = `scale(${_zoom}) translate(${_ox}px,${_oy}px)`;
|
||||
}
|
||||
|
||||
// Zoom-Slider
|
||||
zoomSlider?.addEventListener('input', e => {
|
||||
_zoom = parseFloat(e.target.value);
|
||||
_applyTransform();
|
||||
});
|
||||
|
||||
// Drag-to-pan
|
||||
if (img) {
|
||||
let _dragging = false, _startX = 0, _startY = 0, _baseX = _ox, _baseY = _oy;
|
||||
|
||||
img.addEventListener('pointerdown', e => {
|
||||
_dragging = true; _startX = e.clientX; _startY = e.clientY;
|
||||
_baseX = _ox; _baseY = _oy;
|
||||
img.setPointerCapture(e.pointerId);
|
||||
e.preventDefault();
|
||||
});
|
||||
img.addEventListener('pointermove', e => {
|
||||
if (!_dragging) return;
|
||||
_ox = _baseX + (e.clientX - _startX) / _zoom;
|
||||
_oy = _baseY + (e.clientY - _startY) / _zoom;
|
||||
_applyTransform();
|
||||
});
|
||||
img.addEventListener('pointerup', () => { _dragging = false; });
|
||||
}
|
||||
|
||||
// Speichern
|
||||
document.getElementById('pe-save-btn')?.addEventListener('click', async () => {
|
||||
try {
|
||||
await API.dogs.updatePhotoPosition(dog.id, _zoom, _ox, _oy);
|
||||
_appState.activeDog = { ..._appState.activeDog, foto_zoom: _zoom, foto_offset_x: _ox, foto_offset_y: _oy };
|
||||
_appState.dogs = _appState.dogs.map(d => d.id === dog.id ? _appState.activeDog : d);
|
||||
UI.modal.close();
|
||||
UI.toast.success('Position gespeichert.');
|
||||
_renderProfile(_appState.activeDog);
|
||||
} catch (err) {
|
||||
UI.toast.error(err.message || 'Fehler beim Speichern.');
|
||||
}
|
||||
});
|
||||
|
||||
// Löschen
|
||||
document.getElementById('pe-delete-btn')?.addEventListener('click', async () => {
|
||||
if (!confirm('Foto wirklich löschen?')) return;
|
||||
try {
|
||||
await API.dogs.deletePhoto(dog.id);
|
||||
_appState.activeDog = { ..._appState.activeDog, foto_url: null, foto_zoom: 1, foto_offset_x: 0, foto_offset_y: 0 };
|
||||
_appState.dogs = _appState.dogs.map(d => d.id === dog.id ? _appState.activeDog : d);
|
||||
UI.modal.close();
|
||||
UI.toast.success('Foto gelöscht.');
|
||||
_renderProfile(_appState.activeDog);
|
||||
} catch (err) {
|
||||
UI.toast.error(err.message || 'Fehler beim Löschen.');
|
||||
}
|
||||
});
|
||||
|
||||
// Neues Foto hochladen
|
||||
document.getElementById('pe-file-input')?.addEventListener('change', async e => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
try {
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
const result = await API.dogs.uploadPhoto(dog.id, fd);
|
||||
// Position zurücksetzen
|
||||
await API.dogs.updatePhotoPosition(dog.id, 1.0, 0.0, 0.0);
|
||||
_appState.activeDog = { ..._appState.activeDog, foto_url: result.foto_url, foto_zoom: 1, foto_offset_x: 0, foto_offset_y: 0 };
|
||||
_appState.dogs = _appState.dogs.map(d => d.id === dog.id ? _appState.activeDog : d);
|
||||
UI.modal.close();
|
||||
UI.toast.success('Foto hochgeladen.');
|
||||
_renderProfile(_appState.activeDog);
|
||||
// Editor neu öffnen damit User positionieren kann
|
||||
_showPhotoEditor(_appState.activeDog);
|
||||
} catch (err) {
|
||||
UI.toast.error(err.message || 'Fehler beim Hochladen.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// AUSWEIS
|
||||
// ----------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue