// ============================================================================ // Miku Control Panel — Profile Picture, Album & Activities Module // ============================================================================ // ============================================================================ // Profile Picture Tab (tab11) — Full Management // ============================================================================ // pfpCropper declared in core.js function getPfpCropMode() { const radio = document.querySelector('input[name="pfp-crop-mode"]:checked'); return radio ? radio.value : 'auto'; } function pfpSetStatus(text, color = '#61dafb') { const el = document.getElementById('pfp-tab-status'); if (el) { el.textContent = text; el.style.color = color; } } function pfpRefreshPreviews() { const t = Date.now(); const origImg = document.getElementById('pfp-preview-original'); const curImg = document.getElementById('pfp-preview-current'); if (origImg) origImg.src = `/profile-picture/image/original?t=${t}`; if (curImg) curImg.src = `/profile-picture/image/current?t=${t}`; } async function loadPfpTab() { // Load metadata try { const result = await apiCall('/profile-picture/metadata'); if (result.status === 'ok' && result.metadata) { const metaDiv = document.getElementById('pfp-tab-metadata'); const metaContent = document.getElementById('pfp-tab-metadata-content'); metaContent.textContent = JSON.stringify(result.metadata, null, 2); metaDiv.style.display = 'block'; // Show original dimensions if available const dimsEl = document.getElementById('pfp-original-dims'); if (dimsEl && result.metadata.original_width) { dimsEl.textContent = `${result.metadata.original_width}×${result.metadata.original_height}`; } } } catch (e) { console.error('Failed to load PFP metadata:', e); } // Load description try { const result = await apiCall('/profile-picture/description'); if (result.status === 'ok') { document.getElementById('pfp-description-editor').value = result.description || ''; } } catch (e) { console.error('Failed to load PFP description:', e); } // 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 --- async function pfpChangeDanbooru() { const mode = getPfpCropMode(); const selectedServer = document.getElementById('server-select').value; pfpSetStatus('⏳ Searching Danbooru...'); try { const endpoint = mode === 'manual' ? '/profile-picture/change-no-crop' : '/profile-picture/change'; const params = new URLSearchParams(); if (selectedServer !== 'all') params.append('guild_id', selectedServer); const url = params.toString() ? `${endpoint}?${params.toString()}` : endpoint; const result = await apiCall(url, 'POST'); if (result.status === 'ok') { pfpSetStatus(`✅ ${result.message}`, 'green'); showNotification('Profile picture changed!'); // Show metadata const metaDiv = document.getElementById('pfp-tab-metadata'); const metaContent = document.getElementById('pfp-tab-metadata-content'); if (result.metadata) { metaContent.textContent = JSON.stringify(result.metadata, null, 2); metaDiv.style.display = 'block'; } pfpRefreshPreviews(); // If manual mode, show crop interface if (mode === 'manual') { pfpShowCropInterface(); } } else { throw new Error(result.message || 'Unknown error'); } } catch (error) { console.error('PFP Danbooru error:', error); pfpSetStatus(`❌ Error: ${error.message}`, 'red'); } } // Keep old function names working (backwards compatibility for autonomous/API callers) async function changeProfilePicture() { await pfpChangeDanbooru(); } // --- Custom Upload --- async function pfpUploadCustom() { const fileInput = document.getElementById('pfp-tab-upload'); const mode = getPfpCropMode(); const selectedServer = document.getElementById('server-select').value; if (!fileInput.files || fileInput.files.length === 0) { showNotification('Please select an image file first', 'error'); return; } const file = fileInput.files[0]; if (!file.type.startsWith('image/')) { showNotification('Please select a valid image file', 'error'); return; } pfpSetStatus('⏳ Uploading and processing...'); try { const formData = new FormData(); formData.append('file', file); const endpoint = mode === 'manual' ? '/profile-picture/change-no-crop' : '/profile-picture/change'; let url = endpoint; if (selectedServer !== 'all') url += `?guild_id=${selectedServer}`; const response = await fetch(url, { method: 'POST', body: formData }); const result = await response.json(); if (response.ok && result.status === 'ok') { pfpSetStatus(`✅ ${result.message}`, 'green'); showNotification('Image uploaded successfully!'); fileInput.value = ''; if (result.metadata) { const metaDiv = document.getElementById('pfp-tab-metadata'); const metaContent = document.getElementById('pfp-tab-metadata-content'); metaContent.textContent = JSON.stringify(result.metadata, null, 2); metaDiv.style.display = 'block'; } pfpRefreshPreviews(); if (mode === 'manual') { pfpShowCropInterface(); } } else { throw new Error(result.message || 'Upload failed'); } } catch (error) { console.error('PFP upload error:', error); pfpSetStatus(`❌ Error: ${error.message}`, 'red'); showNotification(error.message, 'error'); } } // Keep old function name working async function uploadCustomPfp() { await pfpUploadCustom(); } // --- Restore Fallback --- async function pfpRestoreFallback() { if (!confirm('Are you sure you want to restore the original fallback avatar?')) return; pfpSetStatus('⏳ Restoring original avatar...'); try { const result = await apiCall('/profile-picture/restore-fallback', 'POST'); pfpSetStatus(`✅ ${result.message}`, 'green'); document.getElementById('pfp-tab-metadata').style.display = 'none'; pfpRefreshPreviews(); showNotification('Original avatar restored!'); } catch (error) { console.error('Restore fallback error:', error); pfpSetStatus(`❌ Error: ${error.message}`, 'red'); } } async function restoreFallbackPfp() { await pfpRestoreFallback(); } // --- Crop Interface --- function pfpShowCropInterface() { const section = document.getElementById('pfp-crop-section'); const img = document.getElementById('pfp-crop-image'); // Destroy previous cropper if any if (pfpCropper) { pfpCropper.destroy(); pfpCropper = null; } // Load original image img.src = `/profile-picture/image/original?t=${Date.now()}`; section.style.display = 'block'; img.onload = function() { pfpCropper = 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 pfpHideCropInterface() { if (pfpCropper) { pfpCropper.destroy(); pfpCropper = null; } document.getElementById('pfp-crop-section').style.display = 'none'; } // Re-crop: open crop interface on stored original function pfpRecrop() { pfpShowCropInterface(); } async function pfpApplyManualCrop() { if (!pfpCropper) { showNotification('No crop region selected', 'error'); return; } const data = pfpCropper.getData(true); // rounded integers pfpSetStatus('⏳ Applying manual crop...'); try { const response = await fetch('/profile-picture/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 response.json(); if (response.ok && result.status === 'ok') { pfpSetStatus(`✅ ${result.message}`, 'green'); showNotification('Manual crop applied!'); pfpHideCropInterface(); pfpRefreshPreviews(); // Refresh metadata if (result.metadata) { const metaContent = document.getElementById('pfp-tab-metadata-content'); const existing = metaContent.textContent ? JSON.parse(metaContent.textContent) : {}; Object.assign(existing, result.metadata); metaContent.textContent = JSON.stringify(existing, null, 2); } } else { throw new Error(result.message || 'Crop failed'); } } catch (error) { console.error('Manual crop error:', error); pfpSetStatus(`❌ Error: ${error.message}`, 'red'); } } async function pfpApplyAutoCrop() { pfpSetStatus('⏳ Running auto-crop (face detection)...'); try { const result = await apiCall('/profile-picture/auto-crop', 'POST'); if (result.status === 'ok') { pfpSetStatus(`✅ ${result.message}`, 'green'); showNotification('Auto-crop applied!'); pfpHideCropInterface(); pfpRefreshPreviews(); } else { throw new Error(result.message || 'Auto-crop failed'); } } catch (error) { console.error('Auto-crop error:', error); pfpSetStatus(`❌ Error: ${error.message}`, 'red'); } } // --- Description --- async function pfpSaveDescription() { const descEl = document.getElementById('pfp-description-editor'); const statusEl = document.getElementById('pfp-desc-status'); const description = descEl.value.trim(); if (!description) { statusEl.textContent = '⚠️ Description cannot be empty'; statusEl.style.color = 'orange'; return; } statusEl.textContent = '⏳ Saving description...'; statusEl.style.color = '#61dafb'; try { const response = await fetch('/profile-picture/description', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ description }) }); const result = await response.json(); if (response.ok && result.status === 'ok') { statusEl.textContent = '✅ Description saved & injected into Cat memory'; statusEl.style.color = 'green'; showNotification('Description saved!'); } else { throw new Error(result.message || 'Save failed'); } } catch (error) { console.error('Save description error:', error); statusEl.textContent = `❌ Error: ${error.message}`; statusEl.style.color = 'red'; } } async function pfpRegenerateDescription() { const statusEl = document.getElementById('pfp-desc-status'); statusEl.textContent = '⏳ Regenerating description via vision model...'; statusEl.style.color = '#61dafb'; try { const result = await apiCall('/profile-picture/regenerate-description', 'POST'); if (result.status === 'ok' && result.description) { document.getElementById('pfp-description-editor').value = result.description; statusEl.textContent = '✅ Description regenerated & saved'; statusEl.style.color = 'green'; showNotification('Description regenerated!'); } else { throw new Error(result.message || 'Regeneration failed'); } } catch (error) { console.error('Regenerate description error:', error); statusEl.textContent = `❌ Error: ${error.message}`; statusEl.style.color = 'red'; } } // --- Role Color (updated element IDs for tab11) --- async function setCustomRoleColor() { const statusDiv = document.getElementById('pfp-tab-role-color-status'); const hexInput = document.getElementById('pfp-tab-role-color-hex'); const hexColor = hexInput.value.trim(); if (!hexColor) { statusDiv.textContent = '⚠️ Please enter a hex color code'; statusDiv.style.color = 'orange'; return; } statusDiv.textContent = '⏳ Updating role colors...'; statusDiv.style.color = '#61dafb'; try { const formData = new FormData(); formData.append('hex_color', hexColor); const response = await fetch('/role-color/custom', { method: 'POST', body: formData }); const result = await response.json(); if (response.ok && result.status === 'ok') { statusDiv.textContent = `✅ ${result.message}`; statusDiv.style.color = 'green'; showNotification(`Role color updated to ${result.color.hex}`); } else { throw new Error(result.message || 'Failed to update role color'); } } catch (error) { console.error('Failed to set custom role color:', error); statusDiv.textContent = `❌ Error: ${error.message}`; statusDiv.style.color = 'red'; showNotification(error.message || 'Failed to update role color', 'error'); } } async function resetRoleColor() { const statusDiv = document.getElementById('pfp-tab-role-color-status'); statusDiv.textContent = '⏳ Resetting to fallback color...'; statusDiv.style.color = '#61dafb'; try { const result = await apiCall('/role-color/reset-fallback', 'POST'); statusDiv.textContent = `✅ ${result.message}`; statusDiv.style.color = 'green'; document.getElementById('pfp-tab-role-color-hex').value = '#86cecb'; showNotification('Role color reset to fallback #86cecb'); } catch (error) { console.error('Failed to reset role color:', error); statusDiv.textContent = `❌ Error: ${error.message}`; statusDiv.style.color = 'red'; } } // ============================================================================ // Album / Gallery System // ============================================================================ // albumEntries, albumSelectedId, albumChecked, albumCropper, albumOpen declared in core.js 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 = '
No album entries yet. Upload images or archive the current PFP.
'; return; } grid.innerHTML = albumEntries.map(e => { const id = e.id; const isSelected = id === albumSelectedId; const isChecked = albumChecked.has(id); const colorDot = e.dominant_color ? `` : ''; const label = (e.source || '').replace('custom_upload', 'upload').substring(0, 12); return `
${colorDot}${label}
`; }).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'; });