import { showToast } from './domUtils.js?v={{APP_QVER}}'; import { t, applyTranslations, setLocale } from './i18n.js?v={{APP_QVER}}'; import { updateAuthenticatedUI } from './auth.js?v={{APP_QVER}}'; import { withBase } from './basePath.js?v={{APP_QVER}}'; import { openTOTPModal } from './authModals.js?v={{APP_QVER}}'; /** * Fetch current user info (username, profile_picture, totp_enabled) */ async function fetchCurrentUser() { try { const res = await fetch(withBase('/api/profile/getCurrentUser.php'), { credentials: 'include' }); if (!res.ok) throw new Error(`HTTP ${res.status}`); return await res.json(); } catch (e) { console.warn('fetchCurrentUser failed:', e); return {}; } } /** * Normalize any profile‐picture URL: * - strip leading colons * - ensure exactly one leading slash */ function normalizePicUrl(raw) { if (!raw) return ''; // take only what's after the last colon const parts = raw.split(':'); let pic = parts[parts.length - 1]; // strip any stray colons pic = pic.replace(/^:+/, ''); // ensure exactly one leading slash if (pic) pic = '/' + pic.replace(/^\/+/, ''); return pic ? withBase(pic) : ''; } export async function openUserPanel() { // 1) load data const { username = 'User', profile_picture = '', totp_enabled = false } = await fetchCurrentUser(); const raw = profile_picture; const picUrl = normalizePicUrl(raw) || withBase('/assets/default-avatar.png'); // 2) dark‐mode helpers const isDark = document.body.classList.contains('dark-mode'); const overlayBg = isDark ? 'rgba(0,0,0,0.7)' : 'rgba(0,0,0,0.3)'; const contentStyle = ` background: ${isDark ? '#2c2c2c' : '#fff'}; color: ${isDark ? '#e0e0e0' : '#000'}; padding: 20px; max-width: 600px; width:90%; overflow-y: auto; height: 555px; max-height: 555px; display: flex; flex-direction: column; gap: 16px; margin: 0; scrollbar-gutter: stable both-edges; border: ${isDark ? '1px solid #444' : '1px solid #ccc'}; box-sizing: border-box; scrollbar-width: none; -ms-overflow-style: none; `; // 3) create or reuse modal let modal = document.getElementById('userPanelModal'); if (!modal) { // overlay modal = document.createElement('div'); modal.id = 'userPanelModal'; Object.assign(modal.style, { position: 'fixed', top: '0', left: '0', right: '0', bottom: '0', background: overlayBg, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '16px', boxSizing: 'border-box', overflow: 'hidden', zIndex: '1000', }); // content container const content = document.createElement('div'); content.className = 'modal-content'; content.style.cssText = contentStyle; // close button const closeBtn = document.createElement('span'); closeBtn.id = 'closeUserPanel'; closeBtn.className = 'editor-close-btn'; closeBtn.textContent = '×'; closeBtn.addEventListener('click', () => modal.style.display = 'none'); content.appendChild(closeBtn); // avatar + picker const avatarWrapper = document.createElement('div'); avatarWrapper.style.cssText = 'text-align:center; margin:0;'; const avatarInner = document.createElement('div'); avatarInner.style.cssText = 'position:relative; width:80px; height:80px; margin:0 auto;'; const img = document.createElement('img'); img.id = 'profilePicPreview'; img.src = picUrl; img.alt = 'Profile Picture'; img.style.cssText = 'width:100%; height:100%; border-radius:50%; object-fit:cover;'; avatarInner.appendChild(img); const label = document.createElement('label'); label.htmlFor = 'profilePicInput'; label.style.cssText = ` position:absolute; bottom:0; right:0; width:24px; height:24px; background:rgba(0,0,0,0.6); border-radius:50%; display:flex; align-items:center; justify-content:center; cursor:pointer; `; const editIcon = document.createElement('i'); editIcon.className = 'material-icons'; editIcon.style.cssText = 'color:#fff; font-size:16px;'; editIcon.textContent = 'edit'; label.appendChild(editIcon); avatarInner.appendChild(label); const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.id = 'profilePicInput'; fileInput.accept = 'image/*'; fileInput.style.display = 'none'; avatarInner.appendChild(fileInput); avatarWrapper.appendChild(avatarInner); content.appendChild(avatarWrapper); // title const title = document.createElement('h3'); title.style.cssText = 'text-align:center; margin:0;'; const titleLabel = document.createElement('span'); titleLabel.setAttribute('data-i18n-key', 'user_panel'); titleLabel.textContent = t('user_panel'); const titleName = document.createElement('span'); titleName.id = 'userPanelUsername'; titleName.textContent = ` (${username})`; title.appendChild(titleLabel); title.appendChild(titleName); content.appendChild(title); // change password btn const pwdBtn = document.createElement('button'); pwdBtn.id = 'openChangePasswordModalBtn'; pwdBtn.className = 'btn btn-primary'; pwdBtn.setAttribute('data-i18n-key', 'change_password'); pwdBtn.textContent = t('change_password'); pwdBtn.addEventListener('click', () => { document.getElementById('changePasswordModal').style.display = 'block'; }); content.appendChild(pwdBtn); // TOTP fieldset const totpFs = document.createElement('fieldset'); totpFs.style.cssText = 'margin:0; border:0; padding:0;'; const totpLegend = document.createElement('legend'); totpLegend.style.cssText = 'margin:0 0 6px; padding:0; font-weight:600;'; totpLegend.setAttribute('data-i18n-key', 'totp_settings'); totpLegend.textContent = t('totp_settings'); totpFs.appendChild(totpLegend); const totpCb = document.createElement('input'); totpCb.type = 'checkbox'; totpCb.id = 'userTOTPEnabled'; totpCb.className = 'form-check-input fr-toggle-input'; totpCb.checked = totp_enabled; totpCb.addEventListener('change', async function () { const resp = await fetch(withBase('/api/updateUserPanel.php'), { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.csrfToken }, body: JSON.stringify({ totp_enabled: this.checked }) }); const js = await resp.json(); if (!js.success) showToast(js.error || t('error_updating_totp_setting')); else if (this.checked) openTOTPModal(); }); const totpRow = document.createElement('div'); totpRow.className = 'form-check fr-toggle'; const totpLabel = document.createElement('label'); totpLabel.className = 'form-check-label'; totpLabel.htmlFor = 'userTOTPEnabled'; totpLabel.setAttribute('data-i18n-key', 'enable_totp'); totpLabel.textContent = t('enable_totp'); totpRow.appendChild(totpCb); totpRow.appendChild(totpLabel); totpFs.appendChild(totpRow); content.appendChild(totpFs); // language fieldset const langFs = document.createElement('fieldset'); langFs.style.cssText = 'margin:0; border:0; padding:0;'; const langLegend = document.createElement('legend'); langLegend.style.cssText = 'margin:0 0 6px; padding:0; font-weight:600;'; langLegend.setAttribute('data-i18n-key', 'language'); langLegend.textContent = t('language'); langFs.appendChild(langLegend); const langSel = document.createElement('select'); langSel.id = 'languageSelector'; langSel.className = 'form-select'; const languages = [ { code: 'en', labelKey: 'english', fallback: 'English' }, { code: 'es', labelKey: 'spanish', fallback: 'Español' }, { code: 'fr', labelKey: 'french', fallback: 'Français' }, { code: 'de', labelKey: 'german', fallback: 'Deutsch' }, { code: 'pl', labelKey: 'polish', fallback: 'Polski' }, { code: 'ru', labelKey: 'russian', fallback: 'Русский' }, { code: 'ja', labelKey: 'japanese', fallback: '日本語' }, { code: 'zh-CN', labelKey: 'chinese_simplified', fallback: '简体中文' }, ]; languages.forEach(({ code, labelKey, fallback }) => { const opt = document.createElement('option'); opt.value = code; // use i18n if available, otherwise fallback opt.setAttribute('data-i18n-key', labelKey); opt.textContent = (typeof t === 'function' ? t(labelKey) : '') || fallback; langSel.appendChild(opt); }); langSel.value = localStorage.getItem('language') || 'en'; langSel.addEventListener('change', async function () { localStorage.setItem('language', this.value); const applied = await setLocale(this.value); applyTranslations(); document.documentElement.lang = applied; }); langFs.appendChild(langSel); content.appendChild(langFs); // --- Display fieldset: strip + inline folder rows --- const dispFs = document.createElement('fieldset'); dispFs.style.cssText = 'margin:0; border:0; padding:0;'; const dispLegend = document.createElement('legend'); dispLegend.style.cssText = 'margin:0 0 6px; padding:0; font-weight:600;'; dispLegend.setAttribute('data-i18n-key', 'display'); dispLegend.textContent = t('display'); dispFs.appendChild(dispLegend); // 1) Show folder strip above list const stripCb = document.createElement('input'); stripCb.type = 'checkbox'; stripCb.id = 'showFoldersInList'; stripCb.className = 'form-check-input fr-toggle-input'; { const storedStrip = localStorage.getItem('showFoldersInList'); stripCb.checked = storedStrip === null ? false : storedStrip === 'true'; } const stripRow = document.createElement('div'); stripRow.className = 'form-check fr-toggle'; stripRow.style.marginBottom = '6px'; const stripLabel = document.createElement('label'); stripLabel.className = 'form-check-label'; stripLabel.htmlFor = 'showFoldersInList'; stripLabel.setAttribute('data-i18n-key', 'show_folders_above_files'); stripLabel.textContent = t('show_folders_above_files'); stripRow.appendChild(stripCb); stripRow.appendChild(stripLabel); dispFs.appendChild(stripRow); // 2) Show inline folder rows above files in table view const inlineCb = document.createElement('input'); inlineCb.type = 'checkbox'; inlineCb.id = 'showInlineFolders'; inlineCb.className = 'form-check-input fr-toggle-input'; { const storedInline = localStorage.getItem('showInlineFolders'); inlineCb.checked = storedInline === null ? true : storedInline === 'true'; } const inlineRow = document.createElement('div'); inlineRow.className = 'form-check fr-toggle'; inlineRow.style.marginBottom = '6px'; const inlineLabel = document.createElement('label'); inlineLabel.className = 'form-check-label'; inlineLabel.htmlFor = 'showInlineFolders'; inlineLabel.setAttribute('data-i18n-key', 'show_inline_folders'); inlineLabel.textContent = t('show_inline_folders'); inlineRow.appendChild(inlineCb); inlineRow.appendChild(inlineLabel); dispFs.appendChild(inlineRow); // 3) Dual pane mode const dualCb = document.createElement('input'); dualCb.type = 'checkbox'; dualCb.id = 'dualPaneMode'; dualCb.className = 'form-check-input fr-toggle-input'; { const storedDual = localStorage.getItem('dualPaneMode'); dualCb.checked = storedDual === 'true'; } const dualRow = document.createElement('div'); dualRow.className = 'form-check fr-toggle'; dualRow.style.marginBottom = '6px'; const dualLabel = document.createElement('label'); dualLabel.className = 'form-check-label'; dualLabel.htmlFor = 'dualPaneMode'; dualLabel.setAttribute('data-i18n-key', 'dual_pane_mode'); dualLabel.textContent = t('dual_pane_mode'); dualRow.appendChild(dualCb); dualRow.appendChild(dualLabel); dispFs.appendChild(dualRow); content.appendChild(dispFs); // Handlers: toggle + refresh list stripCb.addEventListener('change', () => { window.showFoldersInList = stripCb.checked; localStorage.setItem('showFoldersInList', stripCb.checked); if (typeof window.loadFileList === 'function') { window.loadFileList(window.currentFolder || 'root'); } }); inlineCb.addEventListener('change', () => { window.showInlineFolders = inlineCb.checked; localStorage.setItem('showInlineFolders', inlineCb.checked); if (typeof window.loadFileList === 'function') { window.loadFileList(window.currentFolder || 'root'); } }); dualCb.addEventListener('change', () => { const enabled = !!dualCb.checked; localStorage.setItem('dualPaneMode', enabled ? 'true' : 'false'); window.dualPaneEnabled = enabled; if (typeof window.applyDualPaneMode === 'function') { window.applyDualPaneMode(enabled); } else { window.__frDualPanePending = enabled; } }); // 4) Disable hover preview const hoverCb = document.createElement('input'); hoverCb.type = 'checkbox'; hoverCb.id = 'disableHoverPreview'; hoverCb.className = 'form-check-input fr-toggle-input'; { const storedHover = localStorage.getItem('disableHoverPreview'); const isDisabled = storedHover === 'true'; hoverCb.checked = !isDisabled; // also mirror into a global flag for runtime checks window.disableHoverPreview = isDisabled; } const hoverRow = document.createElement('div'); hoverRow.className = 'form-check fr-toggle'; const hoverLabel = document.createElement('label'); hoverLabel.className = 'form-check-label'; hoverLabel.htmlFor = 'disableHoverPreview'; hoverLabel.setAttribute('data-i18n-key', 'show_hover_preview'); hoverLabel.textContent = t('show_hover_preview'); hoverRow.appendChild(hoverCb); hoverRow.appendChild(hoverLabel); dispFs.appendChild(hoverRow); // Handler: toggle hover preview hoverCb.addEventListener('change', () => { const disabled = !hoverCb.checked; localStorage.setItem('disableHoverPreview', disabled ? 'true' : 'false'); window.disableHoverPreview = disabled; // Hide any currently-visible preview right away const preview = document.getElementById('hoverPreview'); if (preview) { preview.style.display = 'none'; } }); // wire up image‐input change fileInput.addEventListener('change', async function () { const f = this.files[0]; if (!f) return; const allowedTypes = ['image/jpeg', 'image/png', 'image/gif']; if (!allowedTypes.includes(f.type)) { showToast(t('error_updating_picture')); return; } // preview immediately const blobUrl = URL.createObjectURL(f); if (typeof blobUrl !== 'string' || !blobUrl.startsWith('blob:')) { showToast(t('error_updating_picture')); return; } img.src = blobUrl; img.addEventListener('load', () => URL.revokeObjectURL(blobUrl), { once: true }); // upload const fd = new FormData(); fd.append('profile_picture', f); try { const res = await fetch(withBase('/api/profile/uploadPicture.php'), { method: 'POST', credentials: 'include', headers: { 'X-CSRF-Token': window.csrfToken }, body: fd }); const text = await res.text(); const js = JSON.parse(text || '{}'); if (!res.ok) { showToast(js.error || t('error_updating_picture')); return; } const newUrl = normalizePicUrl(js.url); img.src = newUrl; localStorage.setItem('profilePicUrl', newUrl); updateAuthenticatedUI(window.__lastAuthData || {}); showToast(t('profile_picture_updated')); } catch (e) { console.error(e); showToast(t('error_updating_picture')); } }); // finalize modal.appendChild(content); document.body.appendChild(modal); } else { // reuse on reopen Object.assign(modal.style, { background: overlayBg, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '16px', boxSizing: 'border-box', overflow: 'hidden' }); const content = modal.querySelector('.modal-content'); content.style.cssText = contentStyle; modal.querySelector('#profilePicPreview').src = picUrl || withBase('/assets/default-avatar.png'); modal.querySelector('#userTOTPEnabled').checked = totp_enabled; modal.querySelector('#languageSelector').value = localStorage.getItem('language') || 'en'; const titleName = modal.querySelector('#userPanelUsername'); if (titleName) titleName.textContent = ` (${username})`; // sync display toggles from localStorage const stripCb = modal.querySelector('#showFoldersInList'); const inlineCb = modal.querySelector('#showInlineFolders'); const dualCb = modal.querySelector('#dualPaneMode'); if (stripCb) { const storedStrip = localStorage.getItem('showFoldersInList'); stripCb.checked = storedStrip === null ? false : storedStrip === 'true'; } if (inlineCb) { const storedInline = localStorage.getItem('showInlineFolders'); inlineCb.checked = storedInline === null ? true : storedInline === 'true'; } if (dualCb) { const storedDual = localStorage.getItem('dualPaneMode'); dualCb.checked = storedDual === 'true'; } } const hoverCb = modal.querySelector('#disableHoverPreview'); if (hoverCb) { const storedHover = localStorage.getItem('disableHoverPreview'); const isDisabled = storedHover === 'true'; hoverCb.checked = !isDisabled; window.disableHoverPreview = isDisabled; } // show modal.style.display = 'flex'; }