feat: add PFP album/gallery system with batch upload, cropping, and disk management
- Backend: album storage in memory/profile_pictures/album/{uuid}/ with
original.png, cropped.png, and metadata.json per entry
- add_to_album/add_batch_to_album with efficient resource management
(vision model + face detector kept alive across batch)
- set_album_entry_as_current auto-archives current PFP before replacing
- manual/auto crop album entries without applying to Discord
- Disk usage tracking, single & bulk delete
- API: full CRUD endpoints under /profile-picture/album/*
- Frontend: collapsible album grid in tab11 with thumbnail cards,
multi-select checkboxes for bulk delete, detail panel with crop
interface (Cropper.js), description editor, set-as-current action
This commit is contained in:
@@ -738,6 +738,107 @@
|
||||
border-color: #61dafb;
|
||||
outline: none;
|
||||
}
|
||||
/* Album / Gallery grid */
|
||||
.album-section {
|
||||
margin: 1.5rem 0;
|
||||
padding: 1rem;
|
||||
background: #1a1a2e;
|
||||
border: 1px solid #444;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.album-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.album-header h4 { margin: 0; }
|
||||
.album-toolbar {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin: 0.75rem 0;
|
||||
}
|
||||
.album-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 0.75rem;
|
||||
max-height: 480px;
|
||||
overflow-y: auto;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
.album-card {
|
||||
position: relative;
|
||||
border: 2px solid #444;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
background: #111;
|
||||
}
|
||||
.album-card:hover { border-color: #61dafb; }
|
||||
.album-card.selected { border-color: #4CAF50; box-shadow: 0 0 8px rgba(76,175,80,0.4); }
|
||||
.album-card.checked { border-color: #ff9800; }
|
||||
.album-card img {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
.album-card .album-check {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
z-index: 2;
|
||||
accent-color: #ff9800;
|
||||
}
|
||||
.album-card .album-card-info {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(0,0,0,0.7);
|
||||
padding: 2px 4px;
|
||||
font-size: 0.7rem;
|
||||
color: #ccc;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.album-card .color-dot {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #888;
|
||||
vertical-align: middle;
|
||||
margin-right: 3px;
|
||||
}
|
||||
.album-detail {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
background: #222;
|
||||
border: 1px solid #555;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.album-detail-previews {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.album-detail-previews .pfp-preview-box img {
|
||||
max-width: 300px;
|
||||
max-height: 300px;
|
||||
}
|
||||
.album-disk-usage {
|
||||
font-size: 0.8rem;
|
||||
color: #888;
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -1795,6 +1896,70 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Album / Gallery -->
|
||||
<div class="album-section">
|
||||
<div class="album-header" onclick="albumToggle()">
|
||||
<h4><span id="album-toggle-icon">▶</span> 📚 Profile Picture Album <span id="album-count" style="color: #888; font-weight: normal;"></span></h4>
|
||||
<span class="album-disk-usage" id="album-disk-usage"></span>
|
||||
</div>
|
||||
<div id="album-body" style="display: none;">
|
||||
<!-- Toolbar -->
|
||||
<div class="album-toolbar">
|
||||
<input type="file" id="album-upload" accept="image/*" multiple style="max-width: 220px;">
|
||||
<button onclick="albumUpload()">📤 Add to Album</button>
|
||||
<button onclick="albumAddCurrent()">📌 Archive Current PFP</button>
|
||||
<button onclick="albumBulkDelete()" style="background: #c0392b;" id="album-bulk-delete-btn" disabled>🗑️ Delete Selected (<span id="album-selected-count">0</span>)</button>
|
||||
<div id="album-status" style="font-size: 0.85rem; color: #61dafb; margin-left: 0.5rem;"></div>
|
||||
</div>
|
||||
<!-- Grid -->
|
||||
<div class="album-grid" id="album-grid"></div>
|
||||
<!-- Detail panel (shown when an entry is clicked) -->
|
||||
<div class="album-detail" id="album-detail" style="display: none;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<h4 style="margin: 0;">Selected Entry</h4>
|
||||
<button onclick="albumCloseDetail()" style="background: #555; padding: 0.3rem 0.6rem;">✖ Close</button>
|
||||
</div>
|
||||
<div class="album-detail-previews">
|
||||
<div class="pfp-preview-box">
|
||||
<span class="label">📷 Original</span>
|
||||
<img id="album-detail-original" src="" alt="Original" style="cursor: pointer;" onclick="albumShowCropInterface()" title="Click to crop">
|
||||
<div id="album-detail-dims" style="font-size: 0.8rem; color: #666; margin-top: 0.3rem;"></div>
|
||||
</div>
|
||||
<div class="pfp-preview-box">
|
||||
<span class="label">🎯 Cropped</span>
|
||||
<img id="album-detail-cropped" src="" alt="Cropped" style="border-radius: 50%; max-width: 256px; max-height: 256px;">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Album entry crop interface -->
|
||||
<div id="album-crop-section" style="display: none; margin: 1rem 0; padding: 1rem; background: #1a1a2e; border: 1px solid #444; border-radius: 8px;">
|
||||
<h4 style="margin-top: 0;">✂️ Crop Album Entry</h4>
|
||||
<div class="pfp-crop-container">
|
||||
<img id="album-crop-image" src="" alt="Crop source">
|
||||
</div>
|
||||
<div style="margin-top: 0.75rem; display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
||||
<button onclick="albumApplyManualCrop()" style="background: #4CAF50; color: #fff; font-weight: bold;">✂️ Apply Crop</button>
|
||||
<button onclick="albumApplyAutoCrop()">🤖 Auto Crop</button>
|
||||
<button onclick="albumHideCropInterface()" style="background: #666;">✖ Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Description -->
|
||||
<div style="margin-top: 1rem;">
|
||||
<label style="color: #aaa;">📝 Description:</label>
|
||||
<textarea id="album-detail-description" class="pfp-description-editor" style="min-height: 80px;"></textarea>
|
||||
<div style="margin-top: 0.5rem; display: flex; gap: 0.5rem;">
|
||||
<button onclick="albumSaveDescription()" style="background: #4CAF50; color: #fff;">💾 Save</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Actions -->
|
||||
<div style="margin-top: 1rem; display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
||||
<button onclick="albumSetAsCurrent()" style="background: #2196F3; color: #fff; font-weight: bold;">🖼️ Set as Current PFP</button>
|
||||
<button onclick="albumDeleteSelected()" style="background: #c0392b; color: #fff;">🗑️ Delete Entry</button>
|
||||
</div>
|
||||
<div id="album-detail-meta" style="margin-top: 0.75rem; font-size: 0.8rem; color: #888;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Crop Mode Toggle -->
|
||||
<div class="crop-mode-toggle">
|
||||
<span style="color: #61dafb; font-weight: bold;">Crop Mode:</span>
|
||||
@@ -3518,6 +3683,25 @@ async function loadPfpTab() {
|
||||
|
||||
// Refresh preview images
|
||||
pfpRefreshPreviews();
|
||||
|
||||
// Update album header counts (without opening)
|
||||
try {
|
||||
const [listRes, usageRes] = await Promise.all([
|
||||
apiCall('/profile-picture/album'),
|
||||
apiCall('/profile-picture/album/disk-usage')
|
||||
]);
|
||||
if (listRes.status === 'ok') {
|
||||
albumEntries = listRes.entries || [];
|
||||
document.getElementById('album-count').textContent = `(${albumEntries.length} entries)`;
|
||||
if (albumOpen) albumRenderGrid();
|
||||
}
|
||||
if (usageRes.status === 'ok') {
|
||||
document.getElementById('album-disk-usage').textContent =
|
||||
`${usageRes.human_readable} · ${usageRes.entry_count} entries`;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load album info:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Danbooru Change ---
|
||||
@@ -3878,6 +4062,353 @@ async function resetRoleColor() {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Album / Gallery System
|
||||
// ============================================================================
|
||||
|
||||
let albumEntries = [];
|
||||
let albumSelectedId = null; // currently-viewed entry
|
||||
let albumChecked = new Set(); // checked for bulk delete
|
||||
let albumCropper = null;
|
||||
let albumOpen = false;
|
||||
|
||||
function albumSetStatus(text, color = '#61dafb') {
|
||||
const el = document.getElementById('album-status');
|
||||
if (el) { el.textContent = text; el.style.color = color; }
|
||||
}
|
||||
|
||||
function albumToggle() {
|
||||
albumOpen = !albumOpen;
|
||||
document.getElementById('album-body').style.display = albumOpen ? 'block' : 'none';
|
||||
document.getElementById('album-toggle-icon').textContent = albumOpen ? '▼' : '▶';
|
||||
if (albumOpen) albumLoad();
|
||||
}
|
||||
|
||||
async function albumLoad() {
|
||||
try {
|
||||
const [listRes, usageRes] = await Promise.all([
|
||||
apiCall('/profile-picture/album'),
|
||||
apiCall('/profile-picture/album/disk-usage')
|
||||
]);
|
||||
if (listRes.status === 'ok') {
|
||||
albumEntries = listRes.entries || [];
|
||||
document.getElementById('album-count').textContent = `(${albumEntries.length} entries)`;
|
||||
albumRenderGrid();
|
||||
}
|
||||
if (usageRes.status === 'ok') {
|
||||
document.getElementById('album-disk-usage').textContent =
|
||||
`${usageRes.human_readable} · ${usageRes.entry_count} entries`;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Album load error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function albumRenderGrid() {
|
||||
const grid = document.getElementById('album-grid');
|
||||
if (!grid) return;
|
||||
|
||||
if (albumEntries.length === 0) {
|
||||
grid.innerHTML = '<div style="color: #888; padding: 1rem; grid-column: 1/-1;">No album entries yet. Upload images or archive the current PFP.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = albumEntries.map(e => {
|
||||
const id = e.id;
|
||||
const isSelected = id === albumSelectedId;
|
||||
const isChecked = albumChecked.has(id);
|
||||
const colorDot = e.dominant_color
|
||||
? `<span class="color-dot" style="background:${e.dominant_color.hex}"></span>`
|
||||
: '';
|
||||
const label = (e.source || '').replace('custom_upload', 'upload').substring(0, 12);
|
||||
return `<div class="album-card${isSelected ? ' selected' : ''}${isChecked ? ' checked' : ''}"
|
||||
data-id="${id}" onclick="albumSelectEntry('${id}')">
|
||||
<input type="checkbox" class="album-check" ${isChecked ? 'checked' : ''}
|
||||
onclick="event.stopPropagation(); albumToggleCheck('${id}', this.checked)">
|
||||
<img src="/profile-picture/album/${id}/image/cropped?t=${Date.now()}"
|
||||
onerror="this.src='/profile-picture/album/${id}/image/original?t=${Date.now()}'"
|
||||
loading="lazy" alt="">
|
||||
<div class="album-card-info">${colorDot}${label}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function albumToggleCheck(id, checked) {
|
||||
if (checked) albumChecked.add(id); else albumChecked.delete(id);
|
||||
document.getElementById('album-selected-count').textContent = albumChecked.size;
|
||||
document.getElementById('album-bulk-delete-btn').disabled = albumChecked.size === 0;
|
||||
// update card class
|
||||
const card = document.querySelector(`.album-card[data-id="${id}"]`);
|
||||
if (card) card.classList.toggle('checked', checked);
|
||||
}
|
||||
|
||||
async function albumSelectEntry(id) {
|
||||
albumSelectedId = id;
|
||||
// highlight card
|
||||
document.querySelectorAll('.album-card').forEach(c => c.classList.toggle('selected', c.dataset.id === id));
|
||||
// show detail
|
||||
const detail = document.getElementById('album-detail');
|
||||
detail.style.display = 'block';
|
||||
const t = Date.now();
|
||||
document.getElementById('album-detail-original').src = `/profile-picture/album/${id}/image/original?t=${t}`;
|
||||
document.getElementById('album-detail-cropped').src = `/profile-picture/album/${id}/image/cropped?t=${t}`;
|
||||
|
||||
// load entry metadata
|
||||
try {
|
||||
const res = await apiCall(`/profile-picture/album/${id}`);
|
||||
if (res.status === 'ok' && res.entry) {
|
||||
const e = res.entry;
|
||||
document.getElementById('album-detail-dims').textContent =
|
||||
e.original_width && e.original_height ? `${e.original_width}×${e.original_height}` : '';
|
||||
document.getElementById('album-detail-description').value = e.description || '';
|
||||
const metaLines = [];
|
||||
if (e.added_at) metaLines.push(`Added: ${new Date(e.added_at).toLocaleString()}`);
|
||||
if (e.source) metaLines.push(`Source: ${e.source}`);
|
||||
if (e.dominant_color) metaLines.push(`Color: ${e.dominant_color.hex}`);
|
||||
if (e.was_current) metaLines.push('📌 Previously active');
|
||||
document.getElementById('album-detail-meta').textContent = metaLines.join(' · ');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Album entry load error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function albumCloseDetail() {
|
||||
document.getElementById('album-detail').style.display = 'none';
|
||||
albumSelectedId = null;
|
||||
albumHideCropInterface();
|
||||
document.querySelectorAll('.album-card').forEach(c => c.classList.remove('selected'));
|
||||
}
|
||||
|
||||
// --- Album Upload ---
|
||||
async function albumUpload() {
|
||||
const input = document.getElementById('album-upload');
|
||||
if (!input.files || input.files.length === 0) {
|
||||
showNotification('Select image(s) to add to album', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const files = Array.from(input.files);
|
||||
albumSetStatus(`⏳ Adding ${files.length} image(s) to album...`);
|
||||
|
||||
try {
|
||||
if (files.length === 1) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', files[0]);
|
||||
const resp = await fetch('/profile-picture/album/add', { method: 'POST', body: formData });
|
||||
const result = await resp.json();
|
||||
if (result.status === 'ok') {
|
||||
albumSetStatus(`✅ Added to album`, 'green');
|
||||
showNotification('Image added to album!');
|
||||
} else {
|
||||
throw new Error(result.message);
|
||||
}
|
||||
} else {
|
||||
const formData = new FormData();
|
||||
files.forEach(f => formData.append('files', f));
|
||||
const resp = await fetch('/profile-picture/album/add-batch', { method: 'POST', body: formData });
|
||||
const result = await resp.json();
|
||||
albumSetStatus(`✅ ${result.message}`, result.failed > 0 ? 'orange' : 'green');
|
||||
showNotification(result.message);
|
||||
}
|
||||
input.value = '';
|
||||
await albumLoad();
|
||||
} catch (error) {
|
||||
console.error('Album upload error:', error);
|
||||
albumSetStatus(`❌ Error: ${error.message}`, 'red');
|
||||
}
|
||||
}
|
||||
|
||||
async function albumAddCurrent() {
|
||||
albumSetStatus('⏳ Archiving current PFP...');
|
||||
try {
|
||||
const result = await apiCall('/profile-picture/album/add-current', 'POST');
|
||||
if (result.status === 'ok') {
|
||||
albumSetStatus(`✅ ${result.message}`, 'green');
|
||||
showNotification('Current PFP archived to album!');
|
||||
await albumLoad();
|
||||
} else {
|
||||
throw new Error(result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
albumSetStatus(`❌ Error: ${error.message}`, 'red');
|
||||
}
|
||||
}
|
||||
|
||||
// --- Album Set as Current ---
|
||||
async function albumSetAsCurrent() {
|
||||
if (!albumSelectedId) return;
|
||||
if (!confirm('Set this album entry as your current Discord profile picture?\nThe current PFP will be archived to the album.')) return;
|
||||
|
||||
albumSetStatus('⏳ Setting as current PFP...');
|
||||
try {
|
||||
const result = await apiCall(`/profile-picture/album/${albumSelectedId}/set-current`, 'POST');
|
||||
if (result.status === 'ok') {
|
||||
albumSetStatus(`✅ ${result.message}`, 'green');
|
||||
showNotification('Album entry set as current PFP!');
|
||||
pfpRefreshPreviews();
|
||||
loadPfpTab(); // refresh metadata + description
|
||||
await albumLoad();
|
||||
} else {
|
||||
throw new Error(result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
albumSetStatus(`❌ Error: ${error.message}`, 'red');
|
||||
}
|
||||
}
|
||||
|
||||
// --- Album Delete ---
|
||||
async function albumDeleteSelected() {
|
||||
if (!albumSelectedId) return;
|
||||
if (!confirm('Delete this album entry permanently?')) return;
|
||||
|
||||
try {
|
||||
const resp = await fetch(`/profile-picture/album/${albumSelectedId}`, { method: 'DELETE' });
|
||||
const result = await resp.json();
|
||||
if (result.status === 'ok') {
|
||||
showNotification('Album entry deleted');
|
||||
albumCloseDetail();
|
||||
await albumLoad();
|
||||
} else {
|
||||
throw new Error(result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification(`Error: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function albumBulkDelete() {
|
||||
if (albumChecked.size === 0) return;
|
||||
if (!confirm(`Delete ${albumChecked.size} selected album entries permanently?`)) return;
|
||||
|
||||
albumSetStatus(`⏳ Deleting ${albumChecked.size} entries...`);
|
||||
try {
|
||||
const resp = await fetch('/profile-picture/album/delete-bulk', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ entry_ids: Array.from(albumChecked) })
|
||||
});
|
||||
const result = await resp.json();
|
||||
albumSetStatus(`✅ ${result.message}`, 'green');
|
||||
showNotification(result.message);
|
||||
albumChecked.clear();
|
||||
document.getElementById('album-selected-count').textContent = '0';
|
||||
document.getElementById('album-bulk-delete-btn').disabled = true;
|
||||
if (albumSelectedId && !albumEntries.find(e => e.id === albumSelectedId)) {
|
||||
albumCloseDetail();
|
||||
}
|
||||
await albumLoad();
|
||||
} catch (error) {
|
||||
albumSetStatus(`❌ Error: ${error.message}`, 'red');
|
||||
}
|
||||
}
|
||||
|
||||
// --- Album Crop ---
|
||||
function albumShowCropInterface() {
|
||||
if (!albumSelectedId) return;
|
||||
if (albumCropper) { albumCropper.destroy(); albumCropper = null; }
|
||||
|
||||
const section = document.getElementById('album-crop-section');
|
||||
const img = document.getElementById('album-crop-image');
|
||||
img.src = `/profile-picture/album/${albumSelectedId}/image/original?t=${Date.now()}`;
|
||||
section.style.display = 'block';
|
||||
|
||||
img.onload = function() {
|
||||
albumCropper = new Cropper(img, {
|
||||
aspectRatio: 1,
|
||||
viewMode: 2,
|
||||
minCropBoxWidth: 64,
|
||||
minCropBoxHeight: 64,
|
||||
responsive: true,
|
||||
autoCropArea: 0.8,
|
||||
background: true,
|
||||
guides: true,
|
||||
center: true,
|
||||
highlight: true,
|
||||
cropBoxMovable: true,
|
||||
cropBoxResizable: true,
|
||||
toggleDragModeOnDblclick: false
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function albumHideCropInterface() {
|
||||
if (albumCropper) { albumCropper.destroy(); albumCropper = null; }
|
||||
document.getElementById('album-crop-section').style.display = 'none';
|
||||
}
|
||||
|
||||
async function albumApplyManualCrop() {
|
||||
if (!albumCropper || !albumSelectedId) return;
|
||||
const data = albumCropper.getData(true);
|
||||
albumSetStatus('⏳ Applying crop to album entry...');
|
||||
|
||||
try {
|
||||
const resp = await fetch(`/profile-picture/album/${albumSelectedId}/manual-crop`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ x: Math.round(data.x), y: Math.round(data.y), width: Math.round(data.width), height: Math.round(data.height) })
|
||||
});
|
||||
const result = await resp.json();
|
||||
if (result.status === 'ok') {
|
||||
albumSetStatus('✅ Crop applied', 'green');
|
||||
showNotification('Album entry cropped!');
|
||||
albumHideCropInterface();
|
||||
// refresh detail images
|
||||
const t = Date.now();
|
||||
document.getElementById('album-detail-cropped').src = `/profile-picture/album/${albumSelectedId}/image/cropped?t=${t}`;
|
||||
await albumLoad(); // refresh grid thumbnails
|
||||
} else {
|
||||
throw new Error(result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
albumSetStatus(`❌ Error: ${error.message}`, 'red');
|
||||
}
|
||||
}
|
||||
|
||||
async function albumApplyAutoCrop() {
|
||||
if (!albumSelectedId) return;
|
||||
albumSetStatus('⏳ Running auto-crop on album entry...');
|
||||
|
||||
try {
|
||||
const result = await apiCall(`/profile-picture/album/${albumSelectedId}/auto-crop`, 'POST');
|
||||
if (result.status === 'ok') {
|
||||
albumSetStatus('✅ Auto-crop applied', 'green');
|
||||
showNotification('Album entry auto-cropped!');
|
||||
albumHideCropInterface();
|
||||
const t = Date.now();
|
||||
document.getElementById('album-detail-cropped').src = `/profile-picture/album/${albumSelectedId}/image/cropped?t=${t}`;
|
||||
await albumLoad();
|
||||
} else {
|
||||
throw new Error(result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
albumSetStatus(`❌ Error: ${error.message}`, 'red');
|
||||
}
|
||||
}
|
||||
|
||||
// --- Album Description ---
|
||||
async function albumSaveDescription() {
|
||||
if (!albumSelectedId) return;
|
||||
const description = document.getElementById('album-detail-description').value.trim();
|
||||
if (!description) { showNotification('Description cannot be empty', 'error'); return; }
|
||||
|
||||
try {
|
||||
const resp = await fetch(`/profile-picture/album/${albumSelectedId}/description`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ description })
|
||||
});
|
||||
const result = await resp.json();
|
||||
if (result.status === 'ok') {
|
||||
showNotification('Album entry description saved!');
|
||||
} else {
|
||||
throw new Error(result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification(`Error: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle functions for custom prompt and manual message target selection
|
||||
function toggleCustomPromptTarget() {
|
||||
const targetType = document.getElementById('custom-prompt-target-type').value;
|
||||
|
||||
Reference in New Issue
Block a user