diff --git a/CHANGELOG.md b/CHANGELOG.md index c4ee13b..eb9e75f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,44 @@ # Changelog +## Changes 11/8/2025 (v1.8.10) + +release(v1.8.10): theme-aware media modal, stronger file drag-and-drop, unified progress color, and favicon overhaul + +UI/UX — Media modal + +- Add fixed top bar to avoid filename/controls overlapping native media chrome; keep hover-on-stage look. +- Show a Material icon by file type next to the filename (image/video/pdf/code/arch/txt, with fallback). +- Restore “X” behavior and make hover theme-aware (red pill + white ‘X’ in light, red pill + black ‘X’ in dark). + +Video/Image controls + +- Top-right action icons use theme-aware styles and align with the filename row. +- Prev/Next paddles remain high-contrast and vertically centered within the stage. + +Progress badges (list & modal) + +- Standardize “in-progress” to darker orange (#ea580c) for better contrast in light/dark; update CSS and list badge rendering. + +Drag & drop + +- Support multi-select drags with a clean JSON payload + text fallback; nicer drag ghost. +- More resilient drops: accept data-dest-folder, safer JSON parse, early guards, and better toasts. +- POST move now sends Accept header, uses global CSRF, and refreshes the active view on success. + +Editor & ONLYOFFICE + +- Full-screen OO modal with preconnect, optional hidden warm-up to reduce first-open latency, and live theme sync. +- CodeMirror path: fix theme/mode setters (use `cm`) and tighten dynamic mode loading. + +Assets & polish + +- Swap in full favicon stack (SVG + PNG 512/32/16 + ICO) and set theme-color; cache-busted via `{{APP_QVER}}`. +- Refresh `logo.svg` (accessibility, cleaner handles/gradients). + +Also added: refreshed resource images and new logo sizes (logo-16, logo-32, logo-64, etc.) for crisper favicons and embeds. + +--- + ## Changes 11/7/2025 (v1.8.9) release(v1.8.9): fix(oidc, admin): first-save Client ID/Secret (closes #64) diff --git a/public/assets/favicon.ico b/public/assets/favicon.ico index 7767951..4b36058 100644 Binary files a/public/assets/favicon.ico and b/public/assets/favicon.ico differ diff --git a/public/assets/logo-128.png b/public/assets/logo-128.png new file mode 100644 index 0000000..5b0e233 Binary files /dev/null and b/public/assets/logo-128.png differ diff --git a/public/assets/logo-16.png b/public/assets/logo-16.png new file mode 100644 index 0000000..ea92db7 Binary files /dev/null and b/public/assets/logo-16.png differ diff --git a/public/assets/logo-192.png b/public/assets/logo-192.png new file mode 100644 index 0000000..45cda28 Binary files /dev/null and b/public/assets/logo-192.png differ diff --git a/public/assets/logo-256.png b/public/assets/logo-256.png new file mode 100644 index 0000000..cb77bd4 Binary files /dev/null and b/public/assets/logo-256.png differ diff --git a/public/assets/logo-32.png b/public/assets/logo-32.png new file mode 100644 index 0000000..120bfcc Binary files /dev/null and b/public/assets/logo-32.png differ diff --git a/public/assets/logo-48.png b/public/assets/logo-48.png new file mode 100644 index 0000000..b020a9e Binary files /dev/null and b/public/assets/logo-48.png differ diff --git a/public/assets/logo-64.png b/public/assets/logo-64.png new file mode 100644 index 0000000..8ddfa2b Binary files /dev/null and b/public/assets/logo-64.png differ diff --git a/public/assets/logo.png b/public/assets/logo.png index 94d14f0..a986fb1 100644 Binary files a/public/assets/logo.png and b/public/assets/logo.png differ diff --git a/public/assets/logo.svg b/public/assets/logo.svg index df88588..3c9f4b4 100644 Binary files a/public/assets/logo.svg and b/public/assets/logo.svg differ diff --git a/public/css/styles.css b/public/css/styles.css index aa04425..c558c89 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -1919,12 +1919,12 @@ body { color: #fff; } .status-badge.watched { - border-color: rgba(34,197,94,.35); /* green-ish */ + border-color: rgba(34,197,94,.45); /* green-ish */ background: rgba(34,197,94,.15); } .status-badge.progress { - border-color: rgba(250,204,21,.35); /* amber-ish */ - background: rgba(250,204,21,.15); + border-color: rgba(234,88,12,.55); /* amber-ish */ + background: rgba(234,88,12,.18); } #downloadProgressModal .modal-body, #downloadProgressModal .rise-modal-body, diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..4b36058 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/index.html b/public/index.html index 4e2b0aa..763b5ef 100644 --- a/public/index.html +++ b/public/index.html @@ -3,17 +3,24 @@ FileRise + - + + + + + + + - + - + diff --git a/public/js/fileDragDrop.js b/public/js/fileDragDrop.js index e80ff8d..574e112 100644 --- a/public/js/fileDragDrop.js +++ b/public/js/fileDragDrop.js @@ -2,124 +2,163 @@ import { showToast } from './domUtils.js?v={{APP_QVER}}'; import { loadFileList } from './fileListView.js?v={{APP_QVER}}'; -export function fileDragStartHandler(event) { - const row = event.currentTarget; - let fileNames = []; - - const selectedCheckboxes = document.querySelectorAll("#fileList .file-checkbox:checked"); - if (selectedCheckboxes.length > 1) { - selectedCheckboxes.forEach(chk => { - const parentRow = chk.closest("tr"); - if (parentRow) { - const cell = parentRow.querySelector("td:nth-child(2)"); - if (cell) { - let rawName = cell.textContent.trim(); - const tagContainer = cell.querySelector(".tag-badges"); - if (tagContainer) { - const tagText = tagContainer.innerText.trim(); - if (rawName.endsWith(tagText)) { - rawName = rawName.slice(0, -tagText.length).trim(); - } - } - fileNames.push(rawName); - } - } - }); - } else { - const fileNameCell = row.querySelector("td:nth-child(2)"); - if (fileNameCell) { - let rawName = fileNameCell.textContent.trim(); - const tagContainer = fileNameCell.querySelector(".tag-badges"); - if (tagContainer) { - const tagText = tagContainer.innerText.trim(); - if (rawName.endsWith(tagText)) { - rawName = rawName.slice(0, -tagText.length).trim(); - } - } - fileNames.push(rawName); - } - } - - if (fileNames.length === 0) return; - - const dragData = fileNames.length === 1 - ? { fileName: fileNames[0], sourceFolder: window.currentFolder || "root" } - : { files: fileNames, sourceFolder: window.currentFolder || "root" }; - - event.dataTransfer.setData("application/json", JSON.stringify(dragData)); - - let dragImage = document.createElement("div"); - dragImage.style.display = "inline-flex"; - dragImage.style.width = "auto"; - dragImage.style.maxWidth = "fit-content"; - dragImage.style.padding = "6px 10px"; - dragImage.style.backgroundColor = "#333"; - dragImage.style.color = "#fff"; - dragImage.style.border = "1px solid #555"; - dragImage.style.borderRadius = "4px"; - dragImage.style.alignItems = "center"; - dragImage.style.boxShadow = "2px 2px 6px rgba(0,0,0,0.3)"; - const icon = document.createElement("span"); - icon.className = "material-icons"; - icon.textContent = "insert_drive_file"; - icon.style.marginRight = "4px"; - const label = document.createElement("span"); - label.textContent = fileNames.length === 1 ? fileNames[0] : fileNames.length + " files"; - dragImage.appendChild(icon); - dragImage.appendChild(label); - - document.body.appendChild(dragImage); - event.dataTransfer.setDragImage(dragImage, 5, 5); - setTimeout(() => { - document.body.removeChild(dragImage); - }, 0); +/* ---------------- helpers ---------------- */ +function getRowEl(el) { + return el?.closest('tr[data-file-name], .gallery-card[data-file-name]') || null; +} +function getNameFromAny(el) { + const row = getRowEl(el); + if (!row) return null; + // 1) canonical + const n = row.getAttribute('data-file-name'); + if (n) return n; + // 2) filename-only span + const span = row.querySelector('.filename-text'); + if (span) return span.textContent.trim(); + return null; +} +function getSelectedFileNames() { + const boxes = Array.from(document.querySelectorAll('#fileList .file-checkbox:checked')); + const names = boxes.map(cb => getNameFromAny(cb)).filter(Boolean); + // de-dup just in case + return Array.from(new Set(names)); +} +function makeDragImage(labelText, iconName = 'insert_drive_file') { + const wrap = document.createElement('div'); + Object.assign(wrap.style, { + display: 'inline-flex', + maxWidth: '420px', + padding: '6px 10px', + backgroundColor: '#333', + color: '#fff', + border: '1px solid #555', + borderRadius: '6px', + alignItems: 'center', + gap: '6px', + boxShadow: '2px 2px 6px rgba(0,0,0,0.3)', + fontSize: '12px', + pointerEvents: 'none' + }); + const icon = document.createElement('span'); + icon.className = 'material-icons'; + icon.textContent = iconName; + const label = document.createElement('span'); + // trim long single-name labels + const txt = String(labelText || ''); + label.textContent = txt.length > 60 ? (txt.slice(0, 57) + '…') : txt; + wrap.appendChild(icon); + wrap.appendChild(label); + document.body.appendChild(wrap); + return wrap; } +/* ---------------- drag start (rows/cards) ---------------- */ +export function fileDragStartHandler(event) { + const row = getRowEl(event.currentTarget); + if (!row) return; + + // Use current selection if present; otherwise drag just this row’s file + let names = getSelectedFileNames(); + if (names.length === 0) { + const single = getNameFromAny(row); + if (single) names = [single]; + } + if (names.length === 0) return; + + const sourceFolder = window.currentFolder || 'root'; + const payload = { files: names, sourceFolder }; + + // primary payload + event.dataTransfer.setData('application/json', JSON.stringify(payload)); + // fallback (lets some environments read something human) + event.dataTransfer.setData('text/plain', names.join('\n')); + + // nicer drag image + const dragLabel = (names.length === 1) ? names[0] : `${names.length} files`; + const ghost = makeDragImage(dragLabel, names.length === 1 ? 'insert_drive_file' : 'folder'); + event.dataTransfer.setDragImage(ghost, 6, 6); + // clean up the ghost as soon as the browser has captured it + setTimeout(() => { try { document.body.removeChild(ghost); } catch { } }, 0); +} + +/* ---------------- folder targets ---------------- */ export function folderDragOverHandler(event) { event.preventDefault(); - event.currentTarget.classList.add("drop-hover"); + event.currentTarget.classList.add('drop-hover'); } - export function folderDragLeaveHandler(event) { - event.currentTarget.classList.remove("drop-hover"); + event.currentTarget.classList.remove('drop-hover'); } -export function folderDropHandler(event) { +export async function folderDropHandler(event) { event.preventDefault(); - event.currentTarget.classList.remove("drop-hover"); - const dropFolder = event.currentTarget.getAttribute("data-folder"); - let dragData; + event.currentTarget.classList.remove('drop-hover'); + + const dropFolder = event.currentTarget.getAttribute('data-folder') + || event.currentTarget.getAttribute('data-dest-folder') + || 'root'; + + // parse drag payload + let dragData = null; try { - dragData = JSON.parse(event.dataTransfer.getData("application/json")); - } catch (e) { - console.error("Invalid drag data"); + const raw = event.dataTransfer.getData('application/json') || '{}'; + dragData = JSON.parse(raw); + } catch { + // ignore + } + if (!dragData) { + showToast('Invalid drag data.'); return; } - if (!dragData || !dragData.fileName) return; - fetch("/api/file/moveFiles.php", { - method: "POST", - credentials: "include", - headers: { - "Content-Type": "application/json", - "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').getAttribute("content") - }, - body: JSON.stringify({ - source: dragData.sourceFolder, - files: [dragData.fileName], - destination: dropFolder - }) - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - showToast(`File "${dragData.fileName}" moved successfully to ${dropFolder}!`); - loadFileList(dragData.sourceFolder); - } else { - showToast("Error moving file: " + (data.error || "Unknown error")); - } - }) - .catch(error => { - console.error("Error moving file via drop:", error); - showToast("Error moving file."); + + // normalize names + let names = Array.isArray(dragData.files) ? dragData.files.slice() + : dragData.fileName ? [dragData.fileName] + : []; + names = names.filter(v => typeof v === 'string' && v.length > 0); + + if (names.length === 0) { + showToast('No files to move.'); + return; + } + + const sourceFolder = dragData.sourceFolder || (window.currentFolder || 'root'); + if (dropFolder === sourceFolder) { + showToast('Source and destination are the same.'); + return; + } + + // POST move + try { + const res = await fetch('/api/file/moveFiles.php', { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-CSRF-Token': window.csrfToken + }, + body: JSON.stringify({ + source: sourceFolder, + files: names, + destination: dropFolder + }) }); + const data = await res.json().catch(() => ({})); + + if (res.ok && data && data.success) { + const msg = (names.length === 1) + ? `Moved "${names[0]}" to ${dropFolder}.` + : `Moved ${names.length} files to ${dropFolder}.`; + showToast(msg); + // Refresh whatever view the user is currently looking at + loadFileList(window.currentFolder || sourceFolder); + } else { + const err = (data && (data.error || data.message)) || `HTTP ${res.status}`; + showToast('Error moving file(s): ' + err); + } + } catch (e) { + console.error('Error moving file(s):', e); + showToast('Error moving file(s).'); + } } \ No newline at end of file diff --git a/public/js/fileEditor.js b/public/js/fileEditor.js index c310a31..39c9ae2 100644 --- a/public/js/fileEditor.js +++ b/public/js/fileEditor.js @@ -70,7 +70,7 @@ function normalizeModeName(modeOption) { function getExt(name) { const i = name.lastIndexOf('.'); return i >= 0 ? name.slice(i + 1).toLowerCase() : ''; } // Cache OO capabilities (enabled flag + ext list) from /api/onlyoffice/status.php -let __ooCaps = { enabled: false, exts: new Set(), fetched: false }; +let __ooCaps = { enabled: false, exts: new Set(), fetched: false, docsOrigin: null }; async function fetchOnlyOfficeCapsOnce() { if (__ooCaps.fetched) return __ooCaps; @@ -80,6 +80,7 @@ async function fetchOnlyOfficeCapsOnce() { const j = await r.json(); __ooCaps.enabled = !!j.enabled; __ooCaps.exts = new Set(Array.isArray(j.exts) ? j.exts : []); + __ooCaps.docsOrigin = j.docsOrigin || null; // harmless if server doesn't send it } } catch { /* ignore; keep defaults */ } __ooCaps.fetched = true; @@ -93,121 +94,23 @@ async function shouldUseOnlyOffice(fileName) { function isAbsoluteHttpUrl(u) { return /^https?:\/\//i.test(u || ''); } -async function ensureOnlyOfficeApi(srcFromConfig, originFromConfig) { - let src = - srcFromConfig || - (originFromConfig ? originFromConfig.replace(/\/$/, '') + '/web-apps/apps/api/documents/api.js' - : (window.ONLYOFFICE_API_SRC || '/onlyoffice/web-apps/apps/api/documents/api.js')); - if (window.DocsAPI && typeof window.DocsAPI.DocEditor === 'function') return; - await loadScriptOnce(src); -} - -async function openOnlyOffice(fileName, folder) { - let editor; // make visible to the whole function - - try { - const url = `/api/onlyoffice/config.php?folder=${encodeURIComponent(folder)}&file=${encodeURIComponent(fileName)}`; - const resp = await fetch(url, { credentials: 'include' }); - - const text = await resp.text(); - let cfg; - try { cfg = JSON.parse(text); } catch { - throw new Error(`ONLYOFFICE config parse failed (HTTP ${resp.status}). First 120 chars: ${text.slice(0,120)}`); - } - if (!resp.ok) throw new Error(cfg.error || `ONLYOFFICE config HTTP ${resp.status}`); - - // Must be absolute - const docUrl = cfg?.document?.url; - const cbUrl = cfg?.editorConfig?.callbackUrl; - if (!/^https?:\/\//i.test(docUrl || '') || !/^https?:\/\//i.test(cbUrl || '')) { - throw new Error(`Config URLs must be absolute. document.url='${docUrl}', callbackUrl='${cbUrl}'`); - } - - // Load DocsAPI if needed - await ensureOnlyOfficeApi(cfg.docs_api_js, cfg.documentServerOrigin); - - // Modal - const modal = document.createElement('div'); - modal.id = 'ooEditorModal'; - modal.classList.add('modal', 'editor-modal'); - modal.setAttribute('tabindex', '-1'); - modal.innerHTML = ` -
-

- ${t("editing")}: ${escapeHTML(fileName)} -

- -
-
-
-
- `; - document.body.appendChild(modal); - modal.style.display = 'block'; - modal.focus(); - - // We’ll fill this after wiring the toggle, so destroy() can unhook it - let removeThemeListener = () => {}; - - const destroy = () => { - try { editor?.destroyEditor?.(); } catch {} - try { removeThemeListener(); } catch {} - try { modal.remove(); } catch {} - }; - - modal.addEventListener('keydown', e => { if (e.key === 'Escape') destroy(); }); - document.getElementById('closeEditorX')?.addEventListener('click', destroy); - - // Let DS request closing - cfg.events = Object.assign({}, cfg.events, { onRequestClose: destroy }); - - // Initial theme - const isDark = - document.documentElement.classList.contains('dark-mode') || - /^(1|true)$/i.test(localStorage.getItem('darkMode') || ''); - - cfg.editorConfig = cfg.editorConfig || {}; - cfg.editorConfig.customization = Object.assign( - {}, - cfg.editorConfig.customization, - { uiTheme: isDark ? 'theme-dark' : 'theme-light' } // <- correct key/value - ); - - // Launch editor - editor = new window.DocsAPI.DocEditor('oo-editor', cfg); - - // Live theme switching (ONLYOFFICE v7.2+ supports setTheme) - const darkToggle = document.getElementById('darkModeToggle'); - const onDarkToggle = () => { - const nowDark = document.documentElement.classList.contains('dark-mode'); - if (editor && typeof editor.setTheme === 'function') { - editor.setTheme(nowDark ? 'dark' : 'light'); - } - }; - if (darkToggle) { - darkToggle.addEventListener('click', onDarkToggle); - removeThemeListener = () => darkToggle.removeEventListener('click', onDarkToggle); - } - } catch (e) { - console.error('[ONLYOFFICE] failed to open:', e); - showToast((e && e.message) ? e.message : 'Unable to open ONLYOFFICE editor.'); - } -} -// ---- /ONLYOFFICE integration ---------------------------------------------- - - +// ---- script/css single-load with timeout guards ---- const _loadedScripts = new Set(); const _loadedCss = new Set(); let _corePromise = null; -function loadScriptOnce(url) { +function loadScriptOnce(url, timeoutMs = 12000) { return new Promise((resolve, reject) => { if (_loadedScripts.has(url)) return resolve(); const s = document.createElement("script"); + const timer = setTimeout(() => { + try { s.remove(); } catch { } + reject(new Error(`Timeout loading: ${url}`)); + }, timeoutMs); s.src = url; s.async = true; - s.onload = () => { _loadedScripts.add(url); resolve(); }; - s.onerror = () => reject(new Error(`Load failed: ${url}`)); + s.onload = () => { clearTimeout(timer); _loadedScripts.add(url); resolve(); }; + s.onerror = () => { clearTimeout(timer); reject(new Error(`Load failed: ${url}`)); }; document.head.appendChild(s); }); } @@ -240,7 +143,6 @@ async function ensureCore() { async function loadSingleMode(name) { const rel = MODE_URL[name]; if (!rel) return; - // prepend base if needed const url = rel.startsWith("http") ? rel : (rel.startsWith("/") ? rel : (CM_BASE + rel)); await loadScriptOnce(url); } @@ -265,9 +167,299 @@ async function ensureModeLoaded(modeOption) { } // Public helper for callers (we keep your existing function name in use): -const MODE_LOAD_TIMEOUT_MS = 2500; // allow closing immediately; don't wait forever +const MODE_LOAD_TIMEOUT_MS = 300; // allow closing immediately; don't wait forever // ==== /CodeMirror lazy loader =============================================== +// ---- OO preconnect / prewarm ---- +function injectOOPreconnect(origin) { + try { + if (!origin || !isAbsoluteHttpUrl(origin)) return; + const make = (rel) => { const l = document.createElement('link'); l.rel = rel; l.href = origin; return l; }; + document.head.appendChild(make('dns-prefetch')); + document.head.appendChild(make('preconnect')); + } catch { } +} + +async function ensureOnlyOfficeApi(srcFromConfig, originFromConfig) { + // Prefer explicit src; else derive from origin; else fall back to window/global or default prefix path + let src = srcFromConfig; + if (!src) { + if (originFromConfig && isAbsoluteHttpUrl(originFromConfig)) { + src = originFromConfig.replace(/\/$/, '') + '/web-apps/apps/api/documents/api.js'; + } else { + src = window.ONLYOFFICE_API_SRC || '/onlyoffice/web-apps/apps/api/documents/api.js'; + } + } + if (window.DocsAPI && typeof window.DocsAPI.DocEditor === 'function') return; + // Try once; if it times out and we derived from origin, fall back to the default prefix path + try { + console.time('oo:api.js'); + await loadScriptOnce(src); + } catch (e) { + if (src !== '/onlyoffice/web-apps/apps/api/documents/api.js') { + await loadScriptOnce('/onlyoffice/web-apps/apps/api/documents/api.js'); + } else { + throw e; + } + } finally { + console.timeEnd('oo:api.js'); + } +} + +// ===== ONLYOFFICE: full-screen modal + warm on every click ===== +const ALWAYS_WARM_OO = true; // warm EVERY time +const OO_WARM_MS = 300; + +function ensureOoModalCss() { + const prev = document.getElementById('ooEditorModalCss'); + if (prev) return; + + const style = document.createElement('style'); + style.id = 'ooEditorModalCss'; + style.textContent = ` + #ooEditorModal{ + --oo-header-h: 40px; + --oo-header-pad-v: 12px; + --oo-header-pad-h: 18px; + --oo-logo-h: 26px; /* tweak logo size */ + } + + #ooEditorModal{ + position:fixed!important; inset:0!important; margin:0!important; padding:0!important; + display:flex!important; flex-direction:column!important; z-index:2147483646!important; + background:var(--oo-modal-bg,#111)!important; + } + + /* Header: logo (left) + title (fill) + absolute close (right) */ + #ooEditorModal .editor-header{ + position:relative; display:flex; align-items:center; gap:12px; + min-height:var(--oo-header-h); + padding:var(--oo-header-pad-v) var(--oo-header-pad-h); + padding-right: calc(var(--oo-header-pad-h) + 64px); /* room for 32px round close */ + border-bottom:1px solid rgba(0,0,0,.15); + box-sizing:border-box; + } + + #ooEditorModal .editor-logo{ + height:var(--oo-logo-h); width:auto; flex:0 0 auto; + display:block; user-select:none; -webkit-user-drag:none; + } + + #ooEditorModal .editor-title{ + margin:0; font-size:18px; font-weight:700; line-height:1.2; + overflow:hidden; white-space:nowrap; text-overflow:ellipsis; + flex:1 1 auto; + } + + /* Your scoped close button style */ + #ooEditorModal .editor-close-btn{ + position:absolute; top:5px; right:10px; + display:flex; justify-content:center; align-items:center; + font-size:20px; font-weight:bold; cursor:pointer; z-index:1000; + width:32px; height:32px; border-radius:50%; text-align:center; line-height:30px; + color:#ff4d4d; background-color:rgba(255,255,255,.9); border:2px solid transparent; + transition:all .3s ease-in-out; + } + #ooEditorModal .editor-close-btn:hover{ + color:#fff; background-color:#ff4d4d; + box-shadow:0 0 6px rgba(255,77,77,.8); transform:scale(1.05); + } + .dark-mode #ooEditorModal .editor-close-btn{ background-color:rgba(0,0,0,.7); color:#ff6666; } + .dark-mode #ooEditorModal .editor-close-btn:hover{ background-color:#ff6666; color:#000; } + + #ooEditorModal .editor-body{ + position:relative!important; flex:1 1 auto!important; min-height:0!important; overflow:hidden!important; + } + #ooEditorModal #oo-editor{ width:100%!important; height:100%!important; } + + #ooEditorModal .oo-warm-overlay{ + position:absolute; inset:0; display:flex; align-items:center; justify-content:center; + background:rgba(0,0,0,.14); z-index:5; font-weight:600; font-size:14px; + } + + html.oo-lock, body.oo-lock{ height:100%!important; overflow:hidden!important; } + `; + document.head.appendChild(style); +} + +// Theme-aware background so there’s no white/gray edge +function applyModalBg(modal){ + const isDark = document.documentElement.classList.contains('dark-mode') + || /^(1|true)$/i.test(localStorage.getItem('darkMode') || ''); + const cs = getComputedStyle(document.documentElement); + const bg = (cs.getPropertyValue('--bg-color') || cs.getPropertyValue('--pre-bg') || '').trim() + || (isDark ? '#121212' : '#ffffff'); + modal.style.setProperty('--oo-modal-bg', bg); +} + +function lockPageScroll(on){ + [document.documentElement, document.body].forEach(el => el.classList.toggle('oo-lock', !!on)); +} + +function ensureOoFullscreenModal(){ + ensureOoModalCss(); + let modal = document.getElementById('ooEditorModal'); + if (!modal){ + modal = document.createElement('div'); + modal.id = 'ooEditorModal'; + modal.innerHTML = ` +
+ +

+ +
+
+
+
+ `; + document.body.appendChild(modal); + } else { + modal.querySelector('.editor-body').innerHTML = `
`; + // ensure logo exists and is placed before title when reusing + const header = modal.querySelector('.editor-header'); + if (!header.querySelector('.editor-logo')){ + const img = document.createElement('img'); + img.className = 'editor-logo'; + img.src = '/assets/logo.svg'; + img.alt = 'FileRise logo'; + header.insertBefore(img, header.querySelector('.editor-title')); + } else { + // make sure order is logo -> title + const logo = header.querySelector('.editor-logo'); + const title = header.querySelector('.editor-title'); + if (logo.nextElementSibling !== title){ + header.insertBefore(logo, title); + } + } + } + applyModalBg(modal); + modal.style.display = 'flex'; + modal.focus(); + lockPageScroll(true); + return modal; +} + +// Overlay lives INSIDE the modal body +function setOoBusy(modal, on, label='Preparing editor…'){ + if (!modal) return; + const body = modal.querySelector('.editor-body'); + let ov = body.querySelector('.oo-warm-overlay'); + if (on){ + if (!ov){ + ov = document.createElement('div'); + ov.className = 'oo-warm-overlay'; + ov.textContent = label; + body.appendChild(ov); + } + } else if (ov){ + ov.remove(); + } +} + +// Hidden warm-up DocEditor (creates DS session/cache) then destroys +async function warmDocServerOnce(cfg){ + let host = null, warmEditor = null; + try{ + host = document.createElement('div'); + host.id = 'oo-warm-' + Math.random().toString(36).slice(2); + Object.assign(host.style, { + position:'absolute', left:'-99999px', top:'0', width:'2px', height:'2px', overflow:'hidden' + }); + document.body.appendChild(host); + + const warmCfg = JSON.parse(JSON.stringify(cfg)); + warmCfg.events = Object.assign({}, warmCfg.events, { onAppReady(){}, onDocumentReady(){} }); + + warmEditor = new window.DocsAPI.DocEditor(host.id, warmCfg); + await new Promise(res => setTimeout(res, OO_WARM_MS)); + }catch{} finally{ + try{ warmEditor?.destroyEditor?.(); }catch{} + try{ host?.remove(); }catch{} + } +} + +// Full-screen OO open with hidden warm-up EVERY click, then real editor +async function openOnlyOffice(fileName, folder){ + let editor = null; + let removeThemeListener = () => {}; + let cfg = null; + let userClosed = false; + + // Build our full-screen modal + const modal = ensureOoFullscreenModal(); + const titleEl = modal.querySelector('.editor-title'); + if (titleEl) titleEl.innerHTML = `${t("editing")}: ${escapeHTML(fileName)}`; + + const destroy = (removeModal = true) => { + try { editor?.destroyEditor?.(); } catch {} + try { removeThemeListener(); } catch {} + if (removeModal) { try { modal.remove(); } catch {} } + lockPageScroll(false); + }; + const onClose = () => { userClosed = true; destroy(true); }; + + modal.querySelector('#closeEditorX')?.addEventListener('click', onClose); + modal.addEventListener('keydown', (e) => { if (e.key === 'Escape') onClose(); }); + + try{ + // 1) Fetch config + const url = `/api/onlyoffice/config.php?folder=${encodeURIComponent(folder)}&file=${encodeURIComponent(fileName)}`; + const resp = await fetch(url, { credentials: 'include' }); + const text = await resp.text(); + + try { cfg = JSON.parse(text); } catch { + throw new Error(`ONLYOFFICE config parse failed (HTTP ${resp.status}). First 120 chars: ${text.slice(0,120)}`); + } + if (!resp.ok) throw new Error(cfg?.error || `ONLYOFFICE config HTTP ${resp.status}`); + + // 2) Preconnect + load DocsAPI + injectOOPreconnect(cfg.documentServerOrigin || null); + await ensureOnlyOfficeApi(cfg.docs_api_js, cfg.documentServerOrigin); + + // 3) Theme + base events + const isDark = document.documentElement.classList.contains('dark-mode') + || /^(1|true)$/i.test(localStorage.getItem('darkMode') || ''); + cfg.events = (cfg.events && typeof cfg.events === 'object') ? cfg.events : {}; + cfg.editorConfig = cfg.editorConfig || {}; + cfg.editorConfig.customization = Object.assign( + {}, cfg.editorConfig.customization, { uiTheme: isDark ? 'theme-dark' : 'theme-light' } + ); + cfg.events.onRequestClose = () => onClose(); + + // 4) Warm EVERY click + if (ALWAYS_WARM_OO && !userClosed){ + setOoBusy(modal, true); // overlay INSIDE modal body + await warmDocServerOnce(cfg); + if (userClosed) return; + } + + // 5) Launch visible editor in full-screen modal + cfg.events.onDocumentReady = () => { setOoBusy(modal, false); }; + editor = new window.DocsAPI.DocEditor('oo-editor', cfg); + + // Live theme switching + keep modal bg in sync + const darkToggle = document.getElementById('darkModeToggle'); + const onDarkToggle = () => { + const nowDark = document.documentElement.classList.contains('dark-mode'); + if (editor && typeof editor.setTheme === 'function') { + editor.setTheme(nowDark ? 'dark' : 'light'); + } + applyModalBg(modal); + }; + if (darkToggle) { + darkToggle.addEventListener('click', onDarkToggle); + removeThemeListener = () => darkToggle.removeEventListener('click', onDarkToggle); + } + }catch(e){ + console.error('[ONLYOFFICE] failed to open:', e); + showToast((e && e.message) ? e.message : 'Unable to open ONLYOFFICE editor.'); + destroy(true); + } +} +// ---- /ONLYOFFICE integration ---------------------------------------------- + +// ==== Editor (CodeMirror) path ============================================= + function getModeForFile(fileName) { const dot = fileName.lastIndexOf("."); const ext = dot >= 0 ? fileName.slice(dot + 1).toLowerCase() : ""; @@ -452,38 +644,36 @@ export async function editFile(fileName, folder) { const normName = normalizeModeName(desiredMode) || "text/plain"; const initialMode = (forcePlainText || !isModeRegistered(normName)) ? "text/plain" : desiredMode; - const cmOptions = { - lineNumbers: !forcePlainText, - mode: initialMode, - theme, - viewportMargin: forcePlainText ? 20 : Infinity, - lineWrapping: false - }; - - const editor = window.CodeMirror.fromTextArea( + const cm = window.CodeMirror.fromTextArea( document.getElementById("fileEditor"), - cmOptions + { + lineNumbers: !forcePlainText, + mode: initialMode, + theme, + viewportMargin: forcePlainText ? 20 : Infinity, + lineWrapping: false + } ); - window.currentEditor = editor; + window.currentEditor = cm; setTimeout(adjustEditorSize, 50); observeModalResize(modal); // Font controls (now that editor exists) let currentFontSize = 14; - const wrapper = editor.getWrapperElement(); + const wrapper = cm.getWrapperElement(); wrapper.style.fontSize = currentFontSize + "px"; - editor.refresh(); + cm.refresh(); decBtn.addEventListener("click", function () { currentFontSize = Math.max(8, currentFontSize - 2); wrapper.style.fontSize = currentFontSize + "px"; - editor.refresh(); + cm.refresh(); }); incBtn.addEventListener("click", function () { currentFontSize = Math.min(32, currentFontSize + 2); wrapper.style.fontSize = currentFontSize + "px"; - editor.refresh(); + cm.refresh(); }); // Save @@ -496,7 +686,7 @@ export async function editFile(fileName, folder) { // Theme switch function updateEditorTheme() { const isDark = document.body.classList.contains("dark-mode"); - editor.setOption("theme", isDark ? "material-darker" : "default"); + cm.setOption("theme", isDark ? "material-darker" : "default"); } const toggle = document.getElementById("darkModeToggle"); if (toggle) toggle.addEventListener("click", updateEditorTheme); @@ -506,12 +696,10 @@ export async function editFile(fileName, folder) { if (!canceled && !forcePlainText) { const nn = normalizeModeName(desiredMode); if (nn && isModeRegistered(nn)) { - editor.setOption("mode", desiredMode); + cm.setOption("mode", desiredMode); } } - }).catch(() => { - // If the mode truly fails to load, we just stay in plain text - }); + }).catch(() => { /* stay in plain text */ }); }); }) .catch(error => { diff --git a/public/js/fileListView.js b/public/js/fileListView.js index 4d5db68..123198e 100644 --- a/public/js/fileListView.js +++ b/public/js/fileListView.js @@ -182,7 +182,7 @@ function makeBadge(state) { el.classList.add('watched'); el.textContent = (t('watched') || t('viewed') || 'Watched'); el.style.borderColor = 'rgba(34,197,94,.45)'; - el.style.background = 'rgba(34,197,94,.12)'; + el.style.background = 'rgba(34,197,94,.15)'; el.style.color = '#22c55e'; return el; } @@ -191,9 +191,9 @@ function makeBadge(state) { const pct = Math.max(1, Math.min(99, Math.round((state.seconds / state.duration) * 100))); el.classList.add('progress'); el.textContent = `${pct}%`; - el.style.borderColor = 'rgba(245,158,11,.45)'; - el.style.background = 'rgba(245,158,11,.12)'; - el.style.color = '#f59e0b'; + el.style.borderColor = 'rgba(234,88,12,.55)'; + el.style.background = 'rgba(234,88,12,.18)'; + el.style.color = '#ea580c'; return el; } diff --git a/public/js/filePreview.js b/public/js/filePreview.js index ca5cc1a..07b0d3d 100644 --- a/public/js/filePreview.js +++ b/public/js/filePreview.js @@ -123,6 +123,21 @@ export function openShareModal(file, folder) { const IMG_RE = /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i; const VID_RE = /\.(mp4|mkv|webm|mov|ogv)$/i; const AUD_RE = /\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i; +const ARCH_RE = /\.(zip|rar|7z|gz|bz2|xz|tar)$/i; +const CODE_RE = /\.(js|mjs|ts|tsx|json|yml|yaml|xml|html?|css|scss|less|php|py|rb|go|rs|c|cpp|h|hpp|java|cs|sh|bat|ps1)$/i; +const TXT_RE = /\.(txt|rtf|md|log)$/i; + +function getIconForFile(name) { + const lower = (name || '').toLowerCase(); + if (IMG_RE.test(lower)) return 'image'; + if (VID_RE.test(lower)) return 'ondemand_video'; + if (AUD_RE.test(lower)) return 'audiotrack'; + if (lower.endsWith('.pdf')) return 'picture_as_pdf'; + if (ARCH_RE.test(lower)) return 'archive'; + if (CODE_RE.test(lower)) return 'code'; + if (TXT_RE.test(lower)) return 'description'; + return 'insert_drive_file'; +} function ensureMediaModal() { let overlay = document.getElementById("filePreviewModal"); @@ -152,109 +167,166 @@ function ensureMediaModal() { const navFg = '#fff'; const navBorder = isDark ? 'rgba(255,255,255,.35)' : 'rgba(0,0,0,.25)'; + // fixed top bar; pad-right to avoid overlap with absolute close “×” overlay.innerHTML = `