Files
FileRise/public/js/userPanel.js
T
Ryan 783622f8d7 release(v3.1.0): default language + portal i18n + ffmpeg path config (closes #88, closes #89)
- Admin: add “Default language” setting (used when a user has not chosen a language yet)
- Admin: add optional FFmpeg binary path setting (env FR_FFMPEG_PATH overrides / locks)
- Thumbnails: resolve ffmpeg path from env first, then admin config, then PATH
- Portals: add language selector + apply defaultLanguage from siteConfig on first run
- Portals: fix button styling (accent-aware) + refresh button color (fixes #88)
- i18n: split locales into separate files and add Polish/Russian/Japanese; refresh German strings (fixes #89)
- UI: replace hardcoded upload/login alerts/toasts with i18n keys for better translation coverage

Closes #88
Closes #89
2026-01-16 03:26:46 -05:00

478 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 profilepicture 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) darkmode 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 imageinput 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';
}