`;
}).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');
}
}
// ============================================================================
// MOOD ACTIVITIES EDITOR
// ============================================================================
// activitiesData, activitiesOpen, activitiesSections, activitiesEditing, activitiesEditCache declared in core.js
function activitiesToggle() {
activitiesOpen = !activitiesOpen;
document.getElementById('activities-body').style.display = activitiesOpen ? 'block' : 'none';
document.getElementById('activities-toggle-icon').textContent = activitiesOpen ? '▼' : '▶';
if (activitiesOpen) {
if (!activitiesData) activitiesLoad();
activityRefreshCurrent();
}
}
function activitiesSectionToggle(section) {
activitiesSections[section] = !activitiesSections[section];
document.getElementById(`activities-${section}-body`).style.display = activitiesSections[section] ? 'block' : 'none';
document.getElementById(`activities-${section}-icon`).textContent = activitiesSections[section] ? '▼' : '▶';
}
async function activitiesLoad() {
const statusEl = document.getElementById('activities-status');
statusEl.textContent = 'Loading...';
try {
activitiesData = await apiCall('/activities');
const normalMoods = Object.keys(activitiesData.normal || {});
const evilMoods = Object.keys(activitiesData.evil || {});
const total = normalMoods.length + evilMoods.length;
document.getElementById('activities-summary').textContent = `(${total} moods configured)`;
activitiesRenderSection('normal');
activitiesRenderSection('evil');
statusEl.textContent = '';
} catch (e) {
statusEl.textContent = 'Failed to load: ' + e.message;
statusEl.style.color = '#e74c3c';
}
}
function activitiesRenderSection(section) {
const container = document.getElementById(`activities-${section}-list`);
if (!activitiesData || !activitiesData[section]) { container.innerHTML = '
No data
'; return; }
const moods = activitiesData[section];
let html = '';
for (const [mood, entries] of Object.entries(moods)) {
const key = `${section}/${mood}`;
const isEditing = activitiesEditing[key];
const songs = entries.filter(e => e.type === 'listening').length;
const games = entries.filter(e => e.type === 'playing').length;
const watches = entries.filter(e => e.type === 'watching').length;
const competes = entries.filter(e => e.type === 'competing').length;
const streams = entries.filter(e => e.type === 'streaming').length;
let stats = `${songs}🎵 ${games}🎮`;
if (watches) stats += ` ${watches}📺`;
if (competes) stats += ` ${competes}🏆`;
if (streams) stats += ` ${streams}🔴`;
html += `
`;
html += `
`;
html += `▶ ${mood}`;
html += `${stats}`;
html += `
`;
html += `
`;
if (isEditing) {
html += activitiesRenderEditForm(section, mood, activitiesEditCache[key] || entries);
} else {
html += activitiesRenderView(section, mood, entries);
}
html += `
`;
}
container.innerHTML = html;
}
function activitiesRenderView(section, mood, entries) {
let html = '';
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
const icons = { listening: '🎵', playing: '🎮', watching: '📺', competing: '🏆', streaming: '🔴' };
const labels = { listening: 'Listening to', playing: 'Playing', watching: 'Watching', competing: 'Competing in', streaming: 'Streaming' };
const icon = icons[entry.type] || '🎮';
const label = labels[entry.type] || 'Playing';
// Encode entry data for the "Set as Activity" button
const entryData = encodeURIComponent(JSON.stringify({ type: entry.type, name: entry.name, state: entry.state || '', url: entry.url || '' }));
html += `
`;
html += `${icon}`;
html += `${label} ${escapeHtml(entry.name)}`;
if (entry.state) html += ` — ${escapeHtml(entry.state)}`;
html += ``;
html += `weight: ${entry.weight}`;
html += ``;
html += `
`;
}
html += `
`;
html += ``;
html += `
`;
return html;
}
function activitiesRenderEditForm(section, mood, entries) {
let html = '';
for (let i = 0; i < entries.length; i++) {
const e = entries[i];
html += `
`;
html += ``;
html += ``;
html += ``;
html += ``;
html += ``;
html += ``;
html += `
`;
}
html += `
`;
html += ``;
html += ``;
html += ``;
html += `
`;
return html;
}
function activitiesTypeChanged(section, mood, index) {
const typeEl = document.getElementById(`act-type-${section}-${mood}-${index}`);
const urlEl = document.getElementById(`act-url-${section}-${mood}-${index}`);
if (!typeEl || !urlEl) return;
urlEl.style.display = typeEl.value === 'streaming' ? '' : 'none';
}
function activitiesMoodToggle(section, mood) {
const el = document.getElementById(`act-content-${section}-${mood}`);
const iconEl = document.getElementById(`act-icon-${section}-${mood}`);
if (!el) return;
const isOpen = el.style.display === 'block';
el.style.display = isOpen ? 'none' : 'block';
if (iconEl) iconEl.textContent = isOpen ? '▶' : '▼';
}
function activitiesStartEdit(section, mood) {
const key = `${section}/${mood}`;
const entries = activitiesData[section][mood];
// Deep clone entries for editing
activitiesEditCache[key] = JSON.parse(JSON.stringify(entries));
activitiesEditing[key] = true;
activitiesRenderSection(section);
// Auto-expand the mood panel
const el = document.getElementById(`act-content-${section}-${mood}`);
const iconEl = document.getElementById(`act-icon-${section}-${mood}`);
if (el) el.style.display = 'block';
if (iconEl) iconEl.textContent = '▼';
}
function activitiesCancelEdit(section, mood) {
const key = `${section}/${mood}`;
delete activitiesEditing[key];
delete activitiesEditCache[key];
activitiesRenderSection(section);
}
function activitiesAddEntry(section, mood) {
const key = `${section}/${mood}`;
// First, sync current form values to cache
activitiesSyncFormToCache(section, mood);
activitiesEditCache[key].push({ type: 'listening', name: '', state: '', weight: 1 });
activitiesRenderSection(section);
// Keep the mood panel open
const el = document.getElementById(`act-content-${section}-${mood}`);
if (el) el.style.display = 'block';
}
function activitiesRemoveEntry(section, mood, index) {
const key = `${section}/${mood}`;
activitiesSyncFormToCache(section, mood);
activitiesEditCache[key].splice(index, 1);
activitiesRenderSection(section);
const el = document.getElementById(`act-content-${section}-${mood}`);
if (el) el.style.display = 'block';
}
function activitiesSyncFormToCache(section, mood) {
const key = `${section}/${mood}`;
const entries = activitiesEditCache[key] || [];
for (let i = 0; i < entries.length; i++) {
const typeEl = document.getElementById(`act-type-${section}-${mood}-${i}`);
const nameEl = document.getElementById(`act-name-${section}-${mood}-${i}`);
const stateEl = document.getElementById(`act-state-${section}-${mood}-${i}`);
const urlEl = document.getElementById(`act-url-${section}-${mood}-${i}`);
const weightEl = document.getElementById(`act-weight-${section}-${mood}-${i}`);
if (typeEl) entries[i].type = typeEl.value;
if (nameEl) entries[i].name = nameEl.value;
if (stateEl) entries[i].state = stateEl.value || undefined;
if (urlEl) entries[i].url = urlEl.value || undefined;
if (weightEl) entries[i].weight = parseInt(weightEl.value) || 1;
}
activitiesEditCache[key] = entries;
}
async function activitiesSave(section, mood) {
const key = `${section}/${mood}`;
activitiesSyncFormToCache(section, mood);
const entries = activitiesEditCache[key];
// Client-side validation
for (let i = 0; i < entries.length; i++) {
if (!entries[i].name || !entries[i].name.trim()) {
showNotification(`Entry ${i + 1}: name cannot be empty`, 'error');
return;
}
if (!entries[i].weight || entries[i].weight < 1) {
showNotification(`Entry ${i + 1}: weight must be at least 1`, 'error');
return;
}
if (entries[i].type === 'streaming' && !entries[i].url) {
showNotification(`Entry ${i + 1}: streaming requires a URL`, 'error');
return;
}
}
try {
await apiCall(`/activities/${section}/${mood}`, 'POST', { activities: entries });
showNotification(`Saved activities for ${section}/${mood}`, 'success');
delete activitiesEditing[key];
delete activitiesEditCache[key];
// Reload to get fresh data
await activitiesLoad();
} catch (e) {
showNotification('Save failed: ' + e.message, 'error');
}
}
// ============================================================================
// CURRENT ACTIVITY OVERRIDE
// ============================================================================
function _activityTypeIcon(type) {
return { listening: '🎵', playing: '🎮', watching: '📺', competing: '🏆', streaming: '🔴' }[type] || '🎮';
}
function _activityTypeLabel(type) {
return { listening: 'Listening to', playing: 'Playing', watching: 'Watching', competing: 'Competing in', streaming: 'Streaming' }[type] || 'Playing';
}
async function activityRefreshCurrent() {
const statusEl = document.getElementById('activity-override-status');
try {
const data = await apiCall('/activities/current');
const act = data.activity;
const isOverride = data.manual_override;
if (act) {
const icon = _activityTypeIcon(act.type);
const label = _activityTypeLabel(act.type);
let html = `${icon} ${label} ${escapeHtml(act.name)}`;
if (act.state) html += ` — ${escapeHtml(act.state)}`;
if (isOverride) html += ` ⚡ MANUAL OVERRIDE (30 min)`;
statusEl.innerHTML = html;
} else {
let html = 'No activity (idle)';
if (isOverride) html += ' ⚡ MANUAL OVERRIDE';
statusEl.innerHTML = html;
}
} catch (e) {
statusEl.innerHTML = `Error: ${e.message}`;
}
}
async function activitySetManual() {
const type = document.getElementById('act-manual-type').value;
const name = document.getElementById('act-manual-name').value.trim();
const state = document.getElementById('act-manual-state').value.trim();
const url = document.getElementById('act-manual-url').value.trim();
if (!name) { showNotification('Activity name is required', 'error'); return; }
if (type === 'streaming' && !url) { showNotification('Streaming requires a URL', 'error'); return; }
try {
const body = { type, name };
if (state) body.state = state;
if (url) body.url = url;
await apiCall('/activities/current', 'POST', body);
showNotification(`Set activity: ${type} ${name}`, 'success');
await activityRefreshCurrent();
} catch (e) {
showNotification('Failed to set activity: ' + e.message, 'error');
}
}
async function activitySetFromEntry(btnElement) {
const raw = btnElement.getAttribute('data-entry');
if (!raw) return;
let entry;
try { entry = JSON.parse(decodeURIComponent(raw)); } catch (e) { showNotification('Failed to parse activity data', 'error'); return; }
const type = entry.type;
const name = entry.name;
const state = entry.state || null;
const url = entry.url || null;
if (!name) { showNotification('Activity name is empty', 'error'); return; }
if (type === 'streaming' && !url) { showNotification('Streaming requires a URL', 'error'); return; }
try {
const body = { type, name };
if (state) body.state = state;
if (url) body.url = url;
await apiCall('/activities/current', 'POST', body);
const icon = _activityTypeIcon(type);
showNotification(`${icon} Set activity: ${name}`, 'success');
await activityRefreshCurrent();
} catch (e) {
showNotification('Failed to set activity: ' + e.message, 'error');
}
}
async function activityClearManual() {
try {
await apiCall('/activities/current', 'DELETE');
showNotification('Activity cleared (manual override active)', 'success');
await activityRefreshCurrent();
} catch (e) {
showNotification('Failed to clear: ' + e.message, 'error');
}
}
async function activityReleaseAuto() {
try {
await apiCall('/activities/current/auto', 'POST');
showNotification('Returned to automatic mode', 'success');
await activityRefreshCurrent();
} catch (e) {
showNotification('Failed to release override: ' + e.message, 'error');
}
}
// Show/hide URL field when streaming is selected in manual override
document.getElementById('act-manual-type').addEventListener('change', function() {
document.getElementById('act-manual-url').style.display = this.value === 'streaming' ? '' : 'none';
});