(() => { // Settings script — and loaded via AJAX or full page load specifically for settings. var i18n = window.f0ckI18n || {}; // Update the navbar avatar image live and all rendered avatars for the current user const updateNavAvatar = (src) => { // 1. Navbar document.querySelectorAll('img.nav-avatar-img').forEach(img => img.src = src); // 2. Sidebar & comment avatars — identify by link to the current user's profile const username = window.f0ckSession?.user?.toLowerCase(); if (username) { // sidebar-activity.js: document.querySelectorAll(`a.sidebar-avatar-link[href="/user/${username}"] img.sidebar-avatar`).forEach(img => img.src = src); // comments.js:
document.querySelectorAll(`.comment-avatar a[href="/user/${username}"] img`).forEach(img => img.src = src); // f0ckm.js activity panel:
document.querySelectorAll(`#sidebar-activity-container .comment-avatar a[href="/user/${username}"] img`).forEach(img => img.src = src); } }; // Update the username color live across the navbar and all rendered username links const updateNavColor = (color) => { // 1. Navbar display name const navName = document.getElementById('nav-display-name'); if (navName) navName.style.color = color; // 2. Comment / sidebar / post username links for the current user const username = window.f0ckSession?.user?.toLowerCase(); if (username) { document.querySelectorAll(`a[href="/user/${username}"]`).forEach(el => { el.style.color = color; }); } }; // Update the display name live across the navbar, sidebar activity and all profile links const updateNavDisplayName = (name) => { const username = window.f0ckSession?.user?.toLowerCase(); if (!username) return; const displayName = name || window.f0ckSession?.user || 'User'; // 1. Navbar const navName = document.getElementById('nav-display-name'); if (navName) navName.textContent = displayName; // 2. All profile links that show the display name document.querySelectorAll(`a[href="/user/${username}"]`).forEach(el => { // Update link text if it currently matches the (old) display name or username // and it's not just an image container if (el.classList.contains('comment-author') || el.id === 'a_username' || el.classList.contains('mention')) { el.textContent = (el.classList.contains('mention') ? '@' : '') + displayName; } // Update tooltips (used in favs list, etc.) if (el.hasAttribute('tooltip')) { el.setAttribute('tooltip', displayName); } }); // 3. Update sidebar activity cache if it exists if (window._sidebarActivityCache && Array.isArray(window._sidebarActivityCache)) { window._sidebarActivityCache.forEach(c => { if (c.username && c.username.toLowerCase() === username) { c.display_name = name; } }); } // 4. Update session if (window.f0ckSession) window.f0ckSession.display_name = name; }; // ==== Avatar Item ID Logic (existing) ==== const saveAvatar = async e => { e.preventDefault(); const avatar = +document.querySelector('input[name="i_avatar"]').value; let res = await fetch('/api/v2/settings/setAvatar', { // ... content continues ... method: 'PUT', headers: { "Content-Type": "application/json", "X-CSRF-Token": window.f0ckSession?.csrf_token }, body: JSON.stringify({ avatar }) }); const code = res.status; res = await res.json(); switch (code) { case 200: // Update preview to show item thumbnail const preview = document.getElementById('avatar-preview'); if (preview) { if (preview.tagName === 'IMG') { preview.src = `/t/${avatar}.webp`; } else { // Replace placeholder with img const img = document.createElement('img'); img.id = 'avatar-preview'; img.className = 'avatar-preview-img'; img.src = `/t/${avatar}.webp`; preview.replaceWith(img); } } // Update navbar avatar updateNavAvatar(`/t/${avatar}.webp`); showStatus('Avatar updated to item #' + avatar, 'success'); break; default: showStatus(res.msg || 'Failed to set avatar', 'error'); break; } }; const sAvatar = document.querySelector('button#s_avatar'); if (sAvatar) sAvatar.addEventListener('click', saveAvatar); const iAvatar = document.querySelector('input[name="i_avatar"]'); if (iAvatar) iAvatar.addEventListener('keyup', async e => { if (e.key === 'Enter') await saveAvatar(e); }); // ==== Avatar File Upload Logic ==== const fileInput = document.getElementById('avatar-file-input'); const chooseBtn = document.getElementById('avatar-choose-btn'); const filenameSpan = document.getElementById('avatar-filename'); const uploadBtn = document.getElementById('avatar-upload-btn'); const removeBtn = document.getElementById('avatar-remove-btn'); const progressWrapper = document.getElementById('avatar-progress-wrapper'); const progressFill = document.getElementById('avatar-progress-fill'); const progressText = document.getElementById('avatar-progress-text'); const statusDiv = document.getElementById('avatar-upload-status'); const preview = document.getElementById('avatar-preview'); const showStatus = (msg, type) => { if (statusDiv) { statusDiv.textContent = msg; statusDiv.className = 'avatar-status ' + type; } }; const maxSize = 5 * 1024 * 1024; // 5MB const allowedTypes = ['image/gif', 'image/jpeg', 'image/png', 'image/webp']; if (chooseBtn && fileInput) { chooseBtn.addEventListener('click', () => fileInput.click()); fileInput.addEventListener('change', () => { const file = fileInput.files[0]; if (!file) { filenameSpan.textContent = 'No file selected'; uploadBtn.disabled = true; return; } // Validate file type if (!allowedTypes.includes(file.type)) { showStatus('Invalid file type. Allowed: gif, jpg, png, webp', 'error'); filenameSpan.textContent = 'Invalid file'; uploadBtn.disabled = true; return; } // Validate file size if (file.size > maxSize) { showStatus(`File too large. Max 5MB, got ${(file.size / 1024 / 1024).toFixed(2)}MB`, 'error'); filenameSpan.textContent = 'File too large'; uploadBtn.disabled = true; return; } filenameSpan.textContent = file.name; uploadBtn.disabled = false; showStatus('', ''); }); } if (uploadBtn) { uploadBtn.addEventListener('click', async () => { const file = fileInput.files[0]; if (!file) return; uploadBtn.disabled = true; chooseBtn.disabled = true; progressWrapper.style.display = 'flex'; progressFill.style.width = '0%'; progressText.textContent = '0%'; showStatus(i18n.uploading || 'Uploading...', ''); const formData = new FormData(); formData.append('file', file); const xhr = new XMLHttpRequest(); xhr.upload.addEventListener('progress', (e) => { if (e.lengthComputable) { const percent = Math.round((e.loaded / e.total) * 100); progressFill.style.width = percent + '%'; progressText.textContent = percent + '%'; } }); xhr.addEventListener('load', () => { try { const res = JSON.parse(xhr.responseText); if (xhr.status === 200 && res.success) { showStatus(res.msg || 'Avatar uploaded!', 'success'); // Update preview if (preview) { if (preview.tagName === 'IMG') { preview.src = '/a/' + res.avatar_file + '?t=' + Date.now(); } else { const img = document.createElement('img'); img.id = 'avatar-preview'; img.className = 'avatar-preview-img'; img.src = '/a/' + res.avatar_file + '?t=' + Date.now(); preview.replaceWith(img); } } // Update navbar avatar updateNavAvatar('/a/' + res.avatar_file + '?t=' + Date.now()); // Show remove button if not present const existingRemoveBtn = document.getElementById('avatar-remove-btn'); if (!existingRemoveBtn) { const actionsDiv = document.querySelector('.avatar-upload-actions'); if (actionsDiv) { const btn = document.createElement('button'); btn.type = 'button'; btn.id = 'avatar-remove-btn'; btn.className = 'button button-danger'; btn.textContent = 'Remove Custom'; actionsDiv.appendChild(btn); } } // Reset file input fileInput.value = ''; filenameSpan.textContent = 'No file selected'; } else { showStatus(res.msg || 'Upload failed', 'error'); } } catch (e) { showStatus('Upload failed: Invalid response', 'error'); } progressWrapper.style.display = 'none'; uploadBtn.disabled = true; chooseBtn.disabled = false; }); xhr.addEventListener('error', () => { showStatus('Upload failed: Network error', 'error'); progressWrapper.style.display = 'none'; uploadBtn.disabled = true; chooseBtn.disabled = false; }); xhr.open('POST', '/api/v2/settings/uploadAvatar'); xhr.setRequestHeader('X-CSRF-Token', window.f0ckSession?.csrf_token); xhr.send(formData); }); } // Remove custom avatar handler (uses event delegation for dynamically added button) document.addEventListener('click', async (e) => { if (e.target.id === 'avatar-remove-btn') { e.target.disabled = true; e.target.textContent = i18n.hall_removing || 'Removing...'; try { const res = await fetch('/api/v2/settings/uploadAvatar', { method: 'DELETE', headers: { "X-CSRF-Token": window.f0ckSession?.csrf_token } }); const data = await res.json(); if (data.success) { showStatus('Custom avatar removed', 'success'); e.target.remove(); // Reload to show item-based avatar or placeholder setTimeout(() => window.location.reload(), 500); } else { showStatus(data.msg || 'Failed to remove', 'error'); e.target.disabled = false; e.target.textContent = 'Remove Custom'; } } catch (err) { showStatus('Failed to remove avatar', 'error'); e.target.disabled = false; e.target.textContent = 'Remove Custom'; } } // Use Custom Avatar button handler if (e.target.id === 'use-custom-btn') { e.target.disabled = true; e.target.textContent = i18n.switching || 'Switching...'; try { const res = await fetch('/api/v2/settings/useCustomAvatar', { method: 'PUT', headers: { "X-CSRF-Token": window.f0ckSession?.csrf_token } }); const data = await res.json(); if (data.success) { showStatus('Switched to custom avatar', 'success'); // Update preview const preview = document.getElementById('avatar-preview'); if (preview && preview.tagName === 'IMG') { preview.src = '/a/' + data.avatar_file + '?t=' + Date.now(); } // Update navbar avatar updateNavAvatar('/a/' + data.avatar_file + '?t=' + Date.now()); // Clear item ID input const itemInput = document.querySelector('input[name="i_avatar"]'); if (itemInput) itemInput.value = '0'; // Re-enable the button e.target.disabled = false; e.target.textContent = 'Use Custom'; } else { showStatus(data.msg || 'Failed to switch', 'error'); e.target.disabled = false; e.target.textContent = 'Use Custom'; } } catch (err) { showStatus('Failed to switch avatar', 'error'); e.target.disabled = false; e.target.textContent = 'Use Custom'; } } }); // Generic Linking Logic const genTokenBtn = document.getElementById('btn-gen-link-token'); const linkedAccountsList = document.getElementById('linked-accounts-list'); const tokenDisplay = document.getElementById('token-display-area'); const tokenCode = document.getElementById('generated-token-code'); const renderLinkedAccounts = (aliases) => { if (!linkedAccountsList) return; if (aliases.length === 0) { linkedAccountsList.innerHTML = 'No linked accounts'; return; } linkedAccountsList.innerHTML = ''; aliases.forEach(a => { const div = document.createElement('div'); div.className = 'linked-account-item'; div.style.cssText = 'display: flex; align-items: center; justify-content: space-between; background: rgba(0,0,0,0.1); padding: 8px; border-radius: 4px;'; const infoDiv = document.createElement('div'); infoDiv.style.display = 'flex'; infoDiv.style.alignItems = 'center'; infoDiv.style.gap = '10px'; // Icon string based on type let icon = '🔗'; let platformName = 'Unknown'; if (a.type === 'discord') { icon = ''; platformName = 'Discord'; } else if (a.type === 'matrix') { icon = ''; platformName = 'Matrix'; } // Simple text fallback if fontawesome not fully loaded/supported in snippet if (a.type === 'matrix') icon = '[Matrix]'; if (a.type === 'discord') icon = '[Discord]'; infoDiv.innerHTML = `${icon} ${escHTML(a.alias || '')}`; const btn = document.createElement('button'); btn.className = 'button button-danger button-small unlink-account-btn'; btn.setAttribute('data-alias', a.alias); btn.setAttribute('data-type', a.type); btn.style.cssText = 'padding: 2px 8px; font-size: 0.8rem; height: auto;'; btn.textContent = 'Unlink'; div.appendChild(infoDiv); div.appendChild(btn); linkedAccountsList.appendChild(div); }); }; const loadLinkedAccounts = async () => { try { const res = await fetch('/api/v2/settings/link/accounts'); const data = await res.json(); if (data.success) { renderLinkedAccounts(data.aliases); } else { linkedAccountsList.innerHTML = 'Error loading accounts'; } } catch (err) { console.error(err); linkedAccountsList.innerHTML = 'Error loading accounts'; } }; if (linkedAccountsList) { loadLinkedAccounts(); } // Unlink handler document.addEventListener('click', async (e) => { if (e.target.classList.contains('unlink-account-btn')) { const alias = e.target.getAttribute('data-alias'); const type = e.target.getAttribute('data-type'); if (!confirm(`Are you sure you want to unlink this ${type} account?`)) return; e.target.disabled = true; e.target.textContent = '...'; try { const res = await fetch(`/api/v2/settings/link/unlink/${type}/${encodeURIComponent(alias)}`, { method: 'DELETE', headers: { "X-CSRF-Token": window.f0ckSession?.csrf_token } }); const data = await res.json(); if (data.success) { loadLinkedAccounts(); // Refresh } else { alert(data.msg || 'Failed to unlink'); e.target.disabled = false; e.target.textContent = 'Unlink'; } } catch (err) { console.error(err); alert('Request failed'); e.target.disabled = false; e.target.textContent = 'Unlink'; } } }); if (genTokenBtn) { genTokenBtn.addEventListener('click', async () => { genTokenBtn.disabled = true; genTokenBtn.textContent = i18n.generating || 'Generating...'; try { // Type is optional, default token works for both if backend matches correctly. // But typically we generate a generic token now. const res = await fetch('/api/v2/settings/link/token', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token }, body: JSON.stringify({ type: 'generic' }) }); const data = await res.json(); if (data.success) { tokenCode.textContent = data.token; tokenDisplay.style.display = 'block'; genTokenBtn.textContent = 'Generate New Token'; genTokenBtn.disabled = false; } else { alert(data.msg || 'Failed to generate token'); genTokenBtn.disabled = false; genTokenBtn.textContent = 'Generate Link Token'; } } catch (err) { console.error(err); alert('Request failed'); genTokenBtn.disabled = false; genTokenBtn.textContent = 'Generate Link Token'; } }); } // MOTD Visibility Toggle const motdToggle = document.getElementById('show_motd_toggle'); if (motdToggle) { motdToggle.addEventListener('change', async () => { const show = motdToggle.checked; try { const res = await fetch('/api/v2/settings/motd', { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token }, body: JSON.stringify({ show }) }); const data = await res.json(); if (data.success) { const userPrefEl = document.getElementById('user-pref-show-motd'); if (userPrefEl) userPrefEl.innerText = show ? 'true' : 'false'; if (show) window['motd_dismissed'] = false; // Reset dismissal if re-enabling if (typeof window.updateMotdUI === 'function') { const dataEl = document.getElementById('motd-data'); window.updateMotdUI(dataEl ? dataEl.innerText : ''); } } else { alert(data.msg || 'Error saving preference'); motdToggle.checked = !show; // Revert } } catch (err) { console.error(err); alert('Failed to save MOTD preference'); motdToggle.checked = !show; // Revert } }); } const swipingToggle = document.getElementById('disable_swiping_toggle'); if (swipingToggle) { swipingToggle.addEventListener('change', async () => { const disable_swiping = swipingToggle.checked; try { const res = await fetch('/api/v2/settings/swiping', { method: 'PUT', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRF-Token': window.f0ckSession.csrf_token }, body: new URLSearchParams({ disable_swiping }) }); const data = await res.json(); if (data.success) { showStatus('Swiping preference updated!', 'success'); if (window.f0ckSession) { window.f0ckSession.disable_swiping = disable_swiping; } } else { alert(data.msg || 'Error saving preference'); swipingToggle.checked = !disable_swiping; // Revert } } catch (err) { console.error('Update Swiping error:', err); alert('Connection error'); swipingToggle.checked = !disable_swiping; // Revert } }); } // New Dual Column Layout Toggle const layoutToggle = document.getElementById('use_new_layout_toggle'); if (layoutToggle) { layoutToggle.addEventListener('change', async () => { const use_new_layout = layoutToggle.checked; try { const res = await fetch('/api/v2/settings/layout', { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token }, body: JSON.stringify({ use_new_layout }) }); const data = await res.json(); if (data.success) { window.location.reload(); } else { alert(data.msg || 'Error saving preference'); layoutToggle.checked = !use_new_layout; // Revert } } catch (err) { console.error(err); alert('Failed to save Layout preference'); layoutToggle.checked = !use_new_layout; // Revert } }); } // Disable Autoplay Toggle const autoplayToggle = document.getElementById('disable_autoplay_toggle'); if (autoplayToggle) { autoplayToggle.addEventListener('change', async () => { const disable_autoplay = autoplayToggle.checked; try { const res = await fetch('/api/v2/settings/autoplay', { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token }, body: JSON.stringify({ disable_autoplay }) }); const data = await res.json(); if (data.success) { showStatus('Autoplay preference updated!', 'success'); if (window.f0ckSession) { window.f0ckSession.disable_autoplay = disable_autoplay; } } else { alert(data.msg || 'Error saving preference'); autoplayToggle.checked = !disable_autoplay; // Revert } } catch (err) { console.error(err); alert('Failed to save Autoplay preference'); autoplayToggle.checked = !disable_autoplay; // Revert } }); } const wheelToggle = document.getElementById('wheel_nav_toggle'); if (wheelToggle) { wheelToggle.checked = localStorage.getItem('wheelNavEnabled') === 'true'; wheelToggle.addEventListener('change', () => { localStorage.setItem('wheelNavEnabled', wheelToggle.checked); }); } // Background Blur Toggle const backgroundToggle = document.getElementById('show_background_toggle'); if (backgroundToggle) { backgroundToggle.addEventListener('change', async () => { const show_background = backgroundToggle.checked; try { const res = await fetch('/api/v2/settings/background', { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token }, body: JSON.stringify({ show_background }) }); const data = await res.json(); if (data.success) { // Immediate reaction window.background = show_background; localStorage.setItem('background', show_background ? 'true' : 'false'); if (window.initBackground) window.initBackground(); // Update session if (window.f0ckSession) { window.f0ckSession.show_background = show_background; } // Update videoplayer toggle buttons if they exist document.querySelectorAll("#togglebg").forEach(el => { el.classList.toggle('active', show_background); }); } else { alert(data.msg || 'Error saving preference'); backgroundToggle.checked = !show_background; } } catch (err) { console.error(err); alert('Failed to save background preference'); backgroundToggle.checked = !show_background; } }); } // Quote Emojis Toggle const quoteEmojisToggle = document.getElementById('quote_emojis_toggle'); if (quoteEmojisToggle) { quoteEmojisToggle.addEventListener('change', async () => { const quote_emojis = quoteEmojisToggle.checked; try { const res = await fetch('/api/v2/settings/quote_emojis', { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token }, body: JSON.stringify({ quote_emojis }) }); const data = await res.json(); if (data.success) { showStatus('Quote emoji preference updated!', 'success'); if (window.f0ckSession) window.f0ckSession.quote_emojis = quote_emojis; } else { alert(data.msg || 'Error saving preference'); quoteEmojisToggle.checked = !quote_emojis; } } catch (err) { console.error(err); alert('Failed to save preference'); quoteEmojisToggle.checked = !quote_emojis; } }); } // Embed YouTube In Comments Toggle const embedYoutubeToggle = document.getElementById('embed_youtube_in_comments_toggle'); if (embedYoutubeToggle) { embedYoutubeToggle.addEventListener('change', async () => { const embed_youtube_in_comments = embedYoutubeToggle.checked; try { const res = await fetch('/api/v2/settings/embed_youtube_in_comments', { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token }, body: JSON.stringify({ embed_youtube_in_comments }) }); const data = await res.json(); if (data.success) { showStatus('YouTube embed preference updated!', 'success'); if (window.f0ckSession) window.f0ckSession.embed_youtube_in_comments = embed_youtube_in_comments; } else { alert(data.msg || 'Error saving preference'); embedYoutubeToggle.checked = !embed_youtube_in_comments; } } catch (err) { console.error(err); alert('Failed to save preference'); embedYoutubeToggle.checked = !embed_youtube_in_comments; } }); } // Hide Köpfe Toggle const hideKoepfeToggle = document.getElementById('hide_koepfe_toggle'); if (hideKoepfeToggle) { hideKoepfeToggle.addEventListener('change', async () => { const hide_koepfe = hideKoepfeToggle.checked; try { const res = await fetch('/api/v2/settings/hide_koepfe', { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token }, body: JSON.stringify({ hide_koepfe }) }); const data = await res.json(); if (data.success) { showStatus('Köpfe preference updated!', 'success'); if (window.f0ckSession) window.f0ckSession.hide_koepfe = hide_koepfe; // Immediately show/hide the koepfe image without a reload const koepfeImg = document.getElementById('koepfe-img'); if (koepfeImg) koepfeImg.style.display = hide_koepfe ? 'none' : ''; } else { alert(data.msg || 'Error saving preference'); hideKoepfeToggle.checked = !hide_koepfe; } } catch (err) { console.error(err); alert('Failed to save preference'); hideKoepfeToggle.checked = !hide_koepfe; } }); } // Language Preference Selector const languageSelect = document.getElementById('language_select'); if (languageSelect) { languageSelect.addEventListener('change', async () => { const language = languageSelect.value; // '' = site default, 'en', 'de', etc. try { const res = await fetch('/api/v2/settings/language', { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token }, body: JSON.stringify({ language }) }); const data = await res.json(); if (data.success) { // Set/clear cookie so the running server picks up the change immediately if (language) { document.cookie = `language=${encodeURIComponent(language)}; path=/; max-age=${365 * 24 * 3600}; SameSite=Lax`; } else { document.cookie = `language=; path=/; max-age=0; SameSite=Lax`; } // Reload so server renders in the new language window.location.reload(); } else { alert(data.msg || 'Error saving language preference'); } } catch (err) { console.error(err); alert('Failed to save language preference'); } }); } const colorPicker = document.getElementById('username_color_picker'); const colorHex = document.getElementById('username_color_hex'); const saveColorBtn = document.getElementById('btn-save-username-color'); const resetColorBtn = document.getElementById('btn-reset-username-color'); // Two-way sync: swatch → text input if (colorPicker && colorHex) { colorPicker.addEventListener('input', () => { colorHex.value = colorPicker.value; }); // Text input → swatch (validate on each keystroke) colorHex.addEventListener('input', () => { const val = colorHex.value.trim(); if (/^#[0-9a-fA-F]{6}$/.test(val)) { colorPicker.value = val; colorHex.style.borderColor = ''; } else { colorHex.style.borderColor = '#e74c3c'; } }); // On blur, normalise to lowercase colorHex.addEventListener('blur', () => { const val = colorHex.value.trim(); if (/^#[0-9a-fA-F]{6}$/.test(val)) { colorHex.value = val.toLowerCase(); colorHex.style.borderColor = ''; } else { // Revert to swatch value if invalid colorHex.value = colorPicker.value; colorHex.style.borderColor = ''; } }); } if (saveColorBtn && colorPicker) { saveColorBtn.addEventListener('click', async () => { const color = colorPicker.value; saveColorBtn.disabled = true; saveColorBtn.textContent = i18n.saving || 'Saving...'; try { const res = await fetch('/api/v2/settings/username_color', { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token }, body: JSON.stringify({ color }) }); const data = await res.json(); if (data.success) { showStatus('Username color saved!', 'success'); updateNavColor(color); } else { alert(data.msg || 'Error saving color'); } } catch (err) { console.error(err); alert('Failed to save username color'); } finally { saveColorBtn.disabled = false; saveColorBtn.textContent = 'Save Color'; } }); } if (resetColorBtn && colorPicker) { resetColorBtn.addEventListener('click', async () => { const defaultColor = '#ffffff'; colorPicker.value = defaultColor; if (colorHex) { colorHex.value = defaultColor; colorHex.style.borderColor = ''; } resetColorBtn.disabled = true; resetColorBtn.textContent = 'Resetting...'; try { const res = await fetch('/api/v2/settings/username_color', { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token }, body: JSON.stringify({ color: defaultColor }) }); const data = await res.json(); if (data.success) { showStatus('Username color reset to default', 'success'); updateNavColor(defaultColor); } else { alert(data.msg || 'Error resetting color'); } } catch (err) { console.error(err); alert('Failed to reset username color'); } finally { resetColorBtn.disabled = false; resetColorBtn.textContent = 'Reset'; } }); } // Website Theme Selection (immediate — cookie + attribute, no API needed) const themeSelect = document.getElementById('website_theme_select'); if (themeSelect) { themeSelect.addEventListener('change', () => { const theme = themeSelect.value; document.documentElement.setAttribute('theme', theme); // Set cookie in same way theme.js does (1-year expiry, SameSite=Strict) document.cookie = `theme=${encodeURIComponent(theme)}; path=/; max-age=${360 * 24 * 3600}; SameSite=Strict`; showStatus(`Theme changed to ${theme}`, 'success'); }); } // Website Font Selection const fontSelect = document.getElementById('website_font_select'); const applyFontLive = async (fontFile) => { let style = document.getElementById('live-font-style'); if (!fontFile) { // Revert to default: remove injected style if (style) style.remove(); return; } // Preload the font via FontFace API so it's available immediately try { const face = new FontFace('CustomUserFont', `url(/s/fonts/${fontFile})`); await face.load(); document.fonts.add(face); } catch (e) { console.warn('[font] FontFace preload failed, continuing anyway:', e); } // Inject/update the same style rules that header.html uses if (!style) { style = document.createElement('style'); style.id = 'live-font-style'; document.head.appendChild(style); } style.textContent = ` @font-face { font-family: 'CustomUserFont'; src: url('/s/fonts/${fontFile}'); } :root { --font: 'CustomUserFont', monospace !important; } *:not(.fa-solid):not(.fa-regular):not(.fa-brands):not(.fa-light):not(.fa-thin):not(.fa-duotone):not([class*=" fa-"]):not([class^="fa-"]) { font-family: 'CustomUserFont', monospace !important; } .fa-solid::before, .fa-regular::before, .fa-brands::before, .fas::before, .far::before, .fab::before, .fa::before { font-family: "Font Awesome 6 Free", "Font Awesome 6 Brands" !important; } `; }; if (fontSelect) { fontSelect.addEventListener('change', async () => { const font = fontSelect.value; try { const res = await fetch('/api/v2/settings/font', { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token }, body: JSON.stringify({ font }) }); const data = await res.json(); if (data.success) { await applyFontLive(font); showStatus('Website font updated!', 'success'); } else { alert(data.msg || 'Error saving font preference'); } } catch (err) { console.error(err); alert('Failed to save font preference'); } }); } // ==== Profile Description Logic ==== const descriptionTextarea = document.getElementById('profile_description'); const saveDescriptionBtn = document.getElementById('btn-save-description'); const descriptionStatus = document.getElementById('description-status'); if (saveDescriptionBtn && descriptionTextarea) { saveDescriptionBtn.addEventListener('click', async () => { const description = descriptionTextarea.value; saveDescriptionBtn.disabled = true; saveDescriptionBtn.textContent = i18n.saving || 'Saving...'; try { const res = await fetch('/api/v2/settings/description', { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token }, body: JSON.stringify({ description }) }); const data = await res.json(); if (data.success) { showAccountStatus(descriptionStatus, 'Description updated successfully!', 'success'); } else { alert(data.msg || 'Error saving description'); } } catch (err) { console.error(err); alert('Failed to save description'); } finally { saveDescriptionBtn.disabled = false; saveDescriptionBtn.textContent = 'Save Description'; } }); } const clearDescriptionBtn = document.getElementById('btn-clear-description'); if (clearDescriptionBtn && descriptionTextarea) { clearDescriptionBtn.addEventListener('click', async () => { if (!confirm('Clear your profile description?')) return; descriptionTextarea.value = ''; clearDescriptionBtn.disabled = true; try { const res = await fetch('/api/v2/settings/description', { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token }, body: JSON.stringify({ description: '' }) }); const data = await res.json(); if (data.success) { showAccountStatus(descriptionStatus, 'Description cleared!', 'success'); } else { alert(data.msg || 'Error clearing description'); } } catch (err) { console.error(err); alert('Failed to clear description'); } finally { clearDescriptionBtn.disabled = false; } }); } // Website Font Size Selection // ==== Account Settings Logic ==== const passwordForm = document.getElementById('password-change-form'); const passwordStatus = document.getElementById('password-status'); const emailForm = document.getElementById('email-update-form'); const emailStatus = document.getElementById('email-status'); const displayEmail = document.getElementById('display-email'); const showAccountStatus = (el, msg, type) => { if (el) { el.textContent = msg; el.className = 'avatar-status ' + type; if (type === 'success') { setTimeout(() => { el.textContent = ''; el.className = 'avatar-status'; }, 5000); } } }; if (passwordForm) { passwordForm.addEventListener('submit', async (e) => { e.preventDefault(); const current_password = document.getElementById('current_password').value; const new_password = document.getElementById('new_password').value; const new_password_confirm = document.getElementById('new_password_confirm').value; if (new_password !== new_password_confirm) { showAccountStatus(passwordStatus, 'New passwords do not match', 'error'); return; } const btn = passwordForm.querySelector('button'); btn.disabled = true; btn.textContent = 'Updating...'; try { const res = await fetch('/api/v2/settings/password', { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token }, body: JSON.stringify({ current_password, new_password, new_password_confirm }) }); const data = await res.json(); if (data.success) { showAccountStatus(passwordStatus, data.msg || 'Password updated correctly', 'success'); passwordForm.reset(); } else { showAccountStatus(passwordStatus, data.msg || 'Failed to update password', 'error'); } } catch (err) { showAccountStatus(passwordStatus, 'Request failed', 'error'); } finally { btn.disabled = false; btn.textContent = 'Update Password'; } }); } if (emailForm) { emailForm.addEventListener('submit', async (e) => { e.preventDefault(); const email = document.getElementById('email_input').value; const btn = emailForm.querySelector('button'); btn.disabled = true; btn.textContent = 'Updating...'; try { const res = await fetch('/api/v2/settings/email', { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token }, body: JSON.stringify({ email }) }); const data = await res.json(); if (data.success) { showAccountStatus(emailStatus, data.msg || 'Email updated correctly', 'success'); if (displayEmail) displayEmail.textContent = email; } else { showAccountStatus(emailStatus, data.msg || 'Failed to update email', 'error'); } } catch (err) { showAccountStatus(emailStatus, 'Request failed', 'error'); } finally { btn.disabled = false; btn.textContent = 'Update Email'; } }); } // Display Name Update const displayNameBtn = document.getElementById('btn-update-display-name'); const displayNameInput = document.getElementById('display_name_input'); const displayNameStatus = document.getElementById('display-name-status'); if (displayNameBtn && displayNameInput) { displayNameBtn.addEventListener('click', async () => { const display_name = displayNameInput.value; displayNameBtn.disabled = true; displayNameBtn.textContent = '...'; try { const res = await fetch('/api/v2/settings/display_name', { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token }, body: JSON.stringify({ display_name }) }); const data = await res.json(); if (data.success) { showAccountStatus(displayNameStatus, data.msg || 'Display name updated!', 'success'); updateNavDisplayName(data.display_name); } else { showAccountStatus(displayNameStatus, data.msg || 'Failed to update display name', 'error'); } } catch (err) { showAccountStatus(displayNameStatus, 'Request failed', 'error'); } finally { displayNameBtn.disabled = false; displayNameBtn.textContent = 'Save'; } }); } // ==== Min xD Score Filter ==== const xdInput = document.getElementById('min_xd_score_input'); const xdValLabel = document.getElementById('xd_score_val'); const xdSaveBtn = document.getElementById('btn-save-min-xd-score'); const xdResetBtn = document.getElementById('btn-reset-min-xd-score'); const xdStatus = document.getElementById('xd-score-status'); const xdTierLabel = document.getElementById('xd_score_tier_label'); const XD_TIERS = [ null, { cls: 'xd-tier-1', label: 'xD' }, { cls: 'xd-tier-2', label: 'xDD' }, { cls: 'xd-tier-3', label: 'xDDD' }, { cls: 'xd-tier-4', label: 'xDDDD' }, { cls: 'xd-tier-5', label: 'xDDDDD+' }, ]; const getXdTier = (score) => { score = +score; if (score <= 0) return 0; if (score < 5) return 1; if (score < 15) return 2; if (score < 30) return 3; if (score < 60) return 4; return 5; }; const XD_TIER_COLORS = ['#888', '#5a9e5a', '#8ac449', '#d4a017', '#e07b2a', '#ff5500']; const updateXdUI = (score) => { score = +score; if (xdValLabel) xdValLabel.textContent = score; const tier = getXdTier(score); // Color-coded slider track if (xdInput) { const pct = Math.round((score / +xdInput.max) * 100); const color = XD_TIER_COLORS[tier]; xdInput.style.setProperty('--xd-fill', color); xdInput.style.setProperty('--xd-pct', pct + '%'); if (xdValLabel) xdValLabel.style.color = color; } if (!xdTierLabel) return; const t = XD_TIERS[tier]; if (!tier || score <= 0) { xdTierLabel.style.display = 'none'; return; } xdTierLabel.className = `xd-score-badge ${t.cls}`; xdTierLabel.textContent = `≥ ${score} · ${t.label}`; xdTierLabel.style.display = 'inline-flex'; }; if (xdInput) { updateXdUI(+xdInput.value); xdInput.addEventListener('input', () => updateXdUI(+xdInput.value)); } if (xdSaveBtn && xdInput) { xdSaveBtn.addEventListener('click', async () => { const min_xd_score = parseInt(xdInput.value, 10) || 0; xdSaveBtn.disabled = true; xdSaveBtn.textContent = i18n.saving || 'Saving...'; try { const res = await fetch('/api/v2/settings/min_xd_score', { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token }, body: JSON.stringify({ min_xd_score }) }); const data = await res.json(); if (data.success) { if (xdStatus) { xdStatus.textContent = min_xd_score > 0 ? `✓ Filter active — showing posts with xD score ≥ ${min_xd_score}` : '✓ Filter disabled'; xdStatus.className = 'avatar-status success'; setTimeout(() => { xdStatus.textContent = ''; xdStatus.className = 'avatar-status'; }, 4000); } } else { alert(data.msg || 'Error saving preference'); } } catch (err) { console.error(err); alert('Failed to save xD score preference'); } finally { xdSaveBtn.disabled = false; xdSaveBtn.textContent = 'Save'; } }); } if (xdResetBtn && xdInput) { xdResetBtn.addEventListener('click', () => { xdInput.value = 0; updateXdUI(0); xdSaveBtn?.click(); }); } // ==== Ruffle (Flash) Settings ==== const ruffleVolumeInput = document.getElementById('ruffle_volume_input'); const ruffleVolumeVal = document.getElementById('ruffle_volume_val'); const ruffleBackToggle = document.getElementById('ruffle_background_toggle'); const ruffleSaveBtn = document.getElementById('btn-save-ruffle-settings'); const ruffleStatus = document.getElementById('ruffle-settings-status'); if (ruffleVolumeInput && ruffleVolumeVal) { ruffleVolumeInput.addEventListener('input', () => { ruffleVolumeVal.textContent = Math.round(ruffleVolumeInput.value * 100) + '%'; }); } if (ruffleSaveBtn) { ruffleSaveBtn.addEventListener('click', async () => { const ruffle_volume = parseFloat(ruffleVolumeInput.value); const ruffle_background = ruffleBackToggle.checked; ruffleSaveBtn.disabled = true; ruffleSaveBtn.textContent = i18n.saving || 'Saving...'; try { const res = await fetch('/api/v2/settings/ruffle', { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token }, body: JSON.stringify({ ruffle_volume, ruffle_background }) }); const data = await res.json(); if (data.success) { showAccountStatus(ruffleStatus, 'Flash settings updated correctly!', 'success'); if (window.f0ckSession) { window.f0ckSession.ruffle_volume = ruffle_volume; window.f0ckSession.ruffle_background = ruffle_background; // Apply to the active Ruffle player if it exists so user doesn't need to refresh const ruffleContainer = document.getElementById('ruffle-container'); if (ruffleContainer) { const player = ruffleContainer.querySelector('ruffle-player') || ruffleContainer.querySelector('ruffle-object'); if (player) { player.volume = ruffle_volume; // Ruffle doesn't dynamically toggle pageVisibility well without recreation, // but we can update the config for subsequent initializations if (window.RufflePlayer && window.RufflePlayer.config) { window.RufflePlayer.config.pageVisibility = !ruffle_background; window.RufflePlayer.config.backgroundExecution = ruffle_background ? "Unthrottled" : undefined; } } } } } else { showAccountStatus(ruffleStatus, data.msg || 'Failed to update Flash settings', 'error'); } } catch (err) { console.error(err); showAccountStatus(ruffleStatus, 'Request failed', 'error'); } finally { ruffleSaveBtn.disabled = false; ruffleSaveBtn.textContent = 'Save Flash Settings'; } }); } })();