diff --git a/CHANGELOG.md b/CHANGELOG.md index a1b7af8..2d41d97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,84 @@ # Changelog +## Changes 5/14/2025 v1.3.4 + +### 1. Button Grouping (Bootstrap) + +- Converted individual action buttons (`download`, `edit`, `rename`, `share`) in both **table view** and **gallery view** into a single Bootstrap button group for a cleaner, more compact UI. +- Applied `btn-group` and `btn-sm` classes for consistent sizing and spacing. + +### 2. Header Dropdown Replacement + +- Replaced the standalone “User Panel” icon button with a **dropdown wrapper** (`.user-dropdown`) in the header. +- Dropdown toggle now shows: + - **Profile picture** (if set) or the Material “account_circle” icon + - **Username** text (between avatar and caret) + - Down-arrow caret span. + +### 3. Menu Items Moved to Dropdown + +- Moved previously standalone header buttons into the dropdown menu: + - **User Panel** opens the modal + - **Admin Panel** only shown when `data.isAdmin` *and* on `demo.filerise.net` + - **API Docs** calls `openApiModal()` + - **Logout** calls `triggerLogout()` +- Each menu item now has a matching Material icon (e.g. `person`, `admin_panel_settings`, `description`, `logout`). + +### 4. Profile Picture Support + +- Added a new `/api/profile/uploadPicture.php` endpoint + `UserController::uploadPicture()` + corresponding `UserModel::setProfilePicture()`. +- On **Open User Panel**, display: + - Default avatar if none set + - Current profile picture if available +- In the **User Panel** modal: + - Stylish “edit” overlay icon on the avatar to launch file picker + - Auto-upload on file selection (no “Save” button click needed) + - Preview updates immediately and header avatar refreshes live + - Persisted in `users.txt` and re-fetched via `getCurrentUser.php` + +### 5. API Docs & Logout Relocation + +- Removed API Docs from User Panel +- Removed “Logout” buttons from the header toolbar. +- Both are now menu entries in the **User Dropdown**. + +### 6. Admin Panel Conditional + +- The **Admin Panel** button was: + - Kept in the dropdown only when `data.isAdmin` + - Removed entirely elsewhere. + +### 7. Utility & Styling Tweaks + +- Introduced a small `normalizePicUrl()` helper to strip stray colons and ensure a leading slash. +- Hidden the scrollbar in the User Panel modal via: + - Inline CSS (`scrollbar-width: none; -ms-overflow-style: none;`) + - Global/WebKit rule for `::-webkit-scrollbar { display: none; }` +- Made the User Panel modal fully responsive and vertically centered, with smooth dark-mode support. + +### 8. File/List View & Gallery View Sliders + +- **Unified “View‐Mode” Slider** + Added a single slider panel (`#viewSliderContainer`) in the file‐list actions toolbar that switches behavior based on the current view mode: + - **Table View**: shows a **Row Height** slider (min 31px, max 60px). + - Adjusts the CSS variable `--file-row-height` to resize all `` heights. + - Persists the chosen height in `localStorage`. + - **Gallery View**: shows a **Columns** slider (min 1, max 6). + - Updates the grid’s `grid-template-columns: repeat(N, 1fr)`. + - Persists the chosen column count in `localStorage`. + +- **Injection Point** + The slider container is dynamically inserted (or updated) just before the folder summary (`#fileSummary`) in `loadFileList()`, ensuring a consistent position across both view modes. + +- **Live Updates** + Moving the slider thumb immediately updates the visible table row heights or gallery column layout without a full re‐render. + +- **Styling & Alignment** + - `#viewSliderContainer` uses `inline-flex` and `align-items: center` so that label, slider, and value text are vertically aligned with the other toolbar elements. + - Reset margins/padding on the label and value span within `#viewSliderContainer` to eliminate any vertical misalignment. + +--- + ## Changes 5/8/2025 ### Docker 🐳 diff --git a/public/api/profile/getCurrentUser.php b/public/api/profile/getCurrentUser.php new file mode 100644 index 0000000..c60fd6c --- /dev/null +++ b/public/api/profile/getCurrentUser.php @@ -0,0 +1,15 @@ +'Unauthorized']); + exit; +} + +$user = $_SESSION['username']; +$data = UserModel::getUser($user); +echo json_encode($data); \ No newline at end of file diff --git a/public/api/profile/uploadPicture.php b/public/api/profile/uploadPicture.php new file mode 100644 index 0000000..7ecee04 --- /dev/null +++ b/public/api/profile/uploadPicture.php @@ -0,0 +1,17 @@ +uploadPicture(); +} catch (\Throwable $e) { + http_response_code(500); + echo json_encode([ + 'success' => false, + 'error' => 'Exception: ' . $e->getMessage() + ]); +} \ No newline at end of file diff --git a/public/assets/default-avatar.png b/public/assets/default-avatar.png new file mode 100644 index 0000000..5e68e60 Binary files /dev/null and b/public/assets/default-avatar.png differ diff --git a/public/css/styles.css b/public/css/styles.css index 86b2e4b..20130d1 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -134,17 +134,27 @@ body.dark-mode header { background: none; border: none; cursor: pointer; - padding: 9px; - border-radius: 50%; color: #fff; transition: background-color 0.2s ease, box-shadow 0.2s ease; } +.header-buttons button:not(#userDropdownToggle) { + border-radius: 50%; + padding: 9px; +} + +#userDropdownToggle { + border-radius: 4px !important; + padding: 6px 10px !important; +} + .header-buttons button:hover { background-color: rgba(255, 255, 255, 0.2); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); + color: #fff; } + @media (max-width: 600px) { header { flex-direction: column; @@ -955,6 +965,23 @@ body.dark-mode #fileList table tr { padding: 8px 10px !important; } +:root { + --file-row-height: 48px; /* default, will be overwritten by your slider */ +} + +/* Force each to be exactly the var() height */ +#fileList table.table tbody tr { + height: var(--file-row-height) !important; +} + +/* And force each to match, with no extra padding or line-height */ +#fileList table.table tbody td { + height: var(--file-row-height) !important; + line-height: var(--file-row-height) !important; + padding-top: 0 !important; + padding-bottom: 0 !important; + vertical-align: middle; +} /* =========================================================== HEADINGS & FORM LABELS @@ -1328,26 +1355,6 @@ body.dark-mode .image-preview-modal-content { border-color: #444; } -.preview-btn, -.download-btn, -.rename-btn, -.share-btn, -.edit-btn { - display: flex; - align-items: center; - padding: 8px 12px; - justify-content: center; -} - -.share-btn { - border: none; - color: white; - padding: 8px 12px; - cursor: pointer; - margin-left: 0px; - transition: background 0.3s; -} - .image-modal-img { max-width: 100%; max-height: 80vh; @@ -2102,13 +2109,23 @@ body.dark-mode .header-drop-zone.drag-active { color: black; } @media only screen and (max-width: 600px) { - #fileSummary { - float: none !important; - margin: 0 auto !important; - text-align: center !important; + #fileSummary, + #rowHeightSliderContainer, + #viewSliderContainer { + float: none !important; + margin: 0 auto !important; + text-align: center !important; + display: block !important; } } +#viewSliderContainer label, +#viewSliderContainer span { + line-height: 1; + margin: 0; + padding: 0; +} + body.dark-mode #fileSummary { color: white; } @@ -2165,4 +2182,64 @@ body.dark-mode #searchIcon .material-icons { body.dark-mode .btn-icon:hover, body.dark-mode .btn-icon:focus { background: rgba(255, 255, 255, 0.1); +} + +.user-dropdown { + position: relative; + display: inline-block; +} + +.user-dropdown .user-menu { + display: none; + position: absolute; + right: 0; + margin-top: 0.25rem; + background: var(--bs-body-bg, #fff); + border: 1px solid #ccc; + border-radius: 4px; + min-width: 150px; + box-shadow: 0 2px 6px rgba(0,0,0,0.2); + z-index: 1000; +} + +.user-dropdown .user-menu.show { + display: block; +} + +.user-dropdown .user-menu .item { + padding: 0.5rem 0.75rem; + cursor: pointer; + white-space: nowrap; +} +.user-dropdown .user-menu .item:hover { + background: #f5f5f5; +} + +.user-dropdown .dropdown-caret { + border-top: 5px solid currentColor; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + display: inline-block; + vertical-align: middle; + margin-left: 0.25rem; +} + +body.dark-mode .user-dropdown .user-menu { + background: #2c2c2c; + border-color: #444; +} + +body.dark-mode .user-dropdown .user-menu .item { + color: #e0e0e0; +} + +body.dark-mode .user-dropdown .user-menu .item:hover { + background: rgba(255,255,255,0.1); +} + +.user-dropdown .dropdown-username { + margin: 0 8px; + font-weight: 500; + vertical-align: middle; + white-space: nowrap; } \ No newline at end of file diff --git a/public/index.html b/public/index.html index 1335911..6655316 100644 --- a/public/index.html +++ b/public/index.html @@ -135,9 +135,6 @@
- diff --git a/public/js/adminPanel.js b/public/js/adminPanel.js index 12b1804..c86ae7a 100644 --- a/public/js/adminPanel.js +++ b/public/js/adminPanel.js @@ -3,7 +3,7 @@ import { loadAdminConfigFunc } from './auth.js'; import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js'; import { sendRequest } from './networkUtils.js'; -const version = "v1.3.3"; +const version = "v1.3.4"; const adminTitle = `${t("admin_panel")} ${version}`; // ————— Inject updated styles ————— diff --git a/public/js/auth.js b/public/js/auth.js index b505f32..2ad7f6b 100644 --- a/public/js/auth.js +++ b/public/js/auth.js @@ -15,10 +15,11 @@ import { openUserPanel, openTOTPModal, closeTOTPModal, - setLastLoginData + setLastLoginData, + openApiModal } from './authModals.js'; import { openAdminPanel } from './adminPanel.js'; -import { initializeApp } from './main.js'; +import { initializeApp, triggerLogout } from './main.js'; // Production OIDC configuration (override via API as needed) const currentOIDCConfig = { @@ -154,7 +155,7 @@ function updateLoginOptionsUIFromStorage() { disableFormLogin: localStorage.getItem("disableFormLogin") === "true", disableBasicAuth: localStorage.getItem("disableBasicAuth") === "true", disableOIDCLogin: localStorage.getItem("disableOIDCLogin") === "true", - authBypass: localStorage.getItem("authBypass") === "true" + authBypass: localStorage.getItem("authBypass") === "true" }); } @@ -199,21 +200,45 @@ function insertAfter(newNode, referenceNode) { referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); } -function updateAuthenticatedUI(data) { - document.getElementById('loadingOverlay').remove(); +async function fetchProfilePicture() { + try { + const res = await fetch('/api/profile/getCurrentUser.php', { + credentials: 'include' + }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const info = await res.json(); + let pic = info.profile_picture || ''; + // --- take only what's after the *last* colon --- + const parts = pic.split(':'); + pic = parts[parts.length - 1] || ''; + // strip any stray leading colons + pic = pic.replace(/^:+/, ''); + // ensure exactly one leading slash + if (pic && !pic.startsWith('/')) pic = '/' + pic; + return pic; + } catch (e) { + console.warn('fetchProfilePicture failed:', e); + return ''; + } +} - // show the wrapper (so the login form can be visible) - document.querySelector('.main-wrapper').style.display = ''; - document.getElementById('loginForm').style.display = 'none'; +export async function updateAuthenticatedUI(data) { + // 1) Remove loading overlay safely + const loading = document.getElementById('loadingOverlay'); + if (loading) loading.remove(); + + // 2) Show main UI + document.querySelector('.main-wrapper').style.display = ''; + document.getElementById('loginForm').style.display = 'none'; toggleVisibility("loginForm", false); toggleVisibility("mainOperations", true); toggleVisibility("uploadFileForm", true); toggleVisibility("fileListContainer", true); - //attachEnterKeyListener("addUserModal", "saveUserBtn"); - attachEnterKeyListener("removeUserModal", "deleteUserBtn"); - attachEnterKeyListener("changePasswordModal", "saveNewPasswordBtn"); + attachEnterKeyListener("removeUserModal", "deleteUserBtn"); + attachEnterKeyListener("changePasswordModal","saveNewPasswordBtn"); document.querySelector(".header-buttons").style.visibility = "visible"; + // 3) Persist auth flags (unchanged) if (typeof data.totp_enabled !== "undefined") { localStorage.setItem("userTOTPEnabled", data.totp_enabled ? "true" : "false"); } @@ -221,64 +246,143 @@ function updateAuthenticatedUI(data) { localStorage.setItem("username", data.username); } if (typeof data.folderOnly !== "undefined") { - localStorage.setItem("folderOnly", data.folderOnly ? "true" : "false"); - localStorage.setItem("readOnly", data.readOnly ? "true" : "false"); - localStorage.setItem("disableUpload", data.disableUpload ? "true" : "false"); + localStorage.setItem("folderOnly", data.folderOnly ? "true" : "false"); + localStorage.setItem("readOnly", data.readOnly ? "true" : "false"); + localStorage.setItem("disableUpload",data.disableUpload? "true" : "false"); } + // 4) Fetch up-to-date profile picture — ALWAYS overwrite localStorage + const profilePicUrl = await fetchProfilePicture(); + localStorage.setItem("profilePicUrl", profilePicUrl); + + // 5) Build / update header buttons const headerButtons = document.querySelector(".header-buttons"); - const firstButton = headerButtons.firstElementChild; + const firstButton = headerButtons.firstElementChild; + // a) restore-from-trash for admins if (data.isAdmin) { - let restoreBtn = document.getElementById("restoreFilesBtn"); - if (!restoreBtn) { - restoreBtn = document.createElement("button"); - restoreBtn.id = "restoreFilesBtn"; - restoreBtn.classList.add("btn", "btn-warning"); - restoreBtn.setAttribute("data-i18n-title", "trash_restore_delete"); - restoreBtn.innerHTML = 'restore_from_trash'; - if (firstButton) insertAfter(restoreBtn, firstButton); - else headerButtons.appendChild(restoreBtn); - } - restoreBtn.style.display = "block"; - - let adminPanelBtn = document.getElementById("adminPanelBtn"); - if (!adminPanelBtn) { - adminPanelBtn = document.createElement("button"); - adminPanelBtn.id = "adminPanelBtn"; - adminPanelBtn.classList.add("btn", "btn-info"); - adminPanelBtn.setAttribute("data-i18n-title", "admin_panel"); - adminPanelBtn.innerHTML = 'admin_panel_settings'; - insertAfter(adminPanelBtn, restoreBtn); - adminPanelBtn.addEventListener("click", openAdminPanel); - } else { - adminPanelBtn.style.display = "block"; + let r = document.getElementById("restoreFilesBtn"); + if (!r) { + r = document.createElement("button"); + r.id = "restoreFilesBtn"; + r.classList.add("btn","btn-warning"); + r.setAttribute("data-i18n-title","trash_restore_delete"); + r.innerHTML = 'restore_from_trash'; + if (firstButton) insertAfter(r, firstButton); + else headerButtons.appendChild(r); } + r.style.display = "block"; } else { - const restoreBtn = document.getElementById("restoreFilesBtn"); - if (restoreBtn) restoreBtn.style.display = "none"; - const adminPanelBtn = document.getElementById("adminPanelBtn"); - if (adminPanelBtn) adminPanelBtn.style.display = "none"; + const r = document.getElementById("restoreFilesBtn"); + if (r) r.style.display = "none"; } - if (window.location.hostname !== "demo.filerise.net") { - let userPanelBtn = document.getElementById("userPanelBtn"); - if (!userPanelBtn) { - userPanelBtn = document.createElement("button"); - userPanelBtn.id = "userPanelBtn"; - userPanelBtn.classList.add("btn", "btn-user"); - userPanelBtn.setAttribute("data-i18n-title", "user_panel"); - userPanelBtn.innerHTML = 'account_circle'; + // b) admin panel button only on demo.filerise.net + if (data.isAdmin && window.location.hostname === "demo.filerise.net") { + let a = document.getElementById("adminPanelBtn"); + if (!a) { + a = document.createElement("button"); + a.id = "adminPanelBtn"; + a.classList.add("btn","btn-info"); + a.setAttribute("data-i18n-title","admin_panel"); + a.innerHTML = 'admin_panel_settings'; + insertAfter(a, document.getElementById("restoreFilesBtn")); + a.addEventListener("click", openAdminPanel); + } + a.style.display = "block"; + } else { + const a = document.getElementById("adminPanelBtn"); + if (a) a.style.display = "none"; + } + + // c) user dropdown on non-demo + if (window.location.hostname !== "demo.filerise.net") { + let dd = document.getElementById("userDropdown"); + + // choose icon *or* img + const avatarHTML = profilePicUrl + ? `` + : `account_circle`; + + if (!dd) { + dd = document.createElement("div"); + dd.id = "userDropdown"; + dd.classList.add("user-dropdown"); + + // toggle button + const toggle = document.createElement("button"); + toggle.id = "userDropdownToggle"; + toggle.classList.add("btn","btn-user"); + toggle.setAttribute("title", t("user_settings")); + toggle.innerHTML = `${avatarHTML}${data.username}`; + dd.append(toggle); + + // menu + const menu = document.createElement("div"); + menu.classList.add("user-menu"); + menu.innerHTML = ` + + ${data.isAdmin ? ` + ` : ''} + + + `; + dd.append(menu); + + // insert + const dm = document.getElementById("darkModeToggle"); + if (dm) insertAfter(dd, dm); + else if (firstButton) insertAfter(dd, firstButton); + else headerButtons.appendChild(dd); + + // open/close + toggle.addEventListener("click", e => { + e.stopPropagation(); + menu.classList.toggle("show"); + }); + document.addEventListener("click", () => menu.classList.remove("show")); + + // actions + document.getElementById("menuUserPanel") + .addEventListener("click", () => { + menu.classList.remove("show"); + openUserPanel(); + }); + if (data.isAdmin) { + document.getElementById("menuAdminPanel") + .addEventListener("click", () => { + menu.classList.remove("show"); + openAdminPanel(); + }); + } + document.getElementById("menuApiDocs") + .addEventListener("click", () => { + menu.classList.remove("show"); + openApiModal(); + }); + document.getElementById("menuLogout") + .addEventListener("click", () => { + menu.classList.remove("show"); + triggerLogout(); + }); - const adminBtn = document.getElementById("adminPanelBtn"); - if (adminBtn) insertAfter(userPanelBtn, adminBtn); - else if (firstButton) insertAfter(userPanelBtn, firstButton); - else headerButtons.appendChild(userPanelBtn); - userPanelBtn.addEventListener("click", openUserPanel); } else { - userPanelBtn.style.display = "block"; + // update avatar only + const tog = dd.querySelector("#userDropdownToggle"); + tog.innerHTML = `${avatarHTML}${data.username}`; + dd.style.display = "inline-block"; } } + + // 6) Finalize initializeApp(); applyTranslations(); updateItemsPerPageSelect(); @@ -289,7 +393,8 @@ function checkAuthentication(showLoginToast = true) { return sendRequest("/api/auth/checkAuth.php") .then(data => { if (data.setup) { - document.getElementById('loadingOverlay').remove(); + const overlay = document.getElementById('loadingOverlay'); + if (overlay) overlay.remove(); // show the wrapper (so the login form can be visible) document.querySelector('.main-wrapper').style.display = ''; @@ -322,13 +427,14 @@ function checkAuthentication(showLoginToast = true) { updateAuthenticatedUI(data); return data; } else { - document.getElementById('loadingOverlay').remove(); + const overlay = document.getElementById('loadingOverlay'); + if (overlay) overlay.remove(); // show the wrapper (so the login form can be visible) document.querySelector('.main-wrapper').style.display = ''; document.getElementById('loginForm').style.display = ''; if (showLoginToast) showToast("Please log in to continue."); - toggleVisibility("loginForm", ! (localStorage.getItem("authBypass")==="true")); + toggleVisibility("loginForm", !(localStorage.getItem("authBypass") === "true")); toggleVisibility("mainOperations", false); toggleVisibility("uploadFileForm", false); toggleVisibility("fileListContainer", false); diff --git a/public/js/authModals.js b/public/js/authModals.js index c862f04..642ba36 100644 --- a/public/js/authModals.js +++ b/public/js/authModals.js @@ -1,8 +1,7 @@ import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js'; import { sendRequest } from './networkUtils.js'; import { t, applyTranslations, setLocale } from './i18n.js'; -import { loadAdminConfigFunc } from './auth.js'; - +import { loadAdminConfigFunc, updateAuthenticatedUI } from './auth.js'; let lastLoginData = null; export function setLastLoginData(data) { @@ -60,14 +59,11 @@ export function openTOTPLoginModal() { const totpSection = document.getElementById("totpSection"); const recoverySection = document.getElementById("recoverySection"); const toggleLink = this; - if (recoverySection.style.display === "none") { - // Switch to recovery totpSection.style.display = "none"; recoverySection.style.display = "block"; toggleLink.textContent = t("use_totp_code_instead"); } else { - // Switch back to TOTP recoverySection.style.display = "none"; totpSection.style.display = "block"; toggleLink.textContent = t("use_recovery_code_instead"); @@ -93,7 +89,6 @@ export function openTOTPLoginModal() { .then(res => res.json()) .then(json => { if (json.status === "ok") { - // recovery succeeded → finalize login window.location.href = "/index.html"; } else { showToast(json.message || t("recovery_code_verification_failed")); @@ -107,17 +102,11 @@ export function openTOTPLoginModal() { // TOTP submission const totpInput = document.getElementById("totpLoginInput"); totpInput.focus(); - totpInput.addEventListener("input", async function () { const code = this.value.trim(); - if (code.length !== 6) { + if (code.length !== 6) return; - return; - } - - const tokenRes = await fetch("/api/auth/token.php", { - credentials: "include" - }); + const tokenRes = await fetch("/api/auth/token.php", { credentials: "include" }); if (!tokenRes.ok) { showToast(t("totp_verification_failed")); return; @@ -144,7 +133,6 @@ export function openTOTPLoginModal() { } else { showToast(t("totp_verification_failed")); } - this.value = ""; totpLoginModal.style.display = "flex"; this.focus(); @@ -160,153 +148,209 @@ export function openTOTPLoginModal() { } } -export function openUserPanel() { - const username = localStorage.getItem("username") || "User"; - let userPanelModal = document.getElementById("userPanelModal"); - const isDarkMode = document.body.classList.contains("dark-mode"); - const overlayBackground = isDarkMode ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.3)"; - const modalContentStyles = ` - background: ${isDarkMode ? "#2c2c2c" : "#fff"}; - color: ${isDarkMode ? "#e0e0e0" : "#000"}; - padding: 20px; - max-width: 600px; - width: 90%; - border-radius: 8px; - overflow-y: auto; - overflow-x: hidden; - max-height: 383px !important; - flex-shrink: 0 !important; - scrollbar-gutter: stable both-edges; - border: ${isDarkMode ? "1px solid #444" : "1px solid #ccc"}; - box-sizing: border-box; - transition: none; - `; - const savedLanguage = localStorage.getItem("language") || "en"; +/** + * Fetch current user info (username, profile_picture, totp_enabled) + */ +async function fetchCurrentUser() { + try { + const res = await fetch('/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 {}; + } +} - if (!userPanelModal) { - userPanelModal = document.createElement("div"); - userPanelModal.id = "userPanelModal"; - userPanelModal.style.cssText = ` - position: fixed; - top: 0; right: 0; bottom: 0; left: 0; - background-color: ${overlayBackground}; - display: flex; - justify-content: center; - align-items: center; - z-index: 1000; - overflow: hidden; +/** + * 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 leading slash + if (pic && !pic.startsWith('/')) pic = '/' + pic; + return 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); + + // 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 contentCss = ` + background: ${isDark ? '#2c2c2c' : '#fff'}; + color: ${isDark ? '#e0e0e0' : '#000'}; + padding: 20px; + max-width: 600px; + width: 90%; + border-radius: 8px; + overflow-y: auto; + max-height: 415px; + border: ${isDark ? '1px solid #444' : '1px solid #ccc'}; + box-sizing: border-box; + + /* hide scrollbar in Firefox */ + scrollbar-width: none; + /* hide scrollbar in IE 10+ */ + -ms-overflow-style: none; +`; + + // 3) build or re-use modal + let modal = document.getElementById('userPanelModal'); + if (!modal) { + modal = document.createElement('div'); + modal.id = 'userPanelModal'; + modal.style.cssText = ` + position:fixed; top:0; left:0; right:0; bottom:0; + background:${overlayBg}; + display:flex; align-items:center; justify-content:center; + z-index:1000; `; - userPanelModal.innerHTML = ` - diff --git a/public/js/folderManager.js b/public/js/folderManager.js index ad377c2..cca3409 100644 --- a/public/js/folderManager.js +++ b/public/js/folderManager.js @@ -236,7 +236,8 @@ function renderFolderTree(tree, parentPath = "", defaultDisplay = "block") { const state = loadFolderTreeState(); let html = `