release(v2.3.3): footer branding, Pro bundle UX + file list polish
This commit is contained in:
56
CHANGELOG.md
56
CHANGELOG.md
@@ -1,5 +1,61 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Changes 12/5/2025 (v2.3.3)
|
||||||
|
|
||||||
|
release(v2.3.3): footer branding, Pro bundle UX + file list polish
|
||||||
|
|
||||||
|
**Branding & footer**
|
||||||
|
|
||||||
|
- Added **Pro-only footer branding** (`branding.footerHtml`) stored in `adminConfig.json` and exposed via the Admin API.
|
||||||
|
- Footer is now rendered from config; if no Pro footer is set, FileRise shows:
|
||||||
|
`© YEAR FileRise` with a link to **filerise.net**.
|
||||||
|
- New **“Header & Footer settings”** section in the Admin Panel, with a textarea for footer HTML (simple HTML + links allowed for Pro users).
|
||||||
|
|
||||||
|
**FileRise Pro & license UX**
|
||||||
|
|
||||||
|
- Bumped UI hint to `PRO_LATEST_BUNDLE_VERSION = v1.2.1`.
|
||||||
|
- Pro bundle install now:
|
||||||
|
- Parses the version from the uploaded ZIP basename (works with `C:\fakepath\FileRisePro-v1.2.1.zip`).
|
||||||
|
- Invalidates OPcache for updated Pro files so new code is active immediately.
|
||||||
|
- Re-fetches admin config after a successful install and displays the actual active Pro bundle version in the status line.
|
||||||
|
- Admin config now exposes richer Pro metadata (plan, expiresAt, maxMajor), and the Admin Panel shows:
|
||||||
|
- License type + email,
|
||||||
|
- Friendly **plan** description (early supporter vs personal/business),
|
||||||
|
- **Lifetime** vs **Valid until …** wording instead of a scary raw timestamp.
|
||||||
|
|
||||||
|
**Upload UX**
|
||||||
|
|
||||||
|
- Upload button is now only visible/enabled when there are files queued (regular or resumable):
|
||||||
|
- Hidden when the list is empty or after clearing uploads.
|
||||||
|
- Shown again when user picks or drags in files.
|
||||||
|
- Adjusted Upload / Choose Files button sizing and spacing for a cleaner upload card, especially on smaller screens.
|
||||||
|
|
||||||
|
**File list & hover preview polish**
|
||||||
|
|
||||||
|
- Inline folders now respect the current sort mode:
|
||||||
|
- **Name** sort: A–Z / Z–A.
|
||||||
|
- **Size** sort: uses folder stats (bytes) and sorts accordingly.
|
||||||
|
- Size and meta columns:
|
||||||
|
- Right-aligned **size**, **uploaded/created**, **modified**, and **owner/uploader** columns.
|
||||||
|
- Use tabular numerals for nicer numeric alignment.
|
||||||
|
- Hover preview:
|
||||||
|
- Skips “fake” rows (e.g. “No files found”) and rows that don’t resolve to a real file.
|
||||||
|
- Uses `sizeBytes` + `formatSize()` for a consistent, human-readable size.
|
||||||
|
- `formatSize()` now uses 1 decimal place (KB/MB/GB) and short `B` label for bytes.
|
||||||
|
- File metadata normalization:
|
||||||
|
- Every file gets a `sizeBytes`, normalized display `size`, and a `cacheKey` derived from modified/uploaded/size, used for stable cache-busting.
|
||||||
|
- Gallery / preview URLs now use `apiFileUrl()` with a stable `t` parameter instead of `Date.now()`, improving browser caching behavior.
|
||||||
|
|
||||||
|
**Layout & animation tweaks**
|
||||||
|
|
||||||
|
- Slightly reduced default upload card padding and button sizes to make the homepage cards feel less “tall”.
|
||||||
|
- New **site footer** styling (subtle border, centered text) added below the main layout.
|
||||||
|
- Drag-and-drop card (upload/folder cards to header dock) animations:
|
||||||
|
- Crisper ghost cards with better text opacity and anti-jank tweaks.
|
||||||
|
- Longer, smoother easing and more readable motion (both collapse-to-header and expand-from-header).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Changes 12/3/2025 (v2.3.2)
|
## Changes 12/3/2025 (v2.3.2)
|
||||||
|
|
||||||
release(v2.3.2): fix media preview URLs and tighten hover card layout
|
release(v2.3.2): fix media preview URLs and tighten hover card layout
|
||||||
|
|||||||
@@ -543,21 +543,22 @@ body{letter-spacing: 0.2px;
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
gap: 5px;}
|
gap: 5px;}
|
||||||
#uploadBtn{font-size: 20px;
|
#uploadBtn{font-size: 18px;
|
||||||
padding: 10px 22px;
|
padding: 10px 18px;
|
||||||
align-items: center;}
|
align-items: center;
|
||||||
|
margin-top:20px;}
|
||||||
.card-body.d-flex.flex-column{padding: 0.75rem !important;}
|
.card-body.d-flex.flex-column{padding: 0.75rem !important;}
|
||||||
#customChooseBtn{background-color: #9E9E9E;
|
#customChooseBtn{background-color: #9E9E9E;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 8px 18px;
|
padding: 8px 14px;
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
white-space: nowrap;}
|
white-space: nowrap;}
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
#customChooseBtn{font-size: 14px;
|
#customChooseBtn{font-size: 12px;
|
||||||
padding: 6px 14px;}
|
padding: 6px 10px;}
|
||||||
}
|
}
|
||||||
.pause-resume-btn{background: none;
|
.pause-resume-btn{background: none;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -772,7 +773,7 @@ body:not(.dark-mode) .material-icons.pauseResumeBtn:hover{background-color: rgba
|
|||||||
text-align: left !important;
|
text-align: left !important;
|
||||||
line-height: 1.2 !important;
|
line-height: 1.2 !important;
|
||||||
vertical-align: middle !important;
|
vertical-align: middle !important;
|
||||||
padding: 8px 10px !important;
|
padding: 2px 4px !important;
|
||||||
max-width: 250px !important;
|
max-width: 250px !important;
|
||||||
min-width: 120px !important;}
|
min-width: 120px !important;}
|
||||||
@media (min-width: 500px) {
|
@media (min-width: 500px) {
|
||||||
@@ -1442,8 +1443,6 @@ label{font-size: 0.9rem;}
|
|||||||
#folderManagementCard{transition: transform 0.3s ease, opacity 0.3s ease;
|
#folderManagementCard{transition: transform 0.3s ease, opacity 0.3s ease;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
min-height: 320px;
|
|
||||||
|
|
||||||
border-radius: var(--menu-radius);
|
border-radius: var(--menu-radius);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid var(--card-border, #e5e7eb);
|
border: 1px solid var(--card-border, #e5e7eb);
|
||||||
@@ -2924,4 +2923,21 @@ th[data-column="actions"]::after {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
clip: rect(0 0 0 0);
|
clip: rect(0 0 0 0);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-footer {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--filr-muted-text, #777);
|
||||||
|
border-top: 1px solid rgba(0,0,0,0.06);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-footer span {
|
||||||
|
text-align: center;
|
||||||
|
max-width: 100%;
|
||||||
|
white-space: normal;
|
||||||
}
|
}
|
||||||
@@ -188,7 +188,7 @@
|
|||||||
<div class="card-header" data-i18n-key="upload_header">Upload Files/Folders</div>
|
<div class="card-header" data-i18n-key="upload_header">Upload Files/Folders</div>
|
||||||
<div class="card-body d-flex flex-column">
|
<div class="card-body d-flex flex-column">
|
||||||
<form id="uploadFileForm" method="post" enctype="multipart/form-data" class="d-flex flex-column">
|
<form id="uploadFileForm" method="post" enctype="multipart/form-data" class="d-flex flex-column">
|
||||||
<div class="form-group flex-grow-1" style="margin-bottom: 1rem;">
|
<div class="form-group flex-grow-1" style="margin-bottom: 0rem;">
|
||||||
<div id="uploadDropArea"
|
<div id="uploadDropArea"
|
||||||
style="border:2px dashed #ccc; padding:20px; cursor:pointer; display:flex; flex-direction:column; justify-content:center; align-items:center; position:relative;">
|
style="border:2px dashed #ccc; padding:20px; cursor:pointer; display:flex; flex-direction:column; justify-content:center; align-items:center; position:relative;">
|
||||||
<span data-i18n-key="upload_instruction">Drop files/folders here or click 'Choose
|
<span data-i18n-key="upload_instruction">Drop files/folders here or click 'Choose
|
||||||
@@ -199,7 +199,7 @@
|
|||||||
<button type="button" id="customChooseBtn" data-i18n-key="choose_files">Choose Files</button>
|
<button type="button" id="customChooseBtn" data-i18n-key="choose_files">Choose Files</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" id="uploadBtn" class="btn btn-primary d-block mx-auto"
|
<button type="submit" id="uploadBtn" class="btn btn-primary mx-auto"
|
||||||
data-i18n-key="upload">Upload</button>
|
data-i18n-key="upload">Upload</button>
|
||||||
<div id="uploadProgressContainer"></div>
|
<div id="uploadProgressContainer"></div>
|
||||||
</form>
|
</form>
|
||||||
@@ -216,7 +216,7 @@
|
|||||||
<div class="form-group d-flex align-items-top" style="padding-top:0; margin-bottom:0;">
|
<div class="form-group d-flex align-items-top" style="padding-top:0; margin-bottom:0;">
|
||||||
<div id="folderTreeContainer"></div>
|
<div id="folderTreeContainer"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="folder-actions mt-3">
|
<div class="folder-actions">
|
||||||
<button id="createFolderBtn" class="btn btn-primary" data-i18n-title="create_folder">
|
<button id="createFolderBtn" class="btn btn-primary" data-i18n-title="create_folder">
|
||||||
<i class="material-icons">create_new_folder</i>
|
<i class="material-icons">create_new_folder</i>
|
||||||
</button>
|
</button>
|
||||||
@@ -538,5 +538,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<footer id="siteFooter" class="site-footer">
|
||||||
|
<span>
|
||||||
|
© 2025
|
||||||
|
<a href="https://filerise.net" target="_blank" rel="noopener noreferrer">
|
||||||
|
FileRise
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -20,7 +20,7 @@ function normalizeLogoPath(raw) {
|
|||||||
const version = window.APP_VERSION || "dev";
|
const version = window.APP_VERSION || "dev";
|
||||||
// Hard-coded *FOR NOW* latest FileRise Pro bundle version for UI hints only.
|
// Hard-coded *FOR NOW* latest FileRise Pro bundle version for UI hints only.
|
||||||
// Update this when I cut a new Pro ZIP.
|
// Update this when I cut a new Pro ZIP.
|
||||||
const PRO_LATEST_BUNDLE_VERSION = 'v1.2.0';
|
const PRO_LATEST_BUNDLE_VERSION = 'v1.2.1';
|
||||||
|
|
||||||
function getAdminTitle(isPro, proVersion) {
|
function getAdminTitle(isPro, proVersion) {
|
||||||
const corePill = `
|
const corePill = `
|
||||||
@@ -110,6 +110,25 @@ function applyHeaderColorsFromAdmin() {
|
|||||||
console.warn('Failed to live-update header colors from admin panel', e);
|
console.warn('Failed to live-update header colors from admin panel', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function applyFooterFromAdmin() {
|
||||||
|
try {
|
||||||
|
const footerEl = document.getElementById('siteFooter');
|
||||||
|
if (!footerEl) return;
|
||||||
|
|
||||||
|
const val = (document.getElementById('brandingFooterHtml')?.value || '').trim();
|
||||||
|
if (val) {
|
||||||
|
// Allow HTML here – rely on backend sanitizing what gets stored.
|
||||||
|
footerEl.innerHTML = val;
|
||||||
|
} else {
|
||||||
|
const year = new Date().getFullYear();
|
||||||
|
footerEl.innerHTML =
|
||||||
|
`© ${year} <a href="https://filerise.net" target="_blank" rel="noopener noreferrer">FileRise</a>`;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to live-update footer from admin panel', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateHeaderLogoFromAdmin() {
|
function updateHeaderLogoFromAdmin() {
|
||||||
try {
|
try {
|
||||||
const input = document.getElementById('brandingCustomLogoUrl');
|
const input = document.getElementById('brandingCustomLogoUrl');
|
||||||
@@ -295,6 +314,7 @@ function captureInitialAdminConfig() {
|
|||||||
brandingCustomLogoUrl: (document.getElementById("brandingCustomLogoUrl")?.value || "").trim(),
|
brandingCustomLogoUrl: (document.getElementById("brandingCustomLogoUrl")?.value || "").trim(),
|
||||||
brandingHeaderBgLight: (document.getElementById("brandingHeaderBgLight")?.value || "").trim(),
|
brandingHeaderBgLight: (document.getElementById("brandingHeaderBgLight")?.value || "").trim(),
|
||||||
brandingHeaderBgDark: (document.getElementById("brandingHeaderBgDark")?.value || "").trim(),
|
brandingHeaderBgDark: (document.getElementById("brandingHeaderBgDark")?.value || "").trim(),
|
||||||
|
brandingFooterHtml: (document.getElementById("brandingFooterHtml")?.value || "").trim(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function hasUnsavedChanges() {
|
function hasUnsavedChanges() {
|
||||||
@@ -315,7 +335,8 @@ function hasUnsavedChanges() {
|
|||||||
getVal("globalOtpauthUrl") !== o.globalOtpauthUrl ||
|
getVal("globalOtpauthUrl") !== o.globalOtpauthUrl ||
|
||||||
getVal("brandingCustomLogoUrl") !== (o.brandingCustomLogoUrl || "") ||
|
getVal("brandingCustomLogoUrl") !== (o.brandingCustomLogoUrl || "") ||
|
||||||
getVal("brandingHeaderBgLight") !== (o.brandingHeaderBgLight || "") ||
|
getVal("brandingHeaderBgLight") !== (o.brandingHeaderBgLight || "") ||
|
||||||
getVal("brandingHeaderBgDark") !== (o.brandingHeaderBgDark || "")
|
getVal("brandingHeaderBgDark") !== (o.brandingHeaderBgDark || "") ||
|
||||||
|
getVal("brandingFooterHtml") !== (o.brandingFooterHtml || "")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,13 +430,42 @@ export function initProBundleInstaller() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const versionText = data.proVersion ? ` (version ${data.proVersion})` : '';
|
// --- NEW: ask the server what version is now active via getConfig.php ---
|
||||||
|
let finalVersion = '';
|
||||||
|
try {
|
||||||
|
const cfgRes = await fetch('/api/admin/getConfig.php?ts=' + Date.now(), {
|
||||||
|
credentials: 'include',
|
||||||
|
cache: 'no-store',
|
||||||
|
headers: { 'Cache-Control': 'no-store' }
|
||||||
|
});
|
||||||
|
const cfg = await safeJson(cfgRes).catch(() => null);
|
||||||
|
const cfgVersion = cfg && cfg.pro && cfg.pro.version;
|
||||||
|
if (cfgVersion) {
|
||||||
|
finalVersion = String(cfgVersion);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// If this fails, just fall back to whatever installProBundle gave us.
|
||||||
|
console.warn('Failed to refresh config after Pro bundle install', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!finalVersion && data.proVersion) {
|
||||||
|
finalVersion = String(data.proVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
const versionText = finalVersion ? ` (version ${finalVersion})` : '';
|
||||||
statusEl.textContent = 'Pro bundle installed' + versionText + '. Reload the page to apply changes.';
|
statusEl.textContent = 'Pro bundle installed' + versionText + '. Reload the page to apply changes.';
|
||||||
statusEl.className = 'small text-success';
|
statusEl.className = 'small text-success';
|
||||||
|
|
||||||
|
// Clear file input so repeat installs feel "fresh"
|
||||||
|
try { fileInput.value = ''; } catch (_) {}
|
||||||
|
|
||||||
|
// Keep existing behavior: refresh any admin config in the header, etc.
|
||||||
if (typeof loadAdminConfigFunc === 'function') {
|
if (typeof loadAdminConfigFunc === 'function') {
|
||||||
loadAdminConfigFunc();
|
loadAdminConfigFunc();
|
||||||
}
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 800);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
statusEl.textContent = 'Install failed: ' + (e && e.message ? e.message : String(e));
|
statusEl.textContent = 'Install failed: ' + (e && e.message ? e.message : String(e));
|
||||||
statusEl.className = 'small text-danger';
|
statusEl.className = 'small text-danger';
|
||||||
@@ -537,10 +587,19 @@ export function openAdminPanel() {
|
|||||||
const proEmail = proInfo.email || '';
|
const proEmail = proInfo.email || '';
|
||||||
const proVersion = proInfo.version || 'not installed';
|
const proVersion = proInfo.version || 'not installed';
|
||||||
const proLicense = proInfo.license || '';
|
const proLicense = proInfo.license || '';
|
||||||
|
// New: richer license metadata from FR_PRO_INFO / backend
|
||||||
|
const proPlan = proInfo.plan || ''; // e.g. "early_supporter_1x", "personal_yearly"
|
||||||
|
const proExpiresAt = proInfo.expiresAt || ''; // ISO timestamp string or ""
|
||||||
|
const proMaxMajor = (
|
||||||
|
typeof proInfo.maxMajor === 'number'
|
||||||
|
? proInfo.maxMajor
|
||||||
|
: (proInfo.maxMajor ? Number(proInfo.maxMajor) : null)
|
||||||
|
);
|
||||||
const brandingCfg = config.branding || {};
|
const brandingCfg = config.branding || {};
|
||||||
const brandingCustomLogoUrl = brandingCfg.customLogoUrl || "";
|
const brandingCustomLogoUrl = brandingCfg.customLogoUrl || "";
|
||||||
const brandingHeaderBgLight = brandingCfg.headerBgLight || "";
|
const brandingHeaderBgLight = brandingCfg.headerBgLight || "";
|
||||||
const brandingHeaderBgDark = brandingCfg.headerBgDark || "";
|
const brandingHeaderBgDark = brandingCfg.headerBgDark || "";
|
||||||
|
const brandingFooterHtml = brandingCfg.footerHtml || "";
|
||||||
const bg = dark ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.3)";
|
const bg = dark ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.3)";
|
||||||
const inner = `
|
const inner = `
|
||||||
background:${dark ? "#2c2c2c" : "#fff"};
|
background:${dark ? "#2c2c2c" : "#fff"};
|
||||||
@@ -569,7 +628,7 @@ export function openAdminPanel() {
|
|||||||
<form id="adminPanelForm">
|
<form id="adminPanelForm">
|
||||||
${[
|
${[
|
||||||
{ id: "userManagement", label: t("user_management") },
|
{ id: "userManagement", label: t("user_management") },
|
||||||
{ id: "headerSettings", label: t("header_settings") },
|
{ id: "headerSettings", label: tf("header_footer_settings", "Header & Footer settings") },
|
||||||
{ id: "loginOptions", label: t("login_options") },
|
{ id: "loginOptions", label: t("login_options") },
|
||||||
{ id: "webdav", label: "WebDAV Access" },
|
{ id: "webdav", label: "WebDAV Access" },
|
||||||
{ id: "onlyoffice", label: "ONLYOFFICE" },
|
{ id: "onlyoffice", label: "ONLYOFFICE" },
|
||||||
@@ -758,8 +817,8 @@ export function openAdminPanel() {
|
|||||||
</label>
|
</label>
|
||||||
<small class="text-muted d-block mb-1">
|
<small class="text-muted d-block mb-1">
|
||||||
${isPro
|
${isPro
|
||||||
? 'Upload a logo image or paste a local path.'
|
? 'Upload a logo image or paste a local path.'
|
||||||
: 'Requires FileRise Pro to enable custom header branding.'}
|
: 'Requires FileRise Pro to enable custom header branding.'}
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
<div class="input-group mb-2">
|
<div class="input-group mb-2">
|
||||||
@@ -818,12 +877,30 @@ export function openAdminPanel() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<small class="text-muted d-block mt-1">
|
<small class="text-muted d-block mt-1">
|
||||||
${isPro
|
${isPro
|
||||||
? 'If left empty, FileRise uses its default blue and dark header colors.'
|
? 'If left empty, FileRise uses its default blue and dark header colors.'
|
||||||
: 'Requires FileRise Pro to enable custom color branding.'}
|
: 'Requires FileRise Pro to enable custom color branding.'}
|
||||||
|
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Pro: Footer text -->
|
||||||
|
<div class="form-group" style="margin-top:16px;">
|
||||||
|
<label for="brandingFooterHtml">
|
||||||
|
Footer text
|
||||||
|
${!isPro ? '<span class="badge badge-pill badge-warning admin-pro-badge" style="margin-left:6px;">Pro</span>' : ''}
|
||||||
|
</label>
|
||||||
|
<small class="text-muted d-block mb-1">
|
||||||
|
${isPro
|
||||||
|
? 'Shown at the bottom of every page. You can include simple HTML like links.'
|
||||||
|
: 'Requires FileRise Pro to customize footer text.'}
|
||||||
|
</small>
|
||||||
|
<textarea
|
||||||
|
id="brandingFooterHtml"
|
||||||
|
class="form-control"
|
||||||
|
rows="2"
|
||||||
|
placeholder="© 2025 Your Company. Powered by FileRise."
|
||||||
|
${!isPro ? 'disabled data-disabled-reason="pro"' : ''}>${isPro ? (brandingFooterHtml || '') : ''}</textarea>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
wireHeaderTitleLive();
|
wireHeaderTitleLive();
|
||||||
|
|
||||||
@@ -946,26 +1023,57 @@ export function openAdminPanel() {
|
|||||||
const hasLatest = !!norm(latestVersionRaw);
|
const hasLatest = !!norm(latestVersionRaw);
|
||||||
const hasUpdate = hasCurrent && hasLatest && norm(currentVersionRaw) !== norm(latestVersionRaw);
|
const hasUpdate = hasCurrent && hasLatest && norm(currentVersionRaw) !== norm(latestVersionRaw);
|
||||||
|
|
||||||
const proMetaHtml =
|
// Friendly description of plan + lifetime/expiry
|
||||||
isPro && (proType || proEmail || proVersion)
|
let planLabel = '';
|
||||||
? `
|
if (proPlan === 'early_supporter_1x' || (!proPlan && isPro)) {
|
||||||
<div class="pro-license-meta" style="margin-top:8px;font-size:12px;color:#777;">
|
const mj = proMaxMajor || 1;
|
||||||
<div>
|
planLabel = `Early supporter – lifetime for FileRise Pro ${mj}.x`;
|
||||||
✅ ${proType ? `License type: ${proType}` : 'License active'}
|
} else if (proPlan) {
|
||||||
${proType && proEmail ? ' • ' : ''}
|
if (proPlan.startsWith('personal_') || proPlan === 'personal_yearly') {
|
||||||
${proEmail ? `Licensed to: ${proEmail}` : ''}
|
planLabel = 'Personal license';
|
||||||
</div>
|
} else if (proPlan.startsWith('business_') || proPlan === 'business_yearly') {
|
||||||
${hasCurrent ? `
|
planLabel = 'Business license';
|
||||||
<div>
|
} else {
|
||||||
Installed Pro bundle: v${norm(currentVersionRaw)}
|
planLabel = proPlan;
|
||||||
</div>` : ''}
|
}
|
||||||
${hasLatest ? `
|
}
|
||||||
<div>
|
|
||||||
Latest Pro bundle (UI hint): ${latestVersionRaw}
|
let expiryLabel = '';
|
||||||
</div>` : ''}
|
if (proPlan === 'early_supporter_1x' || (!proPlan && isPro)) {
|
||||||
</div>
|
// Early supporters: we treat as lifetime for that major – do NOT show an expiry date
|
||||||
`
|
expiryLabel = 'Lifetime license (no expiry)';
|
||||||
: '';
|
} else if (proExpiresAt) {
|
||||||
|
expiryLabel = `Valid until ${proExpiresAt}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const proMetaHtml =
|
||||||
|
isPro && (proType || proEmail || proVersion || planLabel || expiryLabel)
|
||||||
|
? `
|
||||||
|
<div class="pro-license-meta" style="margin-top:8px;font-size:12px;color:#777;">
|
||||||
|
<div>
|
||||||
|
✅ ${proType ? `License type: ${proType}` : 'License active'}
|
||||||
|
${proType && proEmail ? ' • ' : ''}
|
||||||
|
${proEmail ? `Licensed to: ${proEmail}` : ''}
|
||||||
|
</div>
|
||||||
|
${planLabel ? `
|
||||||
|
<div>
|
||||||
|
Plan: ${planLabel}
|
||||||
|
</div>` : ''}
|
||||||
|
${expiryLabel ? `
|
||||||
|
<div>
|
||||||
|
${expiryLabel}
|
||||||
|
</div>` : ''}
|
||||||
|
${hasCurrent ? `
|
||||||
|
<div>
|
||||||
|
Installed Pro bundle: v${norm(currentVersionRaw)}
|
||||||
|
</div>` : ''}
|
||||||
|
${hasLatest ? `
|
||||||
|
<div>
|
||||||
|
Latest Pro bundle (UI hint): ${latestVersionRaw}
|
||||||
|
</div>` : ''}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: '';
|
||||||
|
|
||||||
proContent.innerHTML = `
|
proContent.innerHTML = `
|
||||||
<div class="card pro-card" style="padding:12px; border:1px solid #ddd; border-radius:12px; max-width:720px; margin:8px auto;">
|
<div class="card pro-card" style="padding:12px; border:1px solid #ddd; border-radius:12px; max-width:720px; margin:8px auto;">
|
||||||
@@ -1309,6 +1417,7 @@ function handleSave() {
|
|||||||
customLogoUrl: (document.getElementById("brandingCustomLogoUrl")?.value || "").trim(),
|
customLogoUrl: (document.getElementById("brandingCustomLogoUrl")?.value || "").trim(),
|
||||||
headerBgLight: (document.getElementById("brandingHeaderBgLight")?.value || "").trim(),
|
headerBgLight: (document.getElementById("brandingHeaderBgLight")?.value || "").trim(),
|
||||||
headerBgDark: (document.getElementById("brandingHeaderBgDark")?.value || "").trim(),
|
headerBgDark: (document.getElementById("brandingHeaderBgDark")?.value || "").trim(),
|
||||||
|
footerHtml: (document.getElementById("brandingFooterHtml")?.value || "").trim(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1348,6 +1457,7 @@ function handleSave() {
|
|||||||
closeAdminPanel();
|
closeAdminPanel();
|
||||||
applyHeaderColorsFromAdmin();
|
applyHeaderColorsFromAdmin();
|
||||||
updateHeaderLogoFromAdmin();
|
updateHeaderLogoFromAdmin();
|
||||||
|
applyFooterFromAdmin();
|
||||||
})
|
})
|
||||||
.catch(() => showToast('Save failed.'));
|
.catch(() => showToast('Save failed.'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ function createCardGhost(card, rect, opts) {
|
|||||||
const ghost = card.cloneNode(true);
|
const ghost = card.cloneNode(true);
|
||||||
const cs = window.getComputedStyle(card);
|
const cs = window.getComputedStyle(card);
|
||||||
|
|
||||||
// Give the ghost the same “card” chrome even though it’s attached to <body>
|
|
||||||
Object.assign(ghost.style, {
|
Object.assign(ghost.style, {
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
left: rect.left + 'px',
|
left: rect.left + 'px',
|
||||||
@@ -94,7 +93,6 @@ function createCardGhost(card, rect, opts) {
|
|||||||
transform: 'scale(' + scale + ')',
|
transform: 'scale(' + scale + ')',
|
||||||
opacity: String(opacity),
|
opacity: String(opacity),
|
||||||
|
|
||||||
// pull key visuals from the real card
|
|
||||||
backgroundColor: cs.backgroundColor || 'rgba(24,24,24,.96)',
|
backgroundColor: cs.backgroundColor || 'rgba(24,24,24,.96)',
|
||||||
borderRadius: cs.borderRadius || '',
|
borderRadius: cs.borderRadius || '',
|
||||||
boxShadow: cs.boxShadow || '',
|
boxShadow: cs.boxShadow || '',
|
||||||
@@ -102,8 +100,17 @@ function createCardGhost(card, rect, opts) {
|
|||||||
borderWidth: cs.borderWidth || '',
|
borderWidth: cs.borderWidth || '',
|
||||||
borderStyle: cs.borderStyle || '',
|
borderStyle: cs.borderStyle || '',
|
||||||
backdropFilter: cs.backdropFilter || '',
|
backdropFilter: cs.backdropFilter || '',
|
||||||
|
|
||||||
|
// ✨ make the ghost crisper
|
||||||
|
overflow: 'hidden',
|
||||||
|
willChange: 'transform, opacity',
|
||||||
|
backfaceVisibility: 'hidden'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Subtle: de-emphasize inner text so it doesn’t look “smeared”
|
||||||
|
const ghBody = ghost.querySelector('.card-body');
|
||||||
|
if (ghBody) ghBody.style.opacity = '0.6';
|
||||||
|
|
||||||
return ghost;
|
return ghost;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,7 +403,7 @@ function animateCardsIntoHeaderAndThen(done) {
|
|||||||
return { card, rect };
|
return { card, rect };
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show dock so icons exist / have positions
|
// Make sure header dock is visible so icons are laid out
|
||||||
showHeaderDockPersistent();
|
showHeaderDockPersistent();
|
||||||
|
|
||||||
// Move real cards into header (hidden container + icons)
|
// Move real cards into header (hidden container + icons)
|
||||||
@@ -410,16 +417,16 @@ function animateCardsIntoHeaderAndThen(done) {
|
|||||||
// remember the size for the expand animation later
|
// remember the size for the expand animation later
|
||||||
card.dataset.lastWidth = String(rect.width);
|
card.dataset.lastWidth = String(rect.width);
|
||||||
card.dataset.lastHeight = String(rect.height);
|
card.dataset.lastHeight = String(rect.height);
|
||||||
|
|
||||||
const iconBtn = card.headerIconButton;
|
const iconBtn = card.headerIconButton;
|
||||||
if (!iconBtn) return;
|
if (!iconBtn) return;
|
||||||
|
|
||||||
const iconRect = iconBtn.getBoundingClientRect();
|
const iconRect = iconBtn.getBoundingClientRect();
|
||||||
|
|
||||||
const ghost = createCardGhost(card, rect, { scale: 1, opacity: 1 });
|
const ghost = createCardGhost(card, rect, { scale: 1, opacity: 0.95 });
|
||||||
ghost.id = card.id + '-ghost-collapse';
|
ghost.id = card.id + '-ghost-collapse';
|
||||||
ghost.classList.add('card-collapse-ghost');
|
ghost.classList.add('card-collapse-ghost');
|
||||||
ghost.style.transition = 'transform 0.22s ease-out, opacity 0.22s ease-out';
|
ghost.style.transition = 'transform 0.4s cubic-bezier(.22,.61,.36,1), opacity 0.4s linear';
|
||||||
|
|
||||||
document.body.appendChild(ghost);
|
document.body.appendChild(ghost);
|
||||||
ghosts.push({ ghost, from: rect, to: iconRect });
|
ghosts.push({ ghost, from: rect, to: iconRect });
|
||||||
@@ -430,6 +437,7 @@ function animateCardsIntoHeaderAndThen(done) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Kick off motion on next frame
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
ghosts.forEach(({ ghost, from, to }) => {
|
ghosts.forEach(({ ghost, from, to }) => {
|
||||||
const fromCx = from.left + from.width / 2;
|
const fromCx = from.left + from.width / 2;
|
||||||
@@ -441,17 +449,18 @@ function animateCardsIntoHeaderAndThen(done) {
|
|||||||
const dy = toCy - fromCy;
|
const dy = toCy - fromCy;
|
||||||
|
|
||||||
const rawScale = to.width / from.width;
|
const rawScale = to.width / from.width;
|
||||||
const scale = Math.max(0.25, Math.min(0.5, rawScale * 0.9));
|
const scale = Math.max(0.35, Math.min(0.6, rawScale * 0.9));
|
||||||
|
|
||||||
|
// ✨ more readable: clear slide + shrink, but don’t fully vanish mid-flight
|
||||||
ghost.style.transform = `translate(${dx}px, ${dy}px) scale(${scale})`;
|
ghost.style.transform = `translate(${dx}px, ${dy}px) scale(${scale})`;
|
||||||
ghost.style.opacity = '0';
|
ghost.style.opacity = '0.35';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
ghosts.forEach(({ ghost }) => { try { ghost.remove(); } catch {} });
|
ghosts.forEach(({ ghost }) => { try { ghost.remove(); } catch {} });
|
||||||
done();
|
done();
|
||||||
}, 260);
|
}, 430); // a bit over the 0.4s transition
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveTargetZoneForExpand(cardId) {
|
function resolveTargetZoneForExpand(cardId) {
|
||||||
@@ -508,9 +517,9 @@ function animateCardsOutOfHeaderThen(done) {
|
|||||||
if (sb) sb.style.display = '';
|
if (sb) sb.style.display = '';
|
||||||
if (top) top.style.display = '';
|
if (top) top.style.display = '';
|
||||||
|
|
||||||
const SAFE_TOP = 16; // minimum distance from top of viewport
|
const SAFE_TOP = 16;
|
||||||
const START_OFFSET_Y = 40; // how far BELOW the icon we start the ghost
|
const START_OFFSET_Y = 32; // a touch closer to header
|
||||||
const DEST_EXTRA_Y = 120; // how far down into the zone center we aim
|
const DEST_EXTRA_Y = 120;
|
||||||
|
|
||||||
const ghosts = [];
|
const ghosts = [];
|
||||||
|
|
||||||
@@ -528,24 +537,20 @@ function animateCardsOutOfHeaderThen(done) {
|
|||||||
const zoneRect = host.getBoundingClientRect();
|
const zoneRect = host.getBoundingClientRect();
|
||||||
if (!zoneRect.width) return;
|
if (!zoneRect.width) return;
|
||||||
|
|
||||||
// Where the ghost "comes from" (near the icon)
|
|
||||||
const fromCx = iconRect.left + iconRect.width / 2;
|
const fromCx = iconRect.left + iconRect.width / 2;
|
||||||
const fromCy = iconRect.bottom + START_OFFSET_Y; // lower starting point
|
const fromCy = iconRect.bottom + START_OFFSET_Y;
|
||||||
|
|
||||||
// Where we want it to "land" (roughly center of the zone, a bit down)
|
|
||||||
let toCx = zoneRect.left + zoneRect.width / 2;
|
let toCx = zoneRect.left + zoneRect.width / 2;
|
||||||
let toCy = zoneRect.top + Math.min(zoneRect.height / 2 || DEST_EXTRA_Y, DEST_EXTRA_Y);
|
let toCy = zoneRect.top + Math.min(zoneRect.height / 2 || DEST_EXTRA_Y, DEST_EXTRA_Y);
|
||||||
|
|
||||||
// 🔹 If both cards are going to the sidebar, offset them so they don't stack
|
|
||||||
if (zoneId === ZONES.SIDEBAR) {
|
if (zoneId === ZONES.SIDEBAR) {
|
||||||
if (card.id === 'uploadCard') {
|
if (card.id === 'uploadCard') {
|
||||||
toCy -= 48; // a bit higher
|
toCy -= 48;
|
||||||
} else if (card.id === 'folderManagementCard') {
|
} else if (card.id === 'folderManagementCard') {
|
||||||
toCy += 48; // a bit lower
|
toCy += 48;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to match the real card size we captured during collapse
|
|
||||||
const savedW = parseFloat(card.dataset.lastWidth || '');
|
const savedW = parseFloat(card.dataset.lastWidth || '');
|
||||||
const savedH = parseFloat(card.dataset.lastHeight || '');
|
const savedH = parseFloat(card.dataset.lastHeight || '');
|
||||||
const targetWidth = !Number.isNaN(savedW)
|
const targetWidth = !Number.isNaN(savedW)
|
||||||
@@ -553,10 +558,8 @@ function animateCardsOutOfHeaderThen(done) {
|
|||||||
: Math.min(280, Math.max(220, zoneRect.width * 0.85));
|
: Math.min(280, Math.max(220, zoneRect.width * 0.85));
|
||||||
const targetHeight = !Number.isNaN(savedH) ? savedH : 190;
|
const targetHeight = !Number.isNaN(savedH) ? savedH : 190;
|
||||||
|
|
||||||
// Make sure the top of the ghost never goes above SAFE_TOP
|
|
||||||
const startTop = Math.max(SAFE_TOP, fromCy - targetHeight / 2);
|
const startTop = Math.max(SAFE_TOP, fromCy - targetHeight / 2);
|
||||||
|
|
||||||
// Build a rect for our ghost and use createCardGhost so we KEEP bg/border/shadow.
|
|
||||||
const ghostRect = {
|
const ghostRect = {
|
||||||
left: fromCx - targetWidth / 2,
|
left: fromCx - targetWidth / 2,
|
||||||
top: startTop,
|
top: startTop,
|
||||||
@@ -564,13 +567,12 @@ function animateCardsOutOfHeaderThen(done) {
|
|||||||
height: targetHeight
|
height: targetHeight
|
||||||
};
|
};
|
||||||
|
|
||||||
const ghost = createCardGhost(card, ghostRect, { scale: 0.7, opacity: 0 });
|
const ghost = createCardGhost(card, ghostRect, { scale: 0.75, opacity: 0.25 });
|
||||||
ghost.id = card.id + '-ghost-expand';
|
ghost.id = card.id + '-ghost-expand';
|
||||||
ghost.classList.add('card-expand-ghost');
|
ghost.classList.add('card-expand-ghost');
|
||||||
|
|
||||||
// Override transform/transition for our flight animation
|
ghost.style.transform = 'translate(0,0) scale(0.75)';
|
||||||
ghost.style.transform = 'translate(0,0) scale(0.7)';
|
ghost.style.transition = 'transform 0.4s cubic-bezier(.22,.61,.36,1), opacity 0.4s linear';
|
||||||
ghost.style.transition = 'transform 0.25s ease-out, opacity 0.25s ease-out';
|
|
||||||
|
|
||||||
document.body.appendChild(ghost);
|
document.body.appendChild(ghost);
|
||||||
ghosts.push({
|
ghosts.push({
|
||||||
@@ -586,7 +588,6 @@ function animateCardsOutOfHeaderThen(done) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kick off the flight on the next frame
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
ghosts.forEach(({ ghost, from, to }) => {
|
ghosts.forEach(({ ghost, from, to }) => {
|
||||||
const dx = to.cx - from.cx;
|
const dx = to.cx - from.cx;
|
||||||
@@ -596,13 +597,12 @@ function animateCardsOutOfHeaderThen(done) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clean up ghosts and then do real layout restore
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
ghosts.forEach(({ ghost }) => {
|
ghosts.forEach(({ ghost }) => {
|
||||||
try { ghost.remove(); } catch {}
|
try { ghost.remove(); } catch {}
|
||||||
});
|
});
|
||||||
done();
|
done();
|
||||||
}, 280); // just over the 0.25s transition
|
}, 430);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- zones toggle (collapse to header) --------------------
|
// -------------------- zones toggle (collapse to header) --------------------
|
||||||
|
|||||||
@@ -721,17 +721,31 @@ async function fetchFolderPeek(folder) {
|
|||||||
/* ===========================================================
|
/* ===========================================================
|
||||||
SECURITY: build file URLs only via the API (no /uploads)
|
SECURITY: build file URLs only via the API (no /uploads)
|
||||||
=========================================================== */
|
=========================================================== */
|
||||||
function apiFileUrl(folder, name, inline = false) {
|
function apiFileUrl(folder, name, inline = false) {
|
||||||
const f = folder && folder !== "root" ? folder : "root";
|
const fParam = folder && folder !== "root" ? folder : "root";
|
||||||
const q = new URLSearchParams({
|
const q = new URLSearchParams({
|
||||||
folder: f,
|
folder: fParam,
|
||||||
file: name,
|
file: name,
|
||||||
inline: inline ? "1" : "0",
|
inline: inline ? "1" : "0"
|
||||||
t: String(Date.now()) // cache-bust
|
});
|
||||||
});
|
|
||||||
return `/api/file/download.php?${q.toString()}`;
|
// Try to find this file in fileData to get a stable cache key
|
||||||
}
|
try {
|
||||||
|
if (Array.isArray(fileData)) {
|
||||||
|
const meta = fileData.find(
|
||||||
|
f => f.name === name && (f.folder || "root") === fParam
|
||||||
|
);
|
||||||
|
if (meta) {
|
||||||
|
const v = meta.cacheKey || meta.modified || meta.uploaded || meta.sizeBytes;
|
||||||
|
if (v != null && v !== "") {
|
||||||
|
q.set("t", String(v)); // stable per-file token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch { /* best-effort only */ }
|
||||||
|
|
||||||
|
return `/api/file/download.php?${q.toString()}`;
|
||||||
|
}
|
||||||
// Wire "select all" header checkbox for the current table render
|
// Wire "select all" header checkbox for the current table render
|
||||||
function wireSelectAll(fileListContent) {
|
function wireSelectAll(fileListContent) {
|
||||||
// Be flexible about how the header checkbox is identified
|
// Be flexible about how the header checkbox is identified
|
||||||
@@ -915,20 +929,31 @@ fetchFolderPeek(folderPath).then(result => {
|
|||||||
// ======================
|
// ======================
|
||||||
// FILE HOVER PREVIEW
|
// FILE HOVER PREVIEW
|
||||||
// ======================
|
// ======================
|
||||||
const name = row.getAttribute("data-file-name") || "";
|
const name = row.getAttribute("data-file-name");
|
||||||
const file = fileData.find(f => f.name === name) || null;
|
|
||||||
|
// If this row isn't a real file row (e.g. "No files found"), don't show hover preview.
|
||||||
|
if (!name) {
|
||||||
|
hoverPreviewContext = null;
|
||||||
|
hideHoverPreview();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = Array.isArray(fileData)
|
||||||
|
? fileData.find(f => f.name === name)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// If we can't resolve a real file from fileData, also skip the preview
|
||||||
|
if (!file) {
|
||||||
|
hoverPreviewContext = null;
|
||||||
|
hideHoverPreview();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
hoverPreviewContext = {
|
hoverPreviewContext = {
|
||||||
type: "file",
|
type: "file",
|
||||||
file
|
file
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!file) {
|
|
||||||
titleEl.textContent = name || "(unknown)";
|
|
||||||
metaEl.textContent = "";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
titleEl.textContent = file.name;
|
titleEl.textContent = file.name;
|
||||||
|
|
||||||
// IMPORTANT: no duplicate "size • modified • owner" under the title
|
// IMPORTANT: no duplicate "size • modified • owner" under the title
|
||||||
@@ -977,8 +1002,17 @@ fetchFolderPeek(folderPath).then(result => {
|
|||||||
if (ext) {
|
if (ext) {
|
||||||
props.push(`<div class="hover-prop-line"><strong>${t("extension") || "Ext"}:</strong> .${escapeHTML(ext)}</div>`);
|
props.push(`<div class="hover-prop-line"><strong>${t("extension") || "Ext"}:</strong> .${escapeHTML(ext)}</div>`);
|
||||||
}
|
}
|
||||||
if (file.size) {
|
if (Number.isFinite(file.sizeBytes) && file.sizeBytes >= 0) {
|
||||||
props.push(`<div class="hover-prop-line"><strong>${t("size") || "Size"}:</strong> ${escapeHTML(file.size)}</div>`);
|
const prettySize = formatSize(file.sizeBytes);
|
||||||
|
props.push(`
|
||||||
|
<div class="hover-prop-line hover-prop-size">
|
||||||
|
<strong>${t("size") || "Size"}:</strong>
|
||||||
|
<span class="hover-prop-value"
|
||||||
|
style="margin-left:4px; font-variant-numeric:tabular-nums;">
|
||||||
|
${escapeHTML(prettySize)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
}
|
}
|
||||||
if (file.modified) {
|
if (file.modified) {
|
||||||
props.push(`<div class="hover-prop-line"><strong>${t("modified") || "Modified"}:</strong> ${escapeHTML(file.modified)}</div>`);
|
props.push(`<div class="hover-prop-line"><strong>${t("modified") || "Modified"}:</strong> ${escapeHTML(file.modified)}</div>`);
|
||||||
@@ -1325,14 +1359,16 @@ function parseSizeToBytes(sizeStr) {
|
|||||||
* Format the total bytes as a human-readable string.
|
* Format the total bytes as a human-readable string.
|
||||||
*/
|
*/
|
||||||
function formatSize(totalBytes) {
|
function formatSize(totalBytes) {
|
||||||
|
if (!Number.isFinite(totalBytes) || totalBytes < 0) return "";
|
||||||
|
|
||||||
if (totalBytes < 1024) {
|
if (totalBytes < 1024) {
|
||||||
return totalBytes + " Bytes";
|
return totalBytes + " B";
|
||||||
} else if (totalBytes < 1024 * 1024) {
|
} else if (totalBytes < 1024 * 1024) {
|
||||||
return (totalBytes / 1024).toFixed(2) + " KB";
|
return (totalBytes / 1024).toFixed(1) + " KB";
|
||||||
} else if (totalBytes < 1024 * 1024 * 1024) {
|
} else if (totalBytes < 1024 * 1024 * 1024) {
|
||||||
return (totalBytes / (1024 * 1024)).toFixed(2) + " MB";
|
return (totalBytes / (1024 * 1024)).toFixed(1) + " MB";
|
||||||
} else {
|
} else {
|
||||||
return (totalBytes / (1024 * 1024 * 1024)).toFixed(2) + " GB";
|
return (totalBytes / (1024 * 1024 * 1024)).toFixed(1) + " GB";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1591,18 +1627,30 @@ export async function loadFileList(folderParam) {
|
|||||||
? f.sizeBytes
|
? f.sizeBytes
|
||||||
: parseSizeToBytes(String(f.size || ""));
|
: parseSizeToBytes(String(f.size || ""));
|
||||||
|
|
||||||
// If we can't parse a sane size, treat as "unknown" instead of Infinity
|
|
||||||
if (!Number.isFinite(bytes) || bytes < 0) {
|
if (!Number.isFinite(bytes) || bytes < 0) {
|
||||||
bytes = null;
|
bytes = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
f.sizeBytes = bytes;
|
f.sizeBytes = bytes;
|
||||||
|
|
||||||
|
// New: normalize display size and create a stable cache key
|
||||||
|
if (bytes != null) {
|
||||||
|
f.size = formatSize(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheKey =
|
||||||
|
(f.modified && String(f.modified)) ||
|
||||||
|
(f.uploaded && String(f.uploaded)) ||
|
||||||
|
(bytes != null ? String(bytes) : "") ||
|
||||||
|
f.name;
|
||||||
|
|
||||||
|
f.cacheKey = cacheKey;
|
||||||
|
f.folder = folder;
|
||||||
|
|
||||||
// For editing: if size is unknown, assume it's OK and let the editor enforce limits.
|
// For editing: if size is unknown, assume it's OK and let the editor enforce limits.
|
||||||
const safeForEdit = (bytes == null) || (bytes <= MAX_EDIT_BYTES);
|
const safeForEdit = (bytes == null) || (bytes <= MAX_EDIT_BYTES);
|
||||||
f.editable = canEditFile(f.name) && safeForEdit;
|
f.editable = canEditFile(f.name) && safeForEdit;
|
||||||
|
|
||||||
f.folder = folder;
|
|
||||||
return f;
|
return f;
|
||||||
});
|
});
|
||||||
fileData = data.files;
|
fileData = data.files;
|
||||||
@@ -1676,7 +1724,7 @@ export async function loadFileList(folderParam) {
|
|||||||
<label for="rowHeightSlider" style="margin-right:8px;line-height:1;">
|
<label for="rowHeightSlider" style="margin-right:8px;line-height:1;">
|
||||||
${t("row_height")}:
|
${t("row_height")}:
|
||||||
</label>
|
</label>
|
||||||
<input type="range" id="rowHeightSlider" min="30" max="60" value="${currentHeight}" style="vertical-align:middle;">
|
<input type="range" id="rowHeightSlider" min="20" max="60" value="${currentHeight}" style="vertical-align:middle;">
|
||||||
<span id="rowHeightValue" style="margin-left:6px;line-height:1;">${currentHeight}px</span>
|
<span id="rowHeightValue" style="margin-left:6px;line-height:1;">${currentHeight}px</span>
|
||||||
`;
|
`;
|
||||||
const rowSlider = document.getElementById("rowHeightSlider");
|
const rowSlider = document.getElementById("rowHeightSlider");
|
||||||
@@ -1907,6 +1955,9 @@ if (headerClass) {
|
|||||||
} else if (i === sizeIdx) {
|
} else if (i === sizeIdx) {
|
||||||
td.classList.add("folder-size-cell");
|
td.classList.add("folder-size-cell");
|
||||||
td.textContent = "…"; // placeholder until we load stats
|
td.textContent = "…"; // placeholder until we load stats
|
||||||
|
// NEW: match file-row numeric alignment
|
||||||
|
td.style.textAlign = "right";
|
||||||
|
td.style.fontVariantNumeric = "tabular-nums";
|
||||||
|
|
||||||
// 4) uploader / owner column
|
// 4) uploader / owner column
|
||||||
} else if (i === uploaderIdx) {
|
} else if (i === uploaderIdx) {
|
||||||
@@ -2159,24 +2210,19 @@ function syncFolderIconSizeToRowHeight() {
|
|||||||
const raw = cs.getPropertyValue('--file-row-height') || '48px';
|
const raw = cs.getPropertyValue('--file-row-height') || '48px';
|
||||||
const rowH = parseInt(raw, 10) || 60;
|
const rowH = parseInt(raw, 10) || 60;
|
||||||
|
|
||||||
const FUDGE = 5;
|
const FUDGE = 1;
|
||||||
const MAX_GROWTH_ROW = 44; // after this, stop growing the icon
|
const MAX_GROWTH_ROW = 44; // after this, stop growing the icon
|
||||||
|
|
||||||
const BASE_ROW_FOR_OFFSET = 40; // where icon looks centered
|
const BASE_ROW_FOR_OFFSET = 40; // where icon looks centered
|
||||||
const OFFSET_FACTOR = 0.25;
|
const OFFSET_FACTOR = 0.25;
|
||||||
|
|
||||||
// cap growth for size, like you already do
|
|
||||||
const effectiveRow = Math.min(rowH, MAX_GROWTH_ROW);
|
const effectiveRow = Math.min(rowH, MAX_GROWTH_ROW);
|
||||||
|
|
||||||
const boxSize = Math.max(25, Math.min(35, effectiveRow - 20 + FUDGE));
|
const boxSize = Math.max(20, Math.min(35, effectiveRow - 20 + FUDGE));
|
||||||
const scale = 1.20;
|
const scale = 1.20;
|
||||||
|
|
||||||
// use your existing offset curve
|
// use existing offset curve
|
||||||
const clampedForOffset = Math.max(30, Math.min(60, rowH));
|
const clampedForOffset = Math.max(30, Math.min(60, rowH));
|
||||||
let offsetY = (clampedForOffset - BASE_ROW_FOR_OFFSET) * OFFSET_FACTOR;
|
let offsetY = (clampedForOffset - BASE_ROW_FOR_OFFSET) * OFFSET_FACTOR;
|
||||||
|
|
||||||
// 30–44: untouched (you said this range is perfect)
|
|
||||||
// 45–60: same curve, but shifted up slightly
|
|
||||||
if (rowH > 53) {
|
if (rowH > 53) {
|
||||||
offsetY -= 3;
|
offsetY -= 3;
|
||||||
}
|
}
|
||||||
@@ -2196,6 +2242,77 @@ function syncFolderIconSizeToRowHeight() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function sortSubfoldersForCurrentOrder(subfolders) {
|
||||||
|
const base = Array.isArray(subfolders) ? [...subfolders] : [];
|
||||||
|
if (!base.length) return base;
|
||||||
|
|
||||||
|
const col = sortOrder?.column || "uploaded";
|
||||||
|
const ascending = sortOrder?.ascending !== false;
|
||||||
|
const dir = ascending ? 1 : -1;
|
||||||
|
|
||||||
|
// Name sort (A–Z / Z–A)
|
||||||
|
if (col === "name") {
|
||||||
|
base.sort((a, b) => {
|
||||||
|
const n1 = (a.name || "").toLowerCase();
|
||||||
|
const n2 = (b.name || "").toLowerCase();
|
||||||
|
if (n1 < n2) return -1 * dir;
|
||||||
|
if (n1 > n2) return 1 * dir;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size sort – use folder stats (bytes); keep folders as a block above files
|
||||||
|
if (col === "size" || col === "filesize") {
|
||||||
|
const statsList = await Promise.all(
|
||||||
|
base.map(sf => fetchFolderStats(sf.full).catch(() => null))
|
||||||
|
);
|
||||||
|
|
||||||
|
const decorated = base.map((sf, idx) => {
|
||||||
|
const stats = statsList[idx];
|
||||||
|
let bytes = 0;
|
||||||
|
|
||||||
|
if (stats) {
|
||||||
|
const candidates = [
|
||||||
|
stats.bytes,
|
||||||
|
stats.sizeBytes,
|
||||||
|
stats.size,
|
||||||
|
stats.totalBytes
|
||||||
|
];
|
||||||
|
for (const v of candidates) {
|
||||||
|
const n = Number(v);
|
||||||
|
if (Number.isFinite(n) && n >= 0) {
|
||||||
|
bytes = n;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { sf, bytes };
|
||||||
|
});
|
||||||
|
|
||||||
|
decorated.sort((a, b) => {
|
||||||
|
if (a.bytes < b.bytes) return -1 * dir;
|
||||||
|
if (a.bytes > b.bytes) return 1 * dir;
|
||||||
|
|
||||||
|
// tie-break by name
|
||||||
|
const n1 = (a.sf.name || "").toLowerCase();
|
||||||
|
const n2 = (b.sf.name || "").toLowerCase();
|
||||||
|
if (n1 < n2) return -1 * dir;
|
||||||
|
if (n1 > n2) return 1 * dir;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return decorated.map(d => d.sf);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: keep folders A–Z by name regardless of other sorts
|
||||||
|
base.sort((a, b) =>
|
||||||
|
(a.name || "").localeCompare(b.name || "", undefined, { sensitivity: "base" })
|
||||||
|
);
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
async function openDefaultFileFromHover(file) {
|
async function openDefaultFileFromHover(file) {
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
const folder = file.folder || window.currentFolder || "root";
|
const folder = file.folder || window.currentFolder || "root";
|
||||||
@@ -2219,7 +2336,7 @@ async function openDefaultFileFromHover(file) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
export function renderFileTable(folder, container, subfolders) {
|
export async function renderFileTable(folder, container, subfolders) {
|
||||||
const fileListContent = container || document.getElementById("fileList");
|
const fileListContent = container || document.getElementById("fileList");
|
||||||
const searchTerm = (window.currentSearchTerm || "").toLowerCase();
|
const searchTerm = (window.currentSearchTerm || "").toLowerCase();
|
||||||
const itemsPerPageSetting = parseInt(localStorage.getItem("itemsPerPage") || "50", 10);
|
const itemsPerPageSetting = parseInt(localStorage.getItem("itemsPerPage") || "50", 10);
|
||||||
@@ -2230,11 +2347,11 @@ export function renderFileTable(folder, container, subfolders) {
|
|||||||
|
|
||||||
// Inline folders: sort once (Explorer-style A→Z)
|
// Inline folders: sort once (Explorer-style A→Z)
|
||||||
const allSubfolders = Array.isArray(window.currentSubfolders)
|
const allSubfolders = Array.isArray(window.currentSubfolders)
|
||||||
? window.currentSubfolders
|
? window.currentSubfolders
|
||||||
: [];
|
: [];
|
||||||
const subfoldersSorted = [...allSubfolders].sort((a, b) =>
|
|
||||||
(a.name || "").localeCompare(b.name || "", undefined, { sensitivity: "base" })
|
// NEW: sort folders according to current sort order (name / size)
|
||||||
);
|
const subfoldersSorted = await sortSubfoldersForCurrentOrder(allSubfolders);
|
||||||
|
|
||||||
const totalFiles = filteredFiles.length;
|
const totalFiles = filteredFiles.length;
|
||||||
const totalFolders = subfoldersSorted.length;
|
const totalFolders = subfoldersSorted.length;
|
||||||
@@ -2333,6 +2450,28 @@ export function renderFileTable(folder, container, subfolders) {
|
|||||||
|
|
||||||
fileListContent.innerHTML = combinedTopHTML + headerHTML + rowsHTML + bottomControlsHTML;
|
fileListContent.innerHTML = combinedTopHTML + headerHTML + rowsHTML + bottomControlsHTML;
|
||||||
|
|
||||||
|
(function rightAlignSizeColumn() {
|
||||||
|
const table = fileListContent.querySelector("table.filr-table");
|
||||||
|
if (!table || !table.tHead || !table.tBodies.length) return;
|
||||||
|
|
||||||
|
const headerCells = Array.from(table.tHead.querySelectorAll("th"));
|
||||||
|
const sizeIdx = headerCells.findIndex(th =>
|
||||||
|
(th.dataset && (th.dataset.column === "size" || th.dataset.column === "filesize")) ||
|
||||||
|
/\bsize\b/i.test((th.textContent || "").trim())
|
||||||
|
);
|
||||||
|
if (sizeIdx < 0) return;
|
||||||
|
|
||||||
|
// Header
|
||||||
|
headerCells[sizeIdx].style.textAlign = "right";
|
||||||
|
|
||||||
|
// Body cells
|
||||||
|
Array.from(table.tBodies[0].rows).forEach(row => {
|
||||||
|
if (sizeIdx >= row.cells.length) return;
|
||||||
|
row.cells[sizeIdx].style.textAlign = "right";
|
||||||
|
row.cells[sizeIdx].style.fontVariantNumeric = "tabular-nums";
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
// ---- MOBILE FIX: show "Size" column for files (Name | Size | Actions) ----
|
// ---- MOBILE FIX: show "Size" column for files (Name | Size | Actions) ----
|
||||||
(function fixMobileFileSizeColumn() {
|
(function fixMobileFileSizeColumn() {
|
||||||
const isMobile = window.innerWidth <= 640;
|
const isMobile = window.innerWidth <= 640;
|
||||||
@@ -2387,6 +2526,52 @@ if (window.showInlineFolders !== false && pageFolders.length) {
|
|||||||
injectInlineFolderRows(fileListContent, folder, pageFolders);
|
injectInlineFolderRows(fileListContent, folder, pageFolders);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Right-align meta columns: created / modified / owner
|
||||||
|
(function rightAlignMetaColumns() {
|
||||||
|
const table = fileListContent.querySelector("table.filr-table");
|
||||||
|
if (!table || !table.tHead || !table.tBodies.length) return;
|
||||||
|
|
||||||
|
const headerCells = Array.from(table.tHead.querySelectorAll("th"));
|
||||||
|
const bodyRows = Array.from(table.tBodies[0].rows);
|
||||||
|
|
||||||
|
function alignCol(matchFn, numeric = true) {
|
||||||
|
const idx = headerCells.findIndex(matchFn);
|
||||||
|
if (idx < 0) return;
|
||||||
|
|
||||||
|
const th = headerCells[idx];
|
||||||
|
th.style.textAlign = "right";
|
||||||
|
|
||||||
|
bodyRows.forEach(row => {
|
||||||
|
if (idx >= row.cells.length) return;
|
||||||
|
const td = row.cells[idx];
|
||||||
|
if (!td) return;
|
||||||
|
td.style.textAlign = "right";
|
||||||
|
if (numeric) {
|
||||||
|
td.style.fontVariantNumeric = "tabular-nums";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uploaded / Created
|
||||||
|
alignCol(th =>
|
||||||
|
(th.dataset && (th.dataset.column === "uploaded" || th.dataset.column === "created")) ||
|
||||||
|
/\b(uploaded|created)\b/i.test((th.textContent || "").trim())
|
||||||
|
);
|
||||||
|
|
||||||
|
// Modified
|
||||||
|
alignCol(th =>
|
||||||
|
(th.dataset && th.dataset.column === "modified") ||
|
||||||
|
/\bmodified\b/i.test((th.textContent || "").trim())
|
||||||
|
);
|
||||||
|
|
||||||
|
// Owner / Uploader
|
||||||
|
alignCol(th =>
|
||||||
|
(th.dataset && th.dataset.column === "uploader") ||
|
||||||
|
/\b(owner|uploader)\b/i.test((th.textContent || "").trim()),
|
||||||
|
/* numeric = */ false // names aren't numbers, but right-align anyway
|
||||||
|
);
|
||||||
|
})();
|
||||||
|
|
||||||
// Now wire 3-dot ellipsis so it also picks up folder rows
|
// Now wire 3-dot ellipsis so it also picks up folder rows
|
||||||
wireEllipsisContextMenu(fileListContent);
|
wireEllipsisContextMenu(fileListContent);
|
||||||
|
|
||||||
@@ -2694,8 +2879,7 @@ export function renderGalleryView(folder, container) {
|
|||||||
pageFiles.forEach((file, idx) => {
|
pageFiles.forEach((file, idx) => {
|
||||||
const idSafe = encodeURIComponent(file.name) + "-" + (startIdx + idx);
|
const idSafe = encodeURIComponent(file.name) + "-" + (startIdx + idx);
|
||||||
|
|
||||||
// build preview URL from API (cache-busted)
|
const previewURL = apiFileUrl(folder, file.name, true);
|
||||||
const previewURL = `${apiBase}${encodeURIComponent(file.name)}&t=${Date.now()}`;
|
|
||||||
|
|
||||||
// thumbnail
|
// thumbnail
|
||||||
let thumbnail;
|
let thumbnail;
|
||||||
@@ -2989,10 +3173,16 @@ export function sortFiles(column, folder) {
|
|||||||
sortOrder.column = column;
|
sortOrder.column = column;
|
||||||
sortOrder.ascending = true;
|
sortOrder.ascending = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
fileData.sort((a, b) => {
|
fileData.sort((a, b) => {
|
||||||
let valA = a[column] || "";
|
let valA = a[column] || "";
|
||||||
let valB = b[column] || "";
|
let valB = b[column] || "";
|
||||||
if (column === "modified" || column === "uploaded") {
|
|
||||||
|
if (column === "size" || column === "filesize") {
|
||||||
|
// numeric size
|
||||||
|
valA = Number.isFinite(a.sizeBytes) ? a.sizeBytes : 0;
|
||||||
|
valB = Number.isFinite(b.sizeBytes) ? b.sizeBytes : 0;
|
||||||
|
} else if (column === "modified" || column === "uploaded") {
|
||||||
const parsedA = parseCustomDate(valA);
|
const parsedA = parseCustomDate(valA);
|
||||||
const parsedB = parseCustomDate(valB);
|
const parsedB = parseCustomDate(valB);
|
||||||
valA = parsedA;
|
valA = parsedA;
|
||||||
@@ -3001,10 +3191,12 @@ export function sortFiles(column, folder) {
|
|||||||
valA = valA.toLowerCase();
|
valA = valA.toLowerCase();
|
||||||
valB = valB.toLowerCase();
|
valB = valB.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (valA < valB) return sortOrder.ascending ? -1 : 1;
|
if (valA < valB) return sortOrder.ascending ? -1 : 1;
|
||||||
if (valA > valB) return sortOrder.ascending ? 1 : -1;
|
if (valA > valB) return sortOrder.ascending ? 1 : -1;
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (window.viewMode === "gallery") {
|
if (window.viewMode === "gallery") {
|
||||||
renderGalleryView(folder);
|
renderGalleryView(folder);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -187,6 +187,7 @@ const translations = {
|
|||||||
|
|
||||||
// Admin Panel
|
// Admin Panel
|
||||||
"header_settings": "Header Settings",
|
"header_settings": "Header Settings",
|
||||||
|
"header_footer_settings": "Header & Footer Settings",
|
||||||
"shared_max_upload_size_bytes_title": "Shared Max Upload Size",
|
"shared_max_upload_size_bytes_title": "Shared Max Upload Size",
|
||||||
"shared_max_upload_size_bytes": "Shared Max Upload Size (bytes)",
|
"shared_max_upload_size_bytes": "Shared Max Upload Size (bytes)",
|
||||||
"max_bytes_shared_uploads_note": "Enter maximum bytes allowed for shared-folder uploads",
|
"max_bytes_shared_uploads_note": "Enter maximum bytes allowed for shared-folder uploads",
|
||||||
|
|||||||
@@ -445,107 +445,127 @@ function bindDarkMode() {
|
|||||||
m.content = val;
|
m.content = val;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------- site config / auth ----------
|
// ---------- site config / auth ----------
|
||||||
function applySiteConfig(cfg, { phase = 'final' } = {}) {
|
function applySiteConfig(cfg, { phase = 'final' } = {}) {
|
||||||
try {
|
|
||||||
const title = (cfg && cfg.header_title) ? String(cfg.header_title) : 'FileRise';
|
|
||||||
|
|
||||||
// Always keep <title> correct early (no visual flicker)
|
|
||||||
document.title = title;
|
|
||||||
// --- Header logo (branding) in BOTH phases ---
|
|
||||||
try {
|
try {
|
||||||
const branding = (cfg && cfg.branding) ? cfg.branding : {};
|
const title = (cfg && cfg.header_title) ? String(cfg.header_title) : 'FileRise';
|
||||||
const customLogoUrl = branding.customLogoUrl || "";
|
|
||||||
const logoImg = document.querySelector('.header-logo img');
|
// Always keep <title> correct early (no visual flicker)
|
||||||
if (logoImg) {
|
document.title = title;
|
||||||
if (customLogoUrl) {
|
|
||||||
logoImg.setAttribute('src', customLogoUrl);
|
// --- Header logo (branding) in BOTH phases ---
|
||||||
logoImg.setAttribute('alt', 'Site logo');
|
try {
|
||||||
|
const branding = (cfg && cfg.branding) ? cfg.branding : {};
|
||||||
|
const customLogoUrl = branding.customLogoUrl || "";
|
||||||
|
const logoImg = document.querySelector('.header-logo img');
|
||||||
|
if (logoImg) {
|
||||||
|
if (customLogoUrl) {
|
||||||
|
logoImg.setAttribute('src', customLogoUrl);
|
||||||
|
logoImg.setAttribute('alt', 'Site logo');
|
||||||
|
} else {
|
||||||
|
// fall back to default FileRise logo
|
||||||
|
logoImg.setAttribute('src', '/assets/logo.svg?v={{APP_QVER}}');
|
||||||
|
logoImg.setAttribute('alt', 'FileRise');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// non-fatal; ignore branding issues
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Header colors (branding) in BOTH phases ---
|
||||||
|
try {
|
||||||
|
const branding = (cfg && cfg.branding) ? cfg.branding : {};
|
||||||
|
const root = document.documentElement;
|
||||||
|
|
||||||
|
const light = branding.headerBgLight || '';
|
||||||
|
const dark = branding.headerBgDark || '';
|
||||||
|
|
||||||
|
if (light) root.style.setProperty('--header-bg-light', light);
|
||||||
|
else root.style.removeProperty('--header-bg-light');
|
||||||
|
|
||||||
|
if (dark) root.style.setProperty('--header-bg-dark', dark);
|
||||||
|
else root.style.removeProperty('--header-bg-dark');
|
||||||
|
} catch (e) {
|
||||||
|
// non-fatal
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Footer HTML (branding) in BOTH phases ---
|
||||||
|
try {
|
||||||
|
const branding = (cfg && cfg.branding) ? cfg.branding : {};
|
||||||
|
const footerEl = document.getElementById('siteFooter');
|
||||||
|
if (footerEl) {
|
||||||
|
const html = (branding.footerHtml || '').trim();
|
||||||
|
if (html) {
|
||||||
|
// allow simple HTML from config
|
||||||
|
footerEl.innerHTML = html;
|
||||||
|
} else {
|
||||||
|
const year = new Date().getFullYear();
|
||||||
|
footerEl.innerHTML =
|
||||||
|
`© ${year} <a href="https://filerise.net" target="_blank" rel="noopener noreferrer">FileRise</a>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// non-fatal
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Login options (apply in BOTH phases so login page is correct) ---
|
||||||
|
const lo = (cfg && cfg.loginOptions) ? cfg.loginOptions : {};
|
||||||
|
|
||||||
|
// be tolerant to key variants just in case
|
||||||
|
const disableForm = !!(lo.disableFormLogin ?? lo.disable_form_login ?? lo.disableForm);
|
||||||
|
const disableOIDC = !!(lo.disableOIDCLogin ?? lo.disable_oidc_login ?? lo.disableOIDC);
|
||||||
|
const disableBasic = !!(lo.disableBasicAuth ?? lo.disable_basic_auth ?? lo.disableBasic);
|
||||||
|
|
||||||
|
const showForm = !disableForm;
|
||||||
|
const showOIDC = !disableOIDC;
|
||||||
|
const showBasic = !disableBasic;
|
||||||
|
|
||||||
|
const loginWrap = $('#loginForm'); // outer wrapper that contains buttons + form
|
||||||
|
const authForm = $('#authForm'); // inner username/password form
|
||||||
|
const oidcBtn = $('#oidcLoginBtn'); // OIDC button
|
||||||
|
const basicLink = document.querySelector('a[href="/api/auth/login_basic.php"]');
|
||||||
|
|
||||||
|
// 1) Show the wrapper if ANY method is enabled (form OR OIDC OR basic)
|
||||||
|
if (loginWrap) {
|
||||||
|
const anyMethod = showForm || showOIDC || showBasic;
|
||||||
|
if (anyMethod) {
|
||||||
|
loginWrap.removeAttribute('hidden'); // remove [hidden], which beats display:
|
||||||
|
loginWrap.style.display = ''; // let CSS decide
|
||||||
} else {
|
} else {
|
||||||
// fall back to default FileRise logo
|
loginWrap.setAttribute('hidden', '');
|
||||||
logoImg.setAttribute('src', '/assets/logo.svg?v={{APP_QVER}}');
|
loginWrap.style.display = '';
|
||||||
logoImg.setAttribute('alt', 'FileRise');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
// non-fatal; ignore branding issues
|
// 2) Toggle the pieces inside the wrapper
|
||||||
}
|
if (authForm) authForm.style.display = showForm ? '' : 'none';
|
||||||
// --- Header colors (branding) in BOTH phases ---
|
if (oidcBtn) oidcBtn.style.display = showOIDC ? '' : 'none';
|
||||||
try {
|
if (basicLink) basicLink.style.display = showBasic ? '' : 'none';
|
||||||
const branding = (cfg && cfg.branding) ? cfg.branding : {};
|
const oidc = $('#oidcLoginBtn'); if (oidc) oidc.style.display = disableOIDC ? 'none' : '';
|
||||||
const root = document.documentElement;
|
const basic = document.querySelector('a[href="/api/auth/login_basic.php"]');
|
||||||
|
if (basic) basic.style.display = disableBasic ? 'none' : '';
|
||||||
const light = branding.headerBgLight || '';
|
|
||||||
const dark = branding.headerBgDark || '';
|
// --- Header <h1> only in the FINAL phase (prevents visible flips) ---
|
||||||
|
if (phase === 'final') {
|
||||||
if (light) root.style.setProperty('--header-bg-light', light);
|
const h1 = document.querySelector('.header-title h1');
|
||||||
else root.style.removeProperty('--header-bg-light');
|
if (h1) {
|
||||||
|
// prevent i18n or legacy from overwriting it
|
||||||
if (dark) root.style.setProperty('--header-bg-dark', dark);
|
if (h1.hasAttribute('data-i18n-key')) h1.removeAttribute('data-i18n-key');
|
||||||
else root.style.removeProperty('--header-bg-dark');
|
|
||||||
} catch (e) {
|
if (h1.textContent !== title) h1.textContent = title;
|
||||||
// non-fatal
|
|
||||||
}
|
// lock it so late code can't stomp it
|
||||||
|
if (!h1.__titleLock) {
|
||||||
// --- Login options (apply in BOTH phases so login page is correct) ---
|
const mo = new MutationObserver(() => {
|
||||||
const lo = (cfg && cfg.loginOptions) ? cfg.loginOptions : {};
|
if (h1.textContent !== title) h1.textContent = title;
|
||||||
|
});
|
||||||
|
mo.observe(h1, { childList: true, characterData: true, subtree: true });
|
||||||
// be tolerant to key variants just in case
|
h1.__titleLock = mo;
|
||||||
const disableForm = !!(lo.disableFormLogin ?? lo.disable_form_login ?? lo.disableForm);
|
}
|
||||||
const disableOIDC = !!(lo.disableOIDCLogin ?? lo.disable_oidc_login ?? lo.disableOIDC);
|
|
||||||
const disableBasic = !!(lo.disableBasicAuth ?? lo.disable_basic_auth ?? lo.disableBasic);
|
|
||||||
|
|
||||||
const showForm = !disableForm;
|
|
||||||
const showOIDC = !disableOIDC;
|
|
||||||
const showBasic = !disableBasic;
|
|
||||||
|
|
||||||
const loginWrap = $('#loginForm'); // outer wrapper that contains buttons + form
|
|
||||||
const authForm = $('#authForm'); // inner username/password form
|
|
||||||
const oidcBtn = $('#oidcLoginBtn'); // OIDC button
|
|
||||||
const basicLink = document.querySelector('a[href="/api/auth/login_basic.php"]');
|
|
||||||
|
|
||||||
// 1) Show the wrapper if ANY method is enabled (form OR OIDC OR basic)
|
|
||||||
if (loginWrap) {
|
|
||||||
const anyMethod = showForm || showOIDC || showBasic;
|
|
||||||
if (anyMethod) {
|
|
||||||
loginWrap.removeAttribute('hidden'); // remove [hidden], which beats display:
|
|
||||||
loginWrap.style.display = ''; // let CSS decide
|
|
||||||
} else {
|
|
||||||
loginWrap.setAttribute('hidden', '');
|
|
||||||
loginWrap.style.display = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) Toggle the pieces inside the wrapper
|
|
||||||
if (authForm) authForm.style.display = showForm ? '' : 'none';
|
|
||||||
if (oidcBtn) oidcBtn.style.display = showOIDC ? '' : 'none';
|
|
||||||
if (basicLink) basicLink.style.display = showBasic ? '' : 'none';
|
|
||||||
const oidc = $('#oidcLoginBtn'); if (oidc) oidc.style.display = disableOIDC ? 'none' : '';
|
|
||||||
const basic = document.querySelector('a[href="/api/auth/login_basic.php"]');
|
|
||||||
if (basic) basic.style.display = disableBasic ? 'none' : '';
|
|
||||||
|
|
||||||
// --- Header <h1> only in the FINAL phase (prevents visible flips) ---
|
|
||||||
if (phase === 'final') {
|
|
||||||
const h1 = document.querySelector('.header-title h1');
|
|
||||||
if (h1) {
|
|
||||||
// prevent i18n or legacy from overwriting it
|
|
||||||
if (h1.hasAttribute('data-i18n-key')) h1.removeAttribute('data-i18n-key');
|
|
||||||
|
|
||||||
if (h1.textContent !== title) h1.textContent = title;
|
|
||||||
|
|
||||||
// lock it so late code can't stomp it
|
|
||||||
if (!h1.__titleLock) {
|
|
||||||
const mo = new MutationObserver(() => {
|
|
||||||
if (h1.textContent !== title) h1.textContent = title;
|
|
||||||
});
|
|
||||||
mo.observe(h1, { childList: true, characterData: true, subtree: true });
|
|
||||||
h1.__titleLock = mo;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} catch { }
|
||||||
} catch { }
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async function readyToReveal() {
|
async function readyToReveal() {
|
||||||
// Wait for CSS + fonts so the first revealed frame is fully styled
|
// Wait for CSS + fonts so the first revealed frame is fully styled
|
||||||
|
|||||||
@@ -103,6 +103,14 @@ function wireFileInputChange(fileInput) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setUploadButtonVisible(visible) {
|
||||||
|
const btn = document.getElementById('uploadBtn');
|
||||||
|
if (!btn) return;
|
||||||
|
|
||||||
|
btn.style.display = visible ? 'block' : 'none';
|
||||||
|
btn.disabled = !visible;
|
||||||
|
}
|
||||||
|
|
||||||
function getUserDraftContext() {
|
function getUserDraftContext() {
|
||||||
const all = loadResumableDraftsAll();
|
const all = loadResumableDraftsAll();
|
||||||
const userKey = getCurrentUserKey();
|
const userKey = getCurrentUserKey();
|
||||||
@@ -346,6 +354,8 @@ function setDropAreaDefault() {
|
|||||||
const fileInput = dropArea.querySelector('#file');
|
const fileInput = dropArea.querySelector('#file');
|
||||||
wireFileInputChange(fileInput);
|
wireFileInputChange(fileInput);
|
||||||
wireChooseButton();
|
wireChooseButton();
|
||||||
|
|
||||||
|
setUploadButtonVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function adjustFolderHelpExpansion() {
|
function adjustFolderHelpExpansion() {
|
||||||
@@ -464,6 +474,8 @@ function createFileEntry(file) {
|
|||||||
|
|
||||||
li.remove();
|
li.remove();
|
||||||
updateFileInfoCount();
|
updateFileInfoCount();
|
||||||
|
const anyItems = !!document.querySelector('li.upload-progress-item');
|
||||||
|
setUploadButtonVisible(anyItems);
|
||||||
});
|
});
|
||||||
li.removeBtn = removeBtn;
|
li.removeBtn = removeBtn;
|
||||||
li.appendChild(removeBtn);
|
li.appendChild(removeBtn);
|
||||||
@@ -674,6 +686,7 @@ function processFiles(filesInput) {
|
|||||||
|
|
||||||
window.selectedFiles = files;
|
window.selectedFiles = files;
|
||||||
updateFileInfoCount();
|
updateFileInfoCount();
|
||||||
|
setUploadButtonVisible(files.length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -----------------------------------------------------
|
/* -----------------------------------------------------
|
||||||
@@ -770,6 +783,7 @@ async function initResumableUpload() {
|
|||||||
list.appendChild(li);
|
list.appendChild(li);
|
||||||
updateFileInfoCount();
|
updateFileInfoCount();
|
||||||
updateResumableQuery();
|
updateResumableQuery();
|
||||||
|
setUploadButtonVisible(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
resumableInstance.on("fileProgress", function (file) {
|
resumableInstance.on("fileProgress", function (file) {
|
||||||
@@ -931,6 +945,7 @@ async function initResumableUpload() {
|
|||||||
}
|
}
|
||||||
clearResumableDraftsForFolder(window.currentFolder || 'root');
|
clearResumableDraftsForFolder(window.currentFolder || 'root');
|
||||||
showResumableDraftBanner();
|
showResumableDraftBanner();
|
||||||
|
setUploadButtonVisible(false);
|
||||||
}, 5000);
|
}, 5000);
|
||||||
} else {
|
} else {
|
||||||
showToast("Some files failed to upload. Please check the list.");
|
showToast("Some files failed to upload. Please check the list.");
|
||||||
@@ -1183,6 +1198,8 @@ function submitFiles(allFiles) {
|
|||||||
} else {
|
} else {
|
||||||
showToast(`${succeeded} file(s) succeeded. Please check the list.`);
|
showToast(`${succeeded} file(s) succeeded. Please check the list.`);
|
||||||
}
|
}
|
||||||
|
const anyItems = !!document.querySelector('li.upload-progress-item');
|
||||||
|
setUploadButtonVisible(anyItems);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error("Error fetching file list:", error);
|
console.error("Error fetching file list:", error);
|
||||||
@@ -1275,6 +1292,8 @@ function initUpload() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setUploadButtonVisible(false);
|
||||||
|
|
||||||
const hasResumableFiles =
|
const hasResumableFiles =
|
||||||
useResumable &&
|
useResumable &&
|
||||||
resumableInstance &&
|
resumableInstance &&
|
||||||
|
|||||||
@@ -144,6 +144,9 @@ class AdminController
|
|||||||
$proType = $proPayload['type'] ?? null;
|
$proType = $proPayload['type'] ?? null;
|
||||||
$proEmail = $proPayload['email'] ?? null;
|
$proEmail = $proPayload['email'] ?? null;
|
||||||
$proVersion = defined('FR_PRO_BUNDLE_VERSION') ? FR_PRO_BUNDLE_VERSION : null;
|
$proVersion = defined('FR_PRO_BUNDLE_VERSION') ? FR_PRO_BUNDLE_VERSION : null;
|
||||||
|
$proPlan = $proPayload['plan'] ?? null;
|
||||||
|
$proExpiresAt = $proPayload['expiresAt'] ?? null;
|
||||||
|
$proMaxMajor = $proPayload['maxMajor'] ?? null;
|
||||||
|
|
||||||
// Whitelisted public subset only (+ ONLYOFFICE enabled flag)
|
// Whitelisted public subset only (+ ONLYOFFICE enabled flag)
|
||||||
$public = [
|
$public = [
|
||||||
@@ -169,6 +172,7 @@ class AdminController
|
|||||||
'customLogoUrl' => (string)($config['branding']['customLogoUrl'] ?? ''),
|
'customLogoUrl' => (string)($config['branding']['customLogoUrl'] ?? ''),
|
||||||
'headerBgLight' => (string)($config['branding']['headerBgLight'] ?? ''),
|
'headerBgLight' => (string)($config['branding']['headerBgLight'] ?? ''),
|
||||||
'headerBgDark' => (string)($config['branding']['headerBgDark'] ?? ''),
|
'headerBgDark' => (string)($config['branding']['headerBgDark'] ?? ''),
|
||||||
|
'footerHtml' => (string)($config['branding']['footerHtml'] ?? ''),
|
||||||
],
|
],
|
||||||
'pro' => [
|
'pro' => [
|
||||||
'active' => $proActive,
|
'active' => $proActive,
|
||||||
@@ -176,6 +180,9 @@ class AdminController
|
|||||||
'email' => $proEmail,
|
'email' => $proEmail,
|
||||||
'version' => $proVersion,
|
'version' => $proVersion,
|
||||||
'license' => $licenseString,
|
'license' => $licenseString,
|
||||||
|
'plan' => $proPlan,
|
||||||
|
'expiresAt' => $proExpiresAt,
|
||||||
|
'maxMajor' => $proMaxMajor,
|
||||||
],
|
],
|
||||||
'demoMode' => defined('FR_DEMO_MODE') ? (bool)FR_DEMO_MODE : false,
|
'demoMode' => defined('FR_DEMO_MODE') ? (bool)FR_DEMO_MODE : false,
|
||||||
];
|
];
|
||||||
@@ -581,6 +588,28 @@ public function installProBundle(): void
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NEW: normalize to basename so C:\fakepath\FileRisePro-v1.2.1.zip works.
|
||||||
|
$basename = $origName;
|
||||||
|
if ($basename !== '') {
|
||||||
|
// Normalize slashes and then take basename
|
||||||
|
$basename = str_replace('\\', '/', $basename);
|
||||||
|
$basename = basename($basename);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to parse the bundle version from the *basename*
|
||||||
|
// Supports: FileRisePro-v1.2.3.zip or FileRisePro_1.2.3.zip (case-insensitive)
|
||||||
|
$declaredVersion = null;
|
||||||
|
if (
|
||||||
|
$basename !== '' &&
|
||||||
|
preg_match(
|
||||||
|
'/^FileRisePro[_-]v?([0-9]+\.[0-9]+\.[0-9]+)\.zip$/i',
|
||||||
|
$basename,
|
||||||
|
$m
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
$declaredVersion = 'v' . $m[1];
|
||||||
|
}
|
||||||
|
|
||||||
// Prepare temp working dir
|
// Prepare temp working dir
|
||||||
$tempRoot = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR);
|
$tempRoot = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR);
|
||||||
$workDir = $tempRoot . DIRECTORY_SEPARATOR . 'filerise_pro_' . bin2hex(random_bytes(8));
|
$workDir = $tempRoot . DIRECTORY_SEPARATOR . 'filerise_pro_' . bin2hex(random_bytes(8));
|
||||||
@@ -723,20 +752,36 @@ public function installProBundle(): void
|
|||||||
// Best-effort cleanup; ignore failures
|
// Best-effort cleanup; ignore failures
|
||||||
@unlink($zipPath);
|
@unlink($zipPath);
|
||||||
@rmdir($workDir);
|
@rmdir($workDir);
|
||||||
|
|
||||||
|
// NEW: ensure OPcache picks up new Pro bundle code immediately
|
||||||
|
if (function_exists('opcache_invalidate')) {
|
||||||
|
foreach ($installed['src'] as $pathInfo) {
|
||||||
|
// strip " (overwritten)" suffix if present
|
||||||
|
$path = preg_replace('/\s+\(overwritten\)$/', '', $pathInfo);
|
||||||
|
if (is_string($path) && $path !== '' && is_file($path)) {
|
||||||
|
@opcache_invalidate($path, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Reflect current Pro status in response if bootstrap was loaded
|
// Reflect current Pro status in response if bootstrap was loaded
|
||||||
$proActive = defined('FR_PRO_ACTIVE') && FR_PRO_ACTIVE;
|
$proActive = defined('FR_PRO_ACTIVE') && FR_PRO_ACTIVE;
|
||||||
|
|
||||||
|
$reportedVersion = $declaredVersion;
|
||||||
|
if ($reportedVersion === null && defined('FR_PRO_BUNDLE_VERSION')) {
|
||||||
|
$reportedVersion = FR_PRO_BUNDLE_VERSION;
|
||||||
|
}
|
||||||
|
|
||||||
$proPayload = defined('FR_PRO_INFO') && is_array(FR_PRO_INFO)
|
$proPayload = defined('FR_PRO_INFO') && is_array(FR_PRO_INFO)
|
||||||
? (FR_PRO_INFO['payload'] ?? null)
|
? (FR_PRO_INFO['payload'] ?? null)
|
||||||
: null;
|
: null;
|
||||||
$proVersion = defined('FR_PRO_BUNDLE_VERSION') ? FR_PRO_BUNDLE_VERSION : null;
|
|
||||||
|
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Pro bundle installed.',
|
'message' => 'Pro bundle installed.',
|
||||||
'installed' => $installed,
|
'installed' => $installed,
|
||||||
'proActive' => (bool)$proActive,
|
'proActive' => (bool)$proActive,
|
||||||
'proVersion' => $proVersion,
|
'proVersion' => $reportedVersion,
|
||||||
'proPayload' => $proPayload,
|
'proPayload' => $proPayload,
|
||||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
@@ -809,6 +854,7 @@ public function installProBundle(): void
|
|||||||
'customLogoUrl' => '',
|
'customLogoUrl' => '',
|
||||||
'headerBgLight' => '',
|
'headerBgLight' => '',
|
||||||
'headerBgDark' => '',
|
'headerBgDark' => '',
|
||||||
|
'footerHtml' => '',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -948,21 +994,22 @@ public function installProBundle(): void
|
|||||||
|
|
||||||
$merged['onlyoffice'] = $oo;
|
$merged['onlyoffice'] = $oo;
|
||||||
}
|
}
|
||||||
// Branding: pass through raw strings; AdminModel enforces Pro + sanitization.
|
// Branding: pass through raw strings; AdminModel enforces Pro + sanitization.
|
||||||
if (isset($data['branding']) && is_array($data['branding'])) {
|
if (isset($data['branding']) && is_array($data['branding'])) {
|
||||||
if (!isset($merged['branding']) || !is_array($merged['branding'])) {
|
if (!isset($merged['branding']) || !is_array($merged['branding'])) {
|
||||||
$merged['branding'] = [
|
$merged['branding'] = [
|
||||||
'customLogoUrl' => '',
|
'customLogoUrl' => '',
|
||||||
'headerBgLight' => '',
|
'headerBgLight' => '',
|
||||||
'headerBgDark' => '',
|
'headerBgDark' => '',
|
||||||
];
|
'footerHtml' => '',
|
||||||
}
|
];
|
||||||
foreach (['customLogoUrl', 'headerBgLight', 'headerBgDark'] as $key) {
|
}
|
||||||
if (array_key_exists($key, $data['branding'])) {
|
foreach (['customLogoUrl', 'headerBgLight', 'headerBgDark', 'footerHtml'] as $key) {
|
||||||
$merged['branding'][$key] = (string)$data['branding'][$key];
|
if (array_key_exists($key, $data['branding'])) {
|
||||||
}
|
$merged['branding'][$key] = (string)$data['branding'][$key];
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$result = AdminModel::updateConfig($merged);
|
$result = AdminModel::updateConfig($merged);
|
||||||
if (isset($result['error'])) {
|
if (isset($result['error'])) {
|
||||||
|
|||||||
@@ -110,17 +110,18 @@ private static function sanitizeLogoUrl($url): string
|
|||||||
'providerUrl' => (string)($config['oidc']['providerUrl'] ?? ''),
|
'providerUrl' => (string)($config['oidc']['providerUrl'] ?? ''),
|
||||||
'redirectUri' => (string)($config['oidc']['redirectUri'] ?? ''),
|
'redirectUri' => (string)($config['oidc']['redirectUri'] ?? ''),
|
||||||
],
|
],
|
||||||
'branding' => [
|
'branding' => [
|
||||||
'customLogoUrl' => self::sanitizeLogoUrl(
|
'customLogoUrl' => self::sanitizeLogoUrl(
|
||||||
$config['branding']['customLogoUrl'] ?? ''
|
$config['branding']['customLogoUrl'] ?? ''
|
||||||
),
|
),
|
||||||
'headerBgLight' => self::sanitizeColorHex(
|
'headerBgLight' => self::sanitizeColorHex(
|
||||||
$config['branding']['headerBgLight'] ?? ''
|
$config['branding']['headerBgLight'] ?? ''
|
||||||
),
|
),
|
||||||
'headerBgDark' => self::sanitizeColorHex(
|
'headerBgDark' => self::sanitizeColorHex(
|
||||||
$config['branding']['headerBgDark'] ?? ''
|
$config['branding']['headerBgDark'] ?? ''
|
||||||
),
|
),
|
||||||
],
|
'footerHtml' => (string)($config['branding']['footerHtml'] ?? ''),
|
||||||
|
],
|
||||||
'demoMode' => (defined('FR_DEMO_MODE') && FR_DEMO_MODE),
|
'demoMode' => (defined('FR_DEMO_MODE') && FR_DEMO_MODE),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -261,29 +262,31 @@ private static function sanitizeLogoUrl($url): string
|
|||||||
$configUpdate['onlyoffice'] = $norm;
|
$configUpdate['onlyoffice'] = $norm;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Branding (Pro-only). Normalize and only persist when Pro is active.
|
if (!isset($configUpdate['branding']) || !is_array($configUpdate['branding'])) {
|
||||||
if (!isset($configUpdate['branding']) || !is_array($configUpdate['branding'])) {
|
$configUpdate['branding'] = [
|
||||||
$configUpdate['branding'] = [
|
'customLogoUrl' => '',
|
||||||
'customLogoUrl' => '',
|
'headerBgLight' => '',
|
||||||
'headerBgLight' => '',
|
'headerBgDark' => '',
|
||||||
'headerBgDark' => '',
|
'footerHtml' => '',
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
$logo = self::sanitizeLogoUrl($configUpdate['branding']['customLogoUrl'] ?? '');
|
$logo = self::sanitizeLogoUrl($configUpdate['branding']['customLogoUrl'] ?? '');
|
||||||
$light = self::sanitizeColorHex($configUpdate['branding']['headerBgLight'] ?? '');
|
$light = self::sanitizeColorHex($configUpdate['branding']['headerBgLight'] ?? '');
|
||||||
$dark = self::sanitizeColorHex($configUpdate['branding']['headerBgDark'] ?? '');
|
$dark = self::sanitizeColorHex($configUpdate['branding']['headerBgDark'] ?? '');
|
||||||
|
$footer = trim((string)($configUpdate['branding']['footerHtml'] ?? ''));
|
||||||
if (defined('FR_PRO_ACTIVE') && FR_PRO_ACTIVE) {
|
|
||||||
$configUpdate['branding']['customLogoUrl'] = $logo;
|
if (defined('FR_PRO_ACTIVE') && FR_PRO_ACTIVE) {
|
||||||
$configUpdate['branding']['headerBgLight'] = $light;
|
$configUpdate['branding']['customLogoUrl'] = $logo;
|
||||||
$configUpdate['branding']['headerBgDark'] = $dark;
|
$configUpdate['branding']['headerBgLight'] = $light;
|
||||||
} else {
|
$configUpdate['branding']['headerBgDark'] = $dark;
|
||||||
// Free mode: always clear branding customizations
|
$configUpdate['branding']['footerHtml'] = $footer;
|
||||||
$configUpdate['branding']['customLogoUrl'] = '';
|
} else {
|
||||||
$configUpdate['branding']['headerBgLight'] = '';
|
$configUpdate['branding']['customLogoUrl'] = '';
|
||||||
$configUpdate['branding']['headerBgDark'] = '';
|
$configUpdate['branding']['headerBgLight'] = '';
|
||||||
}
|
$configUpdate['branding']['headerBgDark'] = '';
|
||||||
}
|
$configUpdate['branding']['footerHtml'] = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Convert configuration to JSON.
|
// Convert configuration to JSON.
|
||||||
$plainTextConfig = json_encode($configUpdate, JSON_PRETTY_PRINT);
|
$plainTextConfig = json_encode($configUpdate, JSON_PRETTY_PRINT);
|
||||||
@@ -444,6 +447,7 @@ private static function sanitizeLogoUrl($url): string
|
|||||||
'customLogoUrl' => '',
|
'customLogoUrl' => '',
|
||||||
'headerBgLight' => '',
|
'headerBgLight' => '',
|
||||||
'headerBgDark' => '',
|
'headerBgDark' => '',
|
||||||
|
'footerHtml' => '',
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
$config['branding']['customLogoUrl'] = self::sanitizeLogoUrl(
|
$config['branding']['customLogoUrl'] = self::sanitizeLogoUrl(
|
||||||
@@ -486,6 +490,7 @@ private static function sanitizeLogoUrl($url): string
|
|||||||
'customLogoUrl' => '',
|
'customLogoUrl' => '',
|
||||||
'headerBgLight' => '',
|
'headerBgLight' => '',
|
||||||
'headerBgDark' => '',
|
'headerBgDark' => '',
|
||||||
|
'footerHtml' => '',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user