From 67054bb61a8e8e76ec2d9ebf2cdb133ed9b1e52a Mon Sep 17 00:00:00 2001 From: Ryan Date: Sat, 17 Jan 2026 23:13:02 -0500 Subject: [PATCH] release(v3.1.1): OIDC env overrides + configurable resumable chunk size + clearer startup logs (closes #86, closes #87, closes #90) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - config: allow env overrides for OIDC knobs (auto-create, group claim, admin group, Pro group prefix) - uploads: add configurable Resumable.js chunk size (Admin + siteConfig) and honor it in upload.js - uploads: improve relative-path folder uploads and remote staging/cleanup for non-local sources - admin: add settings search + smoother section open/close animations - admin: restrict Pro license actions to the registered/primary admin user - remote storage: add FR_REMOTE_DIR_MARKER to preserve empty dirs; skip Trash on Google Drive sources - UX: clearer “FileRise startup complete” log line + better long-running delete/restore/loading feedback Closes #86 Closes #87 Closes #90 --- CHANGELOG.md | 62 +++++ config/config.php | 26 +- public/css/styles.css | 223 +++++++++++++++- public/js/adminPanel.js | 370 +++++++++++++++++++++++---- public/js/auth.js | 22 +- public/js/fileActions.js | 38 +++ public/js/fileListView.js | 25 +- public/js/folderManager.js | 59 +++++ public/js/main.js | 13 +- public/js/trashRestoreDelete.js | 40 ++- public/js/upload.js | 184 ++++++++++--- src/controllers/AdminController.php | 62 ++++- src/controllers/UploadController.php | 2 +- src/models/AdminModel.php | 34 +++ src/models/FileModel.php | 194 ++++++++++---- src/models/FolderModel.php | 19 +- src/models/UploadModel.php | 268 ++++++++++++++----- src/models/UserModel.php | 34 +++ start.sh | 3 +- 19 files changed, 1457 insertions(+), 221 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c22ed01..35f2882 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,67 @@ # Changelog +## Changes 01/17/2026 (v3.1.1) + +`release(v3.1.1): OIDC env overrides + configurable resumable chunk size + clearer startup logs (closes #86, closes #87, closes #90)` + +**Commit message** + +```text +release(v3.1.1): OIDC env overrides + configurable resumable chunk size + clearer startup logs (closes #86, closes #87, closes #90) + +- config: allow env overrides for OIDC knobs (auto-create, group claim, admin group, Pro group prefix) +- uploads: add configurable Resumable.js chunk size (Admin + siteConfig) and honor it in upload.js +- uploads: improve relative-path folder uploads and remote staging/cleanup for non-local sources +- admin: add settings search + smoother section open/close animations +- admin: restrict Pro license actions to the registered/primary admin user +- remote storage: add FR_REMOTE_DIR_MARKER to preserve empty dirs; skip Trash on Google Drive sources +- UX: clearer “FileRise startup complete” log line + better long-running delete/restore/loading feedback + +Closes #86 +Closes #87 +Closes #90 +``` + +**Added** + +- **OIDC env overrides** (in addition to config defaults): + `FR_OIDC_AUTO_CREATE`, `FR_OIDC_GROUP_CLAIM`, `FR_OIDC_ADMIN_GROUP`, `FR_OIDC_PRO_GROUP_PREFIX`. +- **Upload tuning (Admin):** “Resumable chunk size (MB)” (0.5–100 MB). + Exported via siteConfig so the frontend can size chunks dynamically. +- **Remote folder marker:** `FR_REMOTE_DIR_MARKER` (default: `.filerise_keep`) to preserve empty remote folders (S3-style prefix backends). +- **Admin settings search:** quick filter for sections/settings in the Admin panel UI. + +**Changed** + +- **Resumable uploads honor configured chunk size** (used by file picker + drag/drop when Resumable is available). +- **Upload handling for folder paths**: + - validates and sanitizes `resumableRelativePath` / `relativePath` + - supports subfolder uploads more consistently + - remote sources stage chunks in meta root (`uploadtmp/`) and push via adapter, then cleanup temp folders +- **Admin Pro license visibility/actions** are restricted to the **primary/registered admin** (first admin in `users.txt` order). +- **Remote deletes / Trash behavior**: + - Google Drive sources skip Trash (deletes are permanent) + - remote folder “empty checks” ignore the marker file +- **Docker startup log clarity**: + - `start.sh` prints a “startup complete” line and clarifies that further output is Apache logs. + +**Fixed** + +- **#86:** OIDC behavior is now controllable via environment variables (no code/config edits required). +- **#87:** Resumable chunk size is now configurable to fit proxy limits (e.g., tunnels/CDNs). +- **#90:** Clearer startup output + better guidance for collecting logs. +- **UI responsiveness / long operations** + - “Deleting…” busy states for file/folder delete confirmations and Trash restore/delete actions + - “Still loading…” toast for slow remote listings, with a fallback if a folder no longer exists + +**Notes** + +- `FR_REMOTE_DIR_MARKER` is best-effort and primarily intended for remote backends that treat directories as prefixes (e.g., S3). +- Google Drive sources do not support Trash semantics in the adapter; the UI notes this and deletes are permanent. +- Some Admin Panel strings still fall back to English; translations will continue to improve over time. + +--- + ## Changes 01/16/2026 (v3.1.0) `release(v3.1.0): default language + portal i18n + ffmpeg path config (closes #88, closes #89)` diff --git a/config/config.php b/config/config.php index f694725..893d597 100644 --- a/config/config.php +++ b/config/config.php @@ -45,6 +45,8 @@ if (!defined('DEFAULT_CAN_ZIP')) define('DEFAULT_CAN_ZIP', true); if (!defined('DEFAULT_VIEW_OWN_ONLY')) define('DEFAULT_VIEW_OWN_ONLY', false); define('FOLDER_OWNERS_FILE', META_DIR . 'folder_owners.json'); define('ACL_INHERIT_ON_CREATE', true); +// Hidden file used to preserve empty remote folders (S3 prefixes, etc.). +if (!defined('FR_REMOTE_DIR_MARKER')) define('FR_REMOTE_DIR_MARKER', '.filerise_keep'); // ONLYOFFICE integration overrides (uncomment and set as needed) /* define('ONLYOFFICE_ENABLED', false); @@ -63,23 +65,39 @@ if (!defined('OIDC_TOKEN_ENDPOINT_AUTH_METHOD')) { // Auto-create users from OIDC when no users.txt match. if (!defined('FR_OIDC_AUTO_CREATE')) { - define('FR_OIDC_AUTO_CREATE', true); + $envVal = getenv('FR_OIDC_AUTO_CREATE'); + if ($envVal !== false && $envVal !== '') { + $val = strtolower(trim((string)$envVal)); + define('FR_OIDC_AUTO_CREATE', in_array($val, ['1', 'true', 'yes', 'on'], true)); + } else { + define('FR_OIDC_AUTO_CREATE', true); + } } // Claim that contains IdP groups/roles (typical: "groups" or "roles"). if (!defined('FR_OIDC_GROUP_CLAIM')) { - define('FR_OIDC_GROUP_CLAIM', 'groups'); + $envVal = getenv('FR_OIDC_GROUP_CLAIM'); + define( + 'FR_OIDC_GROUP_CLAIM', + ($envVal !== false && trim((string)$envVal) !== '') ? trim((string)$envVal) : 'groups' + ); } // Name of an IdP group that should be treated as "FileRise admin". if (!defined('FR_OIDC_ADMIN_GROUP')) { - define('FR_OIDC_ADMIN_GROUP', 'filerise-admins'); + $envVal = getenv('FR_OIDC_ADMIN_GROUP'); + if ($envVal !== false) { + define('FR_OIDC_ADMIN_GROUP', trim((string)$envVal)); + } else { + define('FR_OIDC_ADMIN_GROUP', 'filerise-admins'); + } } // Prefix for IdP groups that should map into FileRise Pro groups. // Example: IdP group "frp_clients_acme" → Pro group "clients_acme". if (!defined('FR_OIDC_PRO_GROUP_PREFIX')) { - define('FR_OIDC_PRO_GROUP_PREFIX', ''); + $envVal = getenv('FR_OIDC_PRO_GROUP_PREFIX'); + define('FR_OIDC_PRO_GROUP_PREFIX', ($envVal !== false) ? trim((string)$envVal) : ''); } // Optional env/constant override: if set, it wins; if not set, UI setting is used. if (!defined('FR_OIDC_ALLOW_DEMOTE')) { diff --git a/public/css/styles.css b/public/css/styles.css index 7ab2ab4..0ff146c 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -441,6 +441,16 @@ body.dark-mode #restoreFilesModal .restore-empty{color: var(--fr-muted-dark,#aaa font-weight:600; box-shadow:0 1px 2px rgba(0,0,0,.08); transition: background-color var(--filr-transition-fast,150ms ease-out), transform var(--filr-transition-fast,150ms ease-out), box-shadow var(--filr-transition-fast,150ms ease-out);} +#restoreFilesModal .restore-btn[aria-busy="true"]{cursor: wait;} +#restoreFilesModal .restore-btn[aria-busy="true"]::after{content:""; + width:14px; height:14px; + border-radius:50%; + border:2px solid currentColor; + border-right-color: transparent; + display:inline-block; + margin-left:4px; + animation: filr-spin .8s linear infinite;} +#restoreFilesModal .restore-btn[aria-busy="true"] .material-icons{opacity:0.6;} #restoreFilesModal .restore-btn .material-icons{font-size:20px;} #restoreFilesModal .restore-btn:hover{transform: translateY(-1px); background: #ffffff; box-shadow:0 6px 14px rgba(0,0,0,.12);} body.dark-mode #restoreFilesModal .restore-btn,body.dark-mode #deleteTrashSelectedBtn{background: var(--fr-surface-dark,#212121); color:#f1f1f1; border-color: var(--fr-border-dark,#303030);} @@ -1331,7 +1341,7 @@ label{font-size: 0.9rem;} } #viewOptionsPopover input[type=range]{ flex:1; - accent-color: var(--filr-accent-500,#0d6efd); + } #viewOptionsPopover .vo-value{ min-width:42px; @@ -2943,6 +2953,171 @@ body.dark-mode #decreaseFont:not(:disabled):hover,body.dark-mode #increaseFont:n .section-content { display:none; margin-left:20px; margin-top:8px; margin-bottom:8px; } +#adminPanelModal .section-header { + background: linear-gradient(180deg, rgba(15, 23, 42, 0.05), rgba(15, 23, 42, 0.02)); + border: 1px solid rgba(15, 23, 42, 0.08); + padding: 8px 12px 8px 22px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + font-size: 0.85rem; + position: relative; + margin-top: 12px; + color: #111827; + transition: background 140ms ease, border-color 140ms ease, box-shadow 140ms ease, transform 140ms ease; +} +#adminPanelModal .section-header:hover { + background: rgba(15, 23, 42, 0.04); + border-color: rgba(15, 23, 42, 0.16); + transform: translateY(-1px); +} +#adminPanelModal .section-header:not(.collapsed) { + background: rgba(59, 130, 246, 0.06); + border-color: rgba(59, 130, 246, 0.35); + box-shadow: 0 6px 14px rgba(15, 23, 42, 0.08); +} +#adminPanelModal .section-header:not(.collapsed)::before { + content: ""; + position: absolute; + left: 8px; + top: 8px; + bottom: 8px; + width: 3px; + border-radius: 999px; + background: linear-gradient(180deg, rgba(37, 99, 235, 0.9), rgba(96, 165, 250, 0.9)); +} +#adminPanelModal .section-header .material-icons { color: #475569; } +body.dark-mode #adminPanelModal .section-header { + background: rgba(255, 255, 255, 0.05); + border-color: rgba(255, 255, 255, 0.08); + color: #f1f5f9; +} +body.dark-mode #adminPanelModal .section-header:hover { + background: rgba(255, 255, 255, 0.08); +} +body.dark-mode #adminPanelModal .section-header:not(.collapsed) { + background: rgba(96, 165, 250, 0.18); + border-color: rgba(96, 165, 250, 0.35); + box-shadow: 0 8px 18px rgba(0, 0, 0, 0.45); +} +body.dark-mode #adminPanelModal .section-header:not(.collapsed)::before { + background: linear-gradient(180deg, rgba(148, 197, 255, 0.95), rgba(96, 165, 250, 0.95)); +} +body.dark-mode #adminPanelModal .section-header .material-icons { color: #cbd5f5; } + +#adminPanelModal .section-content { + display: none; + margin: 8px 0 12px; + padding: 12px 14px; + border-radius: 12px; + border: 1px solid rgba(15, 23, 42, 0.06); + background: #ffffff; + box-shadow: 0 4px 10px rgba(15, 23, 42, 0.04); + opacity: 0; + transform: translateY(-4px); + transition: opacity 160ms ease, transform 160ms ease; +} +#adminPanelModal .section-content.is-open { + opacity: 1; + transform: translateY(0); +} +#adminPanelModal .section-content.is-closing { + opacity: 0; + transform: translateY(-4px); +} +body.dark-mode #adminPanelModal .section-content { + background: #232323; + border-color: rgba(255, 255, 255, 0.08); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.45); +} + +#adminPanelModal .admin-panel-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding-right: 42px; + margin-bottom: 6px; +} +#adminPanelModal .admin-panel-header h3 { + margin: 0; + font-weight: 600; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px; +} +#adminPanelModal .admin-panel-actions { + display: flex; + align-items: center; + gap: 8px; +} +#adminPanelModal .admin-search-toggle { + display: inline-flex; + align-items: center; + gap: 6px; + border-radius: 999px; + padding: 4px 10px; + border: 1px solid rgba(15, 23, 42, 0.12); + background: #fff; + color: #111; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06); + transition: background 120ms ease, border-color 120ms ease, box-shadow 120ms ease; +} +#adminPanelModal .admin-search-toggle .material-icons { font-size: 18px; } +#adminPanelModal .admin-search-toggle:hover { + background: #f8fafc; +} +#adminPanelModal .admin-search-toggle.is-active { + border-color: rgba(37, 99, 235, 0.45); + box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.12); +} +#adminPanelModal .admin-search-toggle-label { + font-size: 12px; + font-weight: 600; + letter-spacing: 0.02em; +} +#adminPanelModal .admin-search-wrap { + margin: 6px 0 12px; + padding: 8px 12px; + border-radius: 12px; + border: 1px solid rgba(15, 23, 42, 0.08); + background: rgba(15, 23, 42, 0.03); + overflow: hidden; + max-height: 180px; + transition: max-height 200ms ease, opacity 200ms ease, transform 200ms ease, margin 200ms ease, padding 200ms ease, border-color 200ms ease; +} +#adminPanelModal .admin-search-wrap.is-collapsed { + max-height: 0; + opacity: 0; + transform: translateY(-6px); + margin: 0; + padding-top: 0; + padding-bottom: 0; + border-color: transparent; +} +body.dark-mode #adminPanelModal .admin-search-toggle { + background: #2a2a2a; + color: #e5e7eb; + border-color: rgba(255, 255, 255, 0.12); + box-shadow: none; +} +body.dark-mode #adminPanelModal .admin-search-toggle:hover { + background: #333; +} +body.dark-mode #adminPanelModal .admin-search-toggle.is-active { + border-color: rgba(147, 197, 253, 0.45); + box-shadow: 0 0 0 2px rgba(147, 197, 253, 0.12); +} +body.dark-mode #adminPanelModal .admin-search-wrap { + background: rgba(255, 255, 255, 0.04); + border-color: rgba(255, 255, 255, 0.08); +} +@media (max-width: 720px) { + #adminPanelModal .admin-search-toggle-label { display: none; } + #adminPanelModal .admin-panel-header { gap: 8px; } +} + #adminPanelModal .editor-close-btn, #closeRestoreModal { position:absolute; top:10px; right:10px; display:flex; align-items:center; justify-content:center; @@ -2957,6 +3132,50 @@ body.dark-mode #decreaseFont:not(:disabled):hover,body.dark-mode #increaseFont:n .action-row { display:flex; justify-content:space-between; margin-top:15px; } +#adminPanelModal .action-row { + position: sticky; + bottom: 0; + z-index: 2; + margin-top: 18px; + padding: 10px 0 6px; + gap: 6px; + justify-content: flex-end; + background: linear-gradient(180deg, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.9) 35%, rgba(255, 255, 255, 1)); + border-top: 1px solid rgba(15, 23, 42, 0.08); +} +#adminPanelModal .action-row .btn { + border-radius: 999px; + min-width: 110px; + box-shadow: none; + transform: none; + transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease; +} +#adminPanelModal .action-row .btn:hover, +#adminPanelModal .action-row .btn:focus-visible, +#adminPanelModal .action-row .btn:active { + box-shadow: none; + transform: none; + opacity: 1; + filter: brightness(1.02); +} +#adminPanelModal .action-row .btn.btn-primary { + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.16); +} +#adminPanelModal .action-row .btn.btn-primary:hover, +#adminPanelModal .action-row .btn.btn-primary:focus-visible { + transform: translateY(-1px); + box-shadow: 0 10px 22px rgba(0, 140, 180, 0.28); + filter: brightness(1.04); +} +#adminPanelModal .action-row .btn.btn-primary:active { + transform: translateY(0); + box-shadow: 0 4px 12px rgba(0, 140, 180, 0.22); +} +body.dark-mode #adminPanelModal .action-row { + background: linear-gradient(180deg, rgba(35, 35, 35, 0), rgba(35, 35, 35, 0.92) 35%, rgba(35, 35, 35, 1)); + border-top-color: rgba(255, 255, 255, 0.08); +} + /* ---------- Folder access editor ---------- */ .folder-access-toolbar { display:flex; gap:8px; align-items:center; flex-wrap:wrap; margin:8px 0 6px; @@ -3979,7 +4198,7 @@ body.dark-mode #adminPanelModal .sources-hint-btn::after{background:var(--fr-sur /* Give every admin section a little breathing room at the top */ #adminPanelModal .section-content { - padding-top: 10px; + padding-top: 12px; } #adminPanelModal .admin-subsection-title { font-weight: 600; diff --git a/public/js/adminPanel.js b/public/js/adminPanel.js index d4b18d8..3d5b1fb 100644 --- a/public/js/adminPanel.js +++ b/public/js/adminPanel.js @@ -39,6 +39,16 @@ const PRO_API_MIN_VERSION_LABELS = { sources: '1.5.0' }; const CORE_REQUIRED_PRO_API_LEVEL = Math.max(...Object.values(PRO_API_LEVELS)); +const DEFAULT_HEADER_TITLE = 'FileRise'; +const PRO_DEFAULT_HEADER_TITLE = 'FileRise Pro'; + +function resolveHeaderTitle(rawTitle, isPro) { + const cleaned = String(rawTitle || '').trim(); + if (!cleaned || cleaned === DEFAULT_HEADER_TITLE) { + return isPro ? PRO_DEFAULT_HEADER_TITLE : DEFAULT_HEADER_TITLE; + } + return cleaned; +} function compareSemver(a, b) { const pa = String(a || '').split('.').map(n => parseInt(n, 10) || 0); @@ -435,7 +445,7 @@ function wireHeaderTitleLive() { input.__live = true; const apply = (val) => { - const title = (val || '').trim() || 'FileRise'; + const title = resolveHeaderTitle(val, window.__FR_IS_PRO === true); const h1 = document.querySelector('.header-title h1'); if (h1) h1.textContent = title; document.title = title; @@ -1414,6 +1424,9 @@ function initSourcesSection({ modalEl, sourcesEnabled, sourcesCfg, isPro, proSou +
+ ${tf('source_gdrive_trash_note', 'Trash is not supported on Google Drive sources; deletes are permanent.')} +
@@ -2317,6 +2330,7 @@ function captureInitialAdminConfig() { enableWebDAV: !!document.getElementById("enableWebDAV")?.checked, sharedMaxUploadSize: (document.getElementById("sharedMaxUploadSize")?.value || "").trim(), + resumableChunkMb: (document.getElementById("resumableChunkMb")?.value || "").trim(), globalOtpauthUrl: (document.getElementById("globalOtpauthUrl")?.value || "").trim(), brandingCustomLogoUrl: (document.getElementById("brandingCustomLogoUrl")?.value || "").trim(), brandingHeaderBgLight: (document.getElementById("brandingHeaderBgLight")?.value || "").trim(), @@ -2361,6 +2375,7 @@ function hasUnsavedChanges() { getChk("enableWebDAV") !== o.enableWebDAV || getVal("sharedMaxUploadSize") !== o.sharedMaxUploadSize || + getVal("resumableChunkMb") !== o.resumableChunkMb || getVal("globalOtpauthUrl") !== o.globalOtpauthUrl || getVal("brandingCustomLogoUrl") !== (o.brandingCustomLogoUrl || "") || getVal("brandingHeaderBgLight") !== (o.brandingHeaderBgLight || "") || @@ -2520,14 +2535,178 @@ function showTypedConfirmModal({ title, message, confirmText, placeholder }) { }); } +const SECTION_ANIM_MS = 160; + +function clearSectionTimer(cnt) { + if (cnt && cnt.__closeTimer) { + clearTimeout(cnt.__closeTimer); + cnt.__closeTimer = null; + } +} + +function openSectionContent(cnt) { + if (!cnt) return; + clearSectionTimer(cnt); + cnt.style.display = "block"; + cnt.classList.remove("is-closing"); + requestAnimationFrame(() => { + cnt.classList.add("is-open"); + }); +} + +function closeSectionContent(cnt) { + if (!cnt) return; + clearSectionTimer(cnt); + cnt.classList.remove("is-open"); + cnt.classList.add("is-closing"); + cnt.__closeTimer = setTimeout(() => { + if (cnt.classList.contains("is-closing")) { + cnt.style.display = "none"; + cnt.classList.remove("is-closing"); + } + cnt.__closeTimer = null; + }, SECTION_ANIM_MS); +} + +function setSectionContentImmediate(cnt, open) { + if (!cnt) return; + clearSectionTimer(cnt); + if (open) { + cnt.style.display = "block"; + cnt.classList.add("is-open"); + cnt.classList.remove("is-closing"); + } else { + cnt.style.display = "none"; + cnt.classList.remove("is-open", "is-closing"); + } +} + function toggleSection(id) { const hdr = document.getElementById(id + "Header"); const cnt = document.getElementById(id + "Content"); if (!hdr || !cnt) return; const isCollapsedNow = hdr.classList.toggle("collapsed"); - cnt.style.display = isCollapsedNow ? "none" : "block"; + if (isCollapsedNow) { + closeSectionContent(cnt); + } else { + openSectionContent(cnt); + } if (!isCollapsedNow && id === "shareLinks") { loadShareLinksSection(); + cnt.dataset.loaded = "1"; + } +} + +function normalizeAdminSearchText(value) { + return String(value || "") + .toLowerCase() + .replace(/\s+/g, " ") + .trim(); +} + +function wireAdminPanelSearch(sectionIds) { + const input = document.getElementById("adminSettingsSearch"); + const emptyState = document.getElementById("adminSettingsSearchEmpty"); + const wrap = document.getElementById("adminSettingsSearchWrap"); + const toggleBtn = document.getElementById("adminSearchToggle"); + if (!input || !Array.isArray(sectionIds)) return; + + const ids = sectionIds.filter(Boolean); + + const setOpen = (open, opts = {}) => { + if (!wrap || !toggleBtn) return; + wrap.classList.toggle("is-collapsed", !open); + toggleBtn.setAttribute("aria-expanded", open ? "true" : "false"); + if (open && opts.focus) { + input.focus(); + } + }; + + const updateToggleState = () => { + if (!toggleBtn) return; + const hasQuery = !!normalizeAdminSearchText(input.value); + toggleBtn.classList.toggle("is-active", hasQuery); + }; + + const restoreState = () => { + ids.forEach(id => { + const hdr = document.getElementById(id + "Header"); + const cnt = document.getElementById(id + "Content"); + if (!hdr || !cnt) return; + const saved = hdr.dataset.prevCollapsed; + hdr.style.display = ""; + cnt.style.display = ""; + if (saved !== undefined) { + const wasCollapsed = saved === "1"; + hdr.classList.toggle("collapsed", wasCollapsed); + setSectionContentImmediate(cnt, !wasCollapsed); + delete hdr.dataset.prevCollapsed; + } + }); + if (emptyState) emptyState.style.display = "none"; + }; + + const applyFilter = () => { + const query = normalizeAdminSearchText(input.value); + if (!query) { + restoreState(); + updateToggleState(); + return; + } + + let matches = 0; + ids.forEach(id => { + const hdr = document.getElementById(id + "Header"); + const cnt = document.getElementById(id + "Content"); + if (!hdr || !cnt) return; + if (hdr.dataset.prevCollapsed === undefined) { + hdr.dataset.prevCollapsed = hdr.classList.contains("collapsed") ? "1" : "0"; + } + + const haystack = normalizeAdminSearchText(hdr.textContent + " " + cnt.textContent); + const match = haystack.includes(query); + hdr.style.display = match ? "" : "none"; + setSectionContentImmediate(cnt, match); + if (match) { + hdr.classList.remove("collapsed"); + matches += 1; + if (id === "shareLinks" && !cnt.dataset.loaded) { + loadShareLinksSection(); + cnt.dataset.loaded = "1"; + } + } + }); + + if (emptyState) emptyState.style.display = matches ? "none" : "block"; + updateToggleState(); + }; + + if (toggleBtn && wrap && !toggleBtn.__wired) { + toggleBtn.__wired = true; + toggleBtn.addEventListener("click", () => { + const isCollapsed = wrap.classList.contains("is-collapsed"); + if (isCollapsed) { + setOpen(true, { focus: true }); + return; + } + const hasQuery = !!normalizeAdminSearchText(input.value); + if (hasQuery) { + input.focus(); + return; + } + setOpen(false); + }); + } + + input.addEventListener("input", applyFilter); + + const initialQuery = normalizeAdminSearchText(input.value); + if (initialQuery) { + setOpen(true); + applyFilter(); + } else { + setOpen(false); + updateToggleState(); } } @@ -3542,11 +3721,8 @@ export function openAdminPanel() { fetch("/api/admin/getConfig.php", { credentials: "include" }) .then(r => r.json()) .then(config => { - if (config.header_title) { - const h = document.querySelector(".header-title h1"); - if (h) h.textContent = config.header_title; - window.headerTitle = config.header_title; - } + const rawHeaderTitle = (typeof config.header_title === 'string') ? config.header_title : ''; + window.headerTitle = rawHeaderTitle; window.currentOIDCConfig = window.currentOIDCConfig || {}; if (config.oidc && typeof config.oidc === 'object') { @@ -3561,6 +3737,10 @@ export function openAdminPanel() { const proInfo = config.pro || {}; const isPro = !!proInfo.active; window.__FR_IS_PRO = isPro; + const headerDisplayTitle = resolveHeaderTitle(rawHeaderTitle, isPro); + const h = document.querySelector(".header-title h1"); + if (h) h.textContent = headerDisplayTitle; + document.title = headerDisplayTitle; const proType = proInfo.type || ''; const proEmail = proInfo.email || ''; const proVersion = proInfo.version || 'not installed'; @@ -3575,6 +3755,9 @@ export function openAdminPanel() { const proPlan = proInfo.plan || ''; // e.g. "early_supporter_1x", "personal_yearly" const proUpdatesUntil = proInfo.updatesUntil || proInfo.expiresAt || ''; // ISO timestamp string or "" const proInstanceId = proInfo.instanceId || ''; + const proPrimaryAdmin = Object.prototype.hasOwnProperty.call(proInfo, 'primaryAdmin') + ? !!proInfo.primaryAdmin + : true; const proMaxMajor = ( typeof proInfo.maxMajor === 'number' ? proInfo.maxMajor @@ -3685,36 +3868,64 @@ export function openAdminPanel() { display:flex; justify-content:center; align-items:center; z-index:3000; `; + const sections = [ + { id: "userManagement", label: tf("users_access", "Users & Access") }, + { id: "headerSettings", label: tf("appearance_ui", "Appearance & UI") }, + { id: "loginOptions", label: tf("auth_webdav", "Auth & WebDAV (OIDC/TOTP)") }, + { id: "upload", label: tf("uploads_antivirus", "Uploads & Antivirus") }, + { id: "shareLinks", label: tf("sharing_links", "Sharing & Links") }, + { id: "network", label: tf("network_proxy", "Network & Proxy") }, + { id: "encryption", label: tf("encryption_at_rest", "Encryption at rest") }, + { id: "onlyoffice", label: "ONLYOFFICE" }, + { id: "storage", label: tf("storage_usage", "Storage / Disk Usage") } + ]; + if (showSourcesSection) { + const sourcesLabel = !isPro + ? `${tf("sources", "Sources")}Pro` + : tf("sources", "Sources"); + sections.push({ id: "sources", label: sourcesLabel }); + } + sections.push( + { id: "proFeatures", label: "Pro Features" }, + { id: "pro", label: "FileRise Pro" }, + { id: "sponsor", label: (typeof tf === 'function' ? tf("sponsor_donations", "Thanks / Sponsor / Donations") : "Thanks / Sponsor / Donations") } + ); + const sectionIds = sections.map(sec => sec.id); mdl.innerHTML = `