258 lines
8.4 KiB
JavaScript
258 lines
8.4 KiB
JavaScript
// /js/appCore.js
|
||
import { showToast } from './domUtils.js?v={{APP_QVER}}';
|
||
import { t } from './i18n.js?v={{APP_QVER}}';
|
||
import { loadFolderTree } from './folderManager.js?v={{APP_QVER}}';
|
||
import { setupTrashRestoreDelete } from './trashRestoreDelete.js?v={{APP_QVER}}';
|
||
import { initDragAndDrop, loadSidebarOrder, loadHeaderOrder } from './dragAndDrop.js?v={{APP_QVER}}';
|
||
import { initTagSearch } from './fileTags.js?v={{APP_QVER}}';
|
||
import { initFileActions, openUploadModal } from './fileActions.js?v={{APP_QVER}}';
|
||
import { initUpload } from './upload.js?v={{APP_QVER}}';
|
||
import { loadAdminConfigFunc } from './auth.js?v={{APP_QVER}}';
|
||
|
||
window.__pendingDropData = null;
|
||
|
||
function waitFor(selector, timeout = 1200) {
|
||
return new Promise(resolve => {
|
||
const t0 = performance.now();
|
||
(function tick() {
|
||
const el = document.querySelector(selector);
|
||
if (el) return resolve(el);
|
||
if (performance.now() - t0 >= timeout) return resolve(null);
|
||
requestAnimationFrame(tick);
|
||
})();
|
||
});
|
||
}
|
||
|
||
// Keep a bound handle to the native fetch so wrappers elsewhere never recurse
|
||
const _nativeFetch = window.fetch.bind(window);
|
||
|
||
/* =========================
|
||
CSRF UTILITIES (shared)
|
||
========================= */
|
||
export function setCsrfToken(token) {
|
||
if (!token) return;
|
||
window.csrfToken = token;
|
||
localStorage.setItem('csrf', token);
|
||
|
||
// meta tag for easy access in other places
|
||
let meta = document.querySelector('meta[name="csrf-token"]');
|
||
if (!meta) {
|
||
meta = document.createElement('meta');
|
||
meta.name = 'csrf-token';
|
||
document.head.appendChild(meta);
|
||
}
|
||
meta.content = token;
|
||
}
|
||
|
||
export function getCsrfToken() {
|
||
return window.csrfToken || localStorage.getItem('csrf') || '';
|
||
}
|
||
|
||
/**
|
||
* Bootstrap/refresh CSRF from the server.
|
||
* Uses the native fetch to avoid wrapper loops and accepts rotated tokens via header.
|
||
*/
|
||
export async function loadCsrfToken() {
|
||
const res = await _nativeFetch('/api/auth/token.php', { method: 'GET', credentials: 'include' });
|
||
|
||
// header-based rotation
|
||
const hdr = res.headers.get('X-CSRF-Token');
|
||
if (hdr) setCsrfToken(hdr);
|
||
|
||
// body (if provided)
|
||
let body = {};
|
||
try { body = await res.json(); } catch { /* token endpoint may return empty */ }
|
||
|
||
const token = body.csrf_token || getCsrfToken();
|
||
setCsrfToken(token);
|
||
|
||
// share-url meta should reflect the actual origin
|
||
const actualShare = window.location.origin;
|
||
let shareMeta = document.querySelector('meta[name="share-url"]');
|
||
if (!shareMeta) {
|
||
shareMeta = document.createElement('meta');
|
||
shareMeta.name = 'share-url';
|
||
document.head.appendChild(shareMeta);
|
||
}
|
||
shareMeta.content = actualShare;
|
||
|
||
return { csrf_token: token, share_url: actualShare };
|
||
}
|
||
|
||
/* =========================
|
||
APP INIT (shared)
|
||
========================= */
|
||
export function initializeApp() {
|
||
const saved = parseInt(localStorage.getItem('rowHeight') || '44', 10);
|
||
document.documentElement.style.setProperty('--file-row-height', saved + 'px');
|
||
|
||
const last = localStorage.getItem('lastOpenedFolder');
|
||
window.currentFolder = last ? last : "root";
|
||
|
||
const stored = localStorage.getItem('showFoldersInList');
|
||
// default: false (unchecked)
|
||
window.showFoldersInList = stored === 'true';
|
||
|
||
const zoomWrap = document.querySelector('.header-zoom-controls');
|
||
if (zoomWrap) {
|
||
const hideZoom = localStorage.getItem('hideZoomControls') === 'true';
|
||
if (hideZoom) {
|
||
zoomWrap.style.display = 'none';
|
||
zoomWrap.setAttribute('aria-hidden', 'true');
|
||
} else {
|
||
zoomWrap.style.display = 'flex';
|
||
zoomWrap.removeAttribute('aria-hidden');
|
||
}
|
||
|
||
// Always load zoom.js once app is running
|
||
const QVER = (window.APP_QVER && String(window.APP_QVER)) || '{{APP_QVER}}';
|
||
import(`/js/zoom.js?v=${encodeURIComponent(QVER)}`).catch(err => {
|
||
console.warn('[zoom] failed to load zoom.js', err);
|
||
});
|
||
}
|
||
|
||
// Load public site config early (safe subset)
|
||
loadAdminConfigFunc();
|
||
|
||
// Enable tag search UI; initial file list load is controlled elsewhere
|
||
initTagSearch();
|
||
|
||
|
||
/*
|
||
// Hook DnD relay from fileList area into upload area
|
||
const fileListArea = document.getElementById('fileList');
|
||
|
||
if (fileListArea) {
|
||
let hoverTimer = null;
|
||
|
||
fileListArea.addEventListener('dragover', e => {
|
||
e.preventDefault();
|
||
fileListArea.classList.add('drop-hover');
|
||
// (optional) auto-open after brief hover so users see the drop target
|
||
if (!hoverTimer) {
|
||
hoverTimer = setTimeout(() => {
|
||
if (typeof window.openUploadModal === 'function') window.openUploadModal();
|
||
}, 400);
|
||
}
|
||
});
|
||
|
||
fileListArea.addEventListener('dragleave', () => {
|
||
fileListArea.classList.remove('drop-hover');
|
||
if (hoverTimer) { clearTimeout(hoverTimer); hoverTimer = null; }
|
||
});
|
||
|
||
fileListArea.addEventListener('drop', async e => {
|
||
e.preventDefault();
|
||
fileListArea.classList.remove('drop-hover');
|
||
if (hoverTimer) { clearTimeout(hoverTimer); hoverTimer = null; }
|
||
|
||
// 1) open the same modal that the Create menu uses
|
||
openUploadModal();
|
||
// 2) wait until the upload area exists *in the modal*, then relay the drop
|
||
// Prefer a scoped selector first to avoid duplicate IDs.
|
||
const uploadArea =
|
||
(await waitFor('#uploadModal #uploadDropArea')) ||
|
||
(await waitFor('#uploadDropArea'));
|
||
if (!uploadArea) return;
|
||
|
||
try {
|
||
// Many browsers make dataTransfer read-only; we try the direct attach first
|
||
const relay = new DragEvent('drop', { bubbles: true, cancelable: true });
|
||
Object.defineProperty(relay, 'dataTransfer', { value: e.dataTransfer });
|
||
uploadArea.dispatchEvent(relay);
|
||
} catch {
|
||
// Fallback: stash DataTransfer and fire a plain event; handler will read the stash
|
||
window.__pendingDropData = e.dataTransfer || null;
|
||
uploadArea.dispatchEvent(new Event('drop', { bubbles: true, cancelable: true }));
|
||
}
|
||
});
|
||
}*/
|
||
|
||
// App subsystems
|
||
initDragAndDrop();
|
||
loadSidebarOrder();
|
||
loadHeaderOrder();
|
||
initFileActions();
|
||
initUpload();
|
||
loadFolderTree();
|
||
|
||
// Only run trash/restore for admins
|
||
const isAdmin =
|
||
localStorage.getItem('isAdmin') === '1' || localStorage.getItem('isAdmin') === 'true';
|
||
if (isAdmin) {
|
||
setupTrashRestoreDelete();
|
||
}
|
||
|
||
// Small help tooltip toggle
|
||
const helpBtn = document.getElementById("folderHelpBtn");
|
||
const helpTooltip = document.getElementById("folderHelpTooltip");
|
||
if (helpBtn && helpTooltip) {
|
||
helpBtn.addEventListener("click", () => {
|
||
helpTooltip.style.display =
|
||
helpTooltip.style.display === "block" ? "none" : "block";
|
||
});
|
||
}
|
||
}
|
||
|
||
// ---- Zoom controls: load only for logged-in app ----
|
||
(function loadZoomControls() {
|
||
const zoomWrap = document.querySelector('.header-zoom-controls');
|
||
if (!zoomWrap) return;
|
||
|
||
// show container (keep CSS default = hidden)
|
||
zoomWrap.style.display = 'flex';
|
||
zoomWrap.style.alignItems = 'center';
|
||
|
||
try {
|
||
const QVER = (window.APP_QVER && String(window.APP_QVER)) || '{{APP_QVER}}';
|
||
import(`/js/zoom.js?v=${encodeURIComponent(QVER)}`)
|
||
.catch(err => console.warn('[zoom] failed to load:', err));
|
||
} catch (e) {
|
||
console.warn('[zoom] load error:', e);
|
||
}
|
||
})();
|
||
|
||
|
||
/* =========================
|
||
LOGOUT (shared)
|
||
========================= */
|
||
export function triggerLogout() {
|
||
const clearWelcomeFlags = () => {
|
||
try {
|
||
// one-per-tab toast guard
|
||
sessionStorage.removeItem('__fr_welcomed');
|
||
// if you also used the per-user (all-tabs) guard, clear that too:
|
||
const u = localStorage.getItem('username') || '';
|
||
if (u) localStorage.removeItem(`__fr_welcomed_${u}`);
|
||
} catch { }
|
||
};
|
||
|
||
_nativeFetch("/api/auth/logout.php", {
|
||
method: "POST",
|
||
credentials: "include",
|
||
headers: { "X-CSRF-Token": getCsrfToken() }
|
||
})
|
||
.then(() => {
|
||
clearWelcomeFlags();
|
||
window.location.reload(true);
|
||
})
|
||
.catch(() => {
|
||
// even if the request fails, clear the flags so the next login can toast
|
||
clearWelcomeFlags();
|
||
window.location.reload(true);
|
||
});
|
||
}
|
||
|
||
/* =========================
|
||
Global UX guard (unchanged)
|
||
========================= */
|
||
window.addEventListener("unhandledrejection", (ev) => {
|
||
const msg = (ev?.reason && ev.reason.message) || "";
|
||
if (msg === "auth") {
|
||
showToast(t("please_sign_in_again") || "Please sign in again.", "error");
|
||
ev.preventDefault();
|
||
} else if (msg === "forbidden") {
|
||
showToast(t("no_access_to_resource") || "You don’t have access to that.", "error");
|
||
ev.preventDefault();
|
||
}
|
||
}); |