From a2e5cfce24ec456390dc073d5d5a732dc7613b08 Mon Sep 17 00:00:00 2001 From: Edouard Vanbelle Date: Tue, 14 Apr 2026 19:03:54 +0200 Subject: [PATCH 1/5] chore(biome): JS check: error on undeclared variables/function --- biome.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/biome.json b/biome.json index 7f0377a0..20ba54c3 100644 --- a/biome.json +++ b/biome.json @@ -14,7 +14,8 @@ "rules": { "recommended": true, "correctness": { - "noUnusedVariables": "warn" + "noUnusedVariables": "warn", + "noUndeclaredVariables": "error" }, "style": { "noDescendingSpecificity": "off" From 6f4abfcec44f19f4533137ea61fa9729f7ed6c60 Mon Sep 17 00:00:00 2001 From: Edouard Vanbelle Date: Mon, 13 Apr 2026 15:09:10 +0200 Subject: [PATCH 2/5] refactor(js): avoid use of window.XXX and move to import/export - change worker: do not cache html pages (not necessary) - remove use of window.XXX and maximize import/export, this will provide more clarety, show circular dependencies + you will benefit IDE help --- static/admin.html | 6 +- static/device-verify.html | 3 +- static/index.html | 63 ++- static/js/app/authSession.js | 26 +- static/js/app/bootstrap.js | 11 +- static/js/app/filesView.js | 74 +-- static/js/app/main.js | 148 +++--- static/js/app/navigation.js | 140 +++--- static/js/app/searchView.js | 25 +- static/js/app/state.js | 4 +- static/js/app/trashView.js | 57 +-- static/js/app/ui.js | 227 ++++----- static/js/app/uiFileTypes.js | 6 +- static/js/app/uiNotifications.js | 8 +- static/js/app/userMenu.js | 19 +- static/js/core/csrf.js | 4 +- static/js/core/formatters.js | 7 +- static/js/core/i18n.js | 3 +- static/js/core/icons.js | 5 +- static/js/core/languageSelector.js | 9 +- static/js/core/modal.js | 21 +- static/js/core/notifications.js | 12 +- static/js/features/auth/auth.js | 24 +- static/js/features/files/contextMenus.js | 465 +++++++++--------- static/js/features/files/fileOperations.js | 227 +++++---- static/js/features/files/inlineViewer.js | 36 +- static/js/features/files/multiSelect.js | 67 +-- static/js/features/files/search.js | 32 +- static/js/features/files/wopiEditor.js | 12 +- static/js/features/library/favorites.js | 41 +- static/js/features/library/music.js | 170 +++---- static/js/features/library/photos.js | 20 +- static/js/features/library/photosLightbox.js | 29 +- static/js/features/library/recent.js | 32 +- static/js/features/sharing/fileSharing.js | 28 +- static/js/views/admin/admin.js | 10 +- .../js/views/device-verify/device-verify.js | 2 + static/js/views/profile/profile.js | 5 +- static/js/views/shared/sharedView.js | 27 +- static/login.html | 5 +- static/profile.html | 5 +- static/sw.js | 27 +- 42 files changed, 1104 insertions(+), 1038 deletions(-) diff --git a/static/admin.html b/static/admin.html index 2522c776..aa21b343 100644 --- a/static/admin.html +++ b/static/admin.html @@ -5,10 +5,6 @@ OxiCloud — Admin Panel - - - - @@ -608,6 +604,6 @@ - + diff --git a/static/device-verify.html b/static/device-verify.html index 3e227450..5fe3e6a3 100644 --- a/static/device-verify.html +++ b/static/device-verify.html @@ -48,7 +48,6 @@ - - + diff --git a/static/index.html b/static/index.html index 96227e2a..b6b034cc 100644 --- a/static/index.html +++ b/static/index.html @@ -19,38 +19,37 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/js/app/authSession.js b/static/js/app/authSession.js index f756c8b9..a1ef161e 100644 --- a/static/js/app/authSession.js +++ b/static/js/app/authSession.js @@ -2,6 +2,12 @@ * Authentication/session bootstrap and home-folder resolution */ +import { getCsrfHeaders } from '../core/csrf.js'; +import { loadFiles } from './filesView.js'; +import { updateStorageUsageDisplay } from './main.js'; +import { app } from './state.js'; +import { ui } from './ui.js'; + async function refreshUserData() { const USER_DATA_KEY = 'oxicloud_user'; @@ -24,7 +30,7 @@ async function refreshUserData() { console.log('Storage from server: used=', userData.storage_used_bytes, 'quota=', userData.storage_quota_bytes); localStorage.setItem(USER_DATA_KEY, JSON.stringify(userData)); - window.updateStorageUsageDisplay(userData); + updateStorageUsageDisplay(userData); return userData; } catch (error) { console.error('Error refreshing user data:', error); @@ -89,7 +95,7 @@ async function checkAuthentication() { if (menuName) menuName.textContent = userData.username; if (menuEmail) menuEmail.textContent = userData.email || ''; - window.updateStorageUsageDisplay(userData); + updateStorageUsageDisplay(userData); // Validate session BEFORE loading files to avoid 401 race condition const freshData = await refreshUserData(); @@ -133,8 +139,8 @@ async function checkAuthentication() { document.querySelectorAll('.user-avatar, .user-menu-avatar').forEach((el) => { el.textContent = userInitials; }); - window.updateStorageUsageDisplay(freshData); - resolveHomeFolder().then(() => window.loadFiles()); + updateStorageUsageDisplay(freshData); + resolveHomeFolder().then(() => loadFiles()); } else { console.warn('Could not retrieve user data, redirecting to login'); localStorage.removeItem(USER_DATA_KEY); @@ -154,8 +160,6 @@ async function checkAuthentication() { } async function resolveHomeFolder() { - const app = window.app; - if (app.userHomeFolderId) return; try { const response = await fetch('/api/folders', { @@ -173,22 +177,20 @@ async function resolveHomeFolder() { app.userHomeFolderName = home.name; app.currentPath = home.id; app.breadcrumbPath = []; - window.ui.updateBreadcrumb(); + ui.updateBreadcrumb(); console.log(`Home folder resolved: ${home.name} (${home.id})`); } else { console.warn('No root folders found for user'); app.currentPath = ''; app.breadcrumbPath = []; - window.ui.updateBreadcrumb(); + ui.updateBreadcrumb(); } } catch (error) { console.error('Error resolving home folder:', error); app.currentPath = ''; app.breadcrumbPath = []; - window.ui.updateBreadcrumb(); + ui.updateBreadcrumb(); } } -window.refreshUserData = refreshUserData; -window.checkAuthentication = checkAuthentication; -window.resolveHomeFolder = resolveHomeFolder; +export { checkAuthentication, refreshUserData, resolveHomeFolder }; diff --git a/static/js/app/bootstrap.js b/static/js/app/bootstrap.js index 54d45a16..fcdd327f 100644 --- a/static/js/app/bootstrap.js +++ b/static/js/app/bootstrap.js @@ -2,13 +2,10 @@ * OxiCloud - App bootstrap * Isolated startup trigger for the main application initializer. */ +import { initApp } from './main.js'; if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', () => { - if (typeof window.initApp === 'function') { - window.initApp(); - } - }); -} else if (typeof window.initApp === 'function') { - window.initApp(); + document.addEventListener('DOMContentLoaded', initApp); +} else { + initApp(); } diff --git a/static/js/app/filesView.js b/static/js/app/filesView.js index 4faa2221..81fff22e 100644 --- a/static/js/app/filesView.js +++ b/static/js/app/filesView.js @@ -1,5 +1,16 @@ // @ts-check +import { i18n } from '../core/i18n.js'; +import { inlineViewer } from '../features/files/inlineViewer.js'; +import { multiSelect } from '../features/files/multiSelect.js'; +import { resolveHomeFolder } from './authSession.js'; +import { updateHistory } from './main.js'; +import { app } from './state.js'; +import { ui } from './ui.js'; +import { uiNotifications } from './uiNotifications.js'; + +let isLoadingFiles = false; + // TODO move to features/files/fileOperations.js ? /** * @typedef {Object} FolderInfo @@ -48,8 +59,6 @@ async function getFolder(id) { * rebuild breadcrumb from selected folder (iterate up to root) */ async function rebuildBreadCrumb() { - const app = window.app; - /** * Store the leaf (this is the current displayed folder) * @type {FolderInfo | null} @@ -87,10 +96,7 @@ async function rebuildBreadCrumb() { } catch (_e) { console.log(`Error loading information from folder ${app.currentPath}, falling back to ${app.userHomeFolderId}`); // fallback of root - window.uiNotifications.show( - 'error: folder not found or permission denied', - 'the given folder is not available or you do not have sufficient rights' - ); + uiNotifications.show('error: folder not found or permission denied', 'the given folder is not available or you do not have sufficient rights'); app.breadcrumbPath = []; id = app.userHomeFolderId; app.currentPath = id; @@ -109,33 +115,31 @@ async function rebuildBreadCrumb() { * @param {boolean} [options.forceRefresh] force refresh of content */ async function loadFiles(options = { insertHistory: true }) { - const app = window.app; - try { console.log('Starting loadFiles() - loading files...', options); const forceRefresh = options.forceRefresh || false; - if (window.isLoadingFiles) { + if (isLoadingFiles) { console.log('A file load is already in progress, ignoring request'); return; } - window.isLoadingFiles = true; + isLoadingFiles = true; // This to avoid blinking page, a better solution would be to put loading on an overlay and remove timeout const loadingFiles = setTimeout(() => { // display loader after few delay (will be canceled if result take less time) - window.ui.showError(` + ui.showError(`
- ${window.i18n ? window.i18n.t('files.loading') : 'Loading files…'} + ${i18n ? i18n.t('files.loading') : 'Loading files…'}
`); }, 100); if (!app.userHomeFolderId) { - await window.resolveHomeFolder(); + await resolveHomeFolder(); } const timestamp = Math.floor(Date.now() / 1000); @@ -143,9 +147,9 @@ async function loadFiles(options = { insertHistory: true }) { await rebuildBreadCrumb(); // request a breadcrumb paint - window.ui.updateBreadcrumb(); + ui.updateBreadcrumb(); - window.updateHistory(options.insertHistory || false); + updateHistory(options.insertHistory || false); let url; @@ -154,7 +158,7 @@ async function loadFiles(options = { insertHistory: true }) { url = `/api/folders/${app.userHomeFolderId}/listing?t=${timestamp}`; app.currentPath = app.userHomeFolderId; app.breadcrumbPath = []; - window.ui.updateBreadcrumb(); + ui.updateBreadcrumb(); console.log(`Loading user folder: ${app.userHomeFolderName} (${app.userHomeFolderId})`); } else { url = `/api/folders?t=${timestamp}`; @@ -193,7 +197,7 @@ async function loadFiles(options = { insertHistory: true }) { if (response.status === 401 || response.status === 403) { console.warn('Auth error when loading files, showing empty list'); // FIXME: i18n - window.ui.showError(`

Could not load files

`); + ui.showError(`

Could not load files

`); return; } @@ -203,44 +207,44 @@ async function loadFiles(options = { insertHistory: true }) { const listing = await response.json(); - window.ui._items.clear(); - window.ui.resetFilesList(); - if (window.multiSelect) { - window.multiSelect.clear(); - window.multiSelect.init(); // this will wire buttons & select-all-checkbox + ui._items.clear(); + ui.resetFilesList(); + if (multiSelect) { + multiSelect.clear(); + multiSelect.init(); // this will wire buttons & select-all-checkbox } const folderList = Array.isArray(listing.folders) ? listing.folders : []; const fileList = Array.isArray(listing.files) ? listing.files : []; if (folderList.length === 0 && fileList.length === 0) { - window.ui.showEmptyList(); + ui.showEmptyList(); } else { - window.ui.renderFolders(folderList); - window.ui.renderFiles(fileList); + ui.renderFolders(folderList); + ui.renderFiles(fileList); // check if a file was provided - if (window.app.viewFile) { + if (app.viewFile) { let fileFound = null; // lookup for the given fle for (const file of fileList) { - if (file.id === window.app.viewFile) { + if (file.id === app.viewFile) { fileFound = file; break; } } if (fileFound) { - console.log(`file ${window.app.viewFile} found, calling viewer`); - await window.inlineViewer.openFile(fileFound); + console.log(`file ${app.viewFile} found, calling viewer`); + await inlineViewer.openFile(fileFound); } else { // remove file - console.log(`file ${window.app.viewFile} not found`); - window.app.viewFile = null; + console.log(`file ${app.viewFile} not found`); + app.viewFile = null; // correct url/history as file is not found - window.updateHistory(false); + updateHistory(false); } } } @@ -248,10 +252,10 @@ async function loadFiles(options = { insertHistory: true }) { console.log(`Loaded ${folderList.length} folders and ${fileList.length} files`); } catch (error) { console.error('Error loading folders:', error); - window.ui.showNotification('Error', 'Could not load files and folders'); + ui.showNotification('Error', 'Could not load files and folders'); } finally { - window.isLoadingFiles = false; + isLoadingFiles = false; } } -window.loadFiles = loadFiles; +export { loadFiles }; diff --git a/static/js/app/main.js b/static/js/app/main.js index d7ac93cb..fdba54f0 100644 --- a/static/js/app/main.js +++ b/static/js/app/main.js @@ -3,8 +3,31 @@ * This file contains the core functionality, initialization and state management */ -const app = window.app; -const elements = window.appElements; +import { formatFileSize, formatQuotaSize } from '../core/formatters.js'; +import { i18n } from '../core/i18n.js'; +import { Modal } from '../core/modal.js'; +import { fileOps } from '../features/files/fileOperations.js'; +import { multiSelect } from '../features/files/multiSelect.js'; +import { favorites } from '../features/library/favorites.js'; +import { recent } from '../features/library/recent.js'; +import { fileSharing } from '../features/sharing/fileSharing.js'; +import { sharedView } from '../views/shared/sharedView.js'; +import { checkAuthentication } from './authSession.js'; +import { loadFiles } from './filesView.js'; +import { + switchToFavoritesSection, + switchToFilesSection, + switchToMusicSection, + switchToPhotosSection, + switchToRecentFilesSection, + switchToSharedSection, + switchToTrashSection +} from './navigation.js'; +import { performSearch } from './searchView.js'; +import { app, appElements as elements } from './state.js'; +import { loadTrashItems } from './trashView.js'; +import { ui } from './ui.js'; +import { setupUserMenu } from './userMenu.js'; // Upload dropdown listener state (prevents accumulated listeners) /** @type { function | null } */ @@ -125,8 +148,8 @@ function setActionsBarMode(mode, force = false) { elements.gridViewBtn = document.getElementById('grid-view-btn'); elements.listViewBtn = document.getElementById('list-view-btn'); - if (window.i18n?.translateElement) { - window.i18n.translateElement(elements.actionsBar); + if (i18n?.translateElement) { + i18n.translateElement(elements.actionsBar); } if (mode === 'files') { @@ -159,7 +182,7 @@ function setupActionsBarDelegation() { break; } case 'new-folder-btn': { - const folderName = await window.Modal.promptNewFolder(); + const folderName = await Modal.promptNewFolder(); if (folderName) { fileOps.createFolder(folderName); } @@ -173,14 +196,14 @@ function setupActionsBarDelegation() { break; case 'empty-trash-btn': if (await fileOps.emptyTrash()) { - window.loadTrashItems(); + loadTrashItems(); } break; case 'clear-recent-btn': - if (window.recent) { - window.recent.clearRecentFiles(); - window.recent.displayRecentFiles(); - window.ui.showNotification('Cleanup completed', 'Recent files history has been cleared'); + if (recent) { + recent.clearRecentFiles(); + recent.displayRecentFiles(); + ui.showNotification('Cleanup completed', 'Recent files history has been cleared'); } break; default: @@ -245,8 +268,6 @@ function deserializeHash() { * @param {boolean} insertHistory true to change url and browser's history, false to change url only */ function updateHistory(insertHistory) { - const app = window.app; - const historyData = { section: app.currentSection, id: app.currentFolder, @@ -259,8 +280,8 @@ function updateHistory(insertHistory) { historyData.id = app.currentFolder; historyUrl = historyUrl.concat('/folder/', app.currentFolderInfo.id); - if (window.app.viewFile) { - historyUrl = historyUrl.concat('/file/', window.app.viewFile); + if (app.viewFile) { + historyUrl = historyUrl.concat('/file/', app.viewFile); } // update title document.title = `OxiCloud: ${app.currentFolderInfo.path}`; @@ -281,7 +302,7 @@ function updateHistory(insertHistory) { * @returns */ function switchSectionTo(section) { - if (window.app.currentSection === section) + if (app.currentSection === section) // no change ... return; @@ -324,8 +345,8 @@ function initApp() { cacheElements(); // Initialize file sharing module first - if (window.fileSharing?.init) { - window.fileSharing.init(); + if (fileSharing?.init) { + fileSharing.init(); } else { console.warn('fileSharing module not fully initialized'); } @@ -338,35 +359,26 @@ function initApp() { // Setup event listeners setupEventListeners(); - // Ensure inline viewer is initialized - if (!window.inlineViewer && typeof InlineViewer !== 'undefined') { - try { - window.inlineViewer = new InlineViewer(); - } catch (e) { - console.error('Error initializing inline viewer:', e); - } - } - // Initialize favorites module if available - if (window.favorites?.init) { + if (favorites?.init) { console.log('Initializing favorites module'); - window.favorites.init(); + favorites.init(); } else { console.warn('Favorites module not available or not initializable'); } // Initialize recent files module if available - if (window.recent?.init) { + if (recent?.init) { console.log('Initializing recent files module'); - window.recent.init(); + recent.init(); } else { console.warn('Recent files module not available or not initializable'); } // Initialize multi-select / batch actions - if (window.multiSelect?.init) { + if (multiSelect?.init) { console.log('Initializing multi-select module'); - window.multiSelect.init(); + multiSelect.init(); } window.addEventListener('authenticationDone', () => { @@ -376,33 +388,33 @@ function initApp() { if (hashContext.section === 'files') { if (hashContext.path) { console.log(`init: reusing folder from hash URL: ${hashContext.path}`); - window.app.currentPath = hashContext.path; + app.currentPath = hashContext.path; } if (hashContext.file !== null) { - window.app.viewFile = hashContext.file; + app.viewFile = hashContext.file; } - window.loadFiles(); + loadFiles(); } }); // Wait for translations to load before checking authentication - if (window.i18n?.isLoaded?.()) { + if (i18n?.isLoaded?.()) { // Translations already loaded, proceed with authentication - window.checkAuthentication(); + checkAuthentication(); } else { // Wait for translations to be loaded before proceeding console.log('Waiting for translations to load...'); window.addEventListener('translationsLoaded', () => { console.log('Translations loaded, proceeding with authentication'); - window.checkAuthentication(); + checkAuthentication(); }); // Set a timeout as a fallback in case translations take too long setTimeout(() => { - if (!window.i18n?.isLoaded?.()) { + if (!i18n?.isLoaded?.()) { console.warn('Translations loading timeout, proceeding with authentication anyway'); - window.checkAuthentication(); + checkAuthentication(); } }, 3000); // 3 second timeout } @@ -493,14 +505,14 @@ function setupEventListeners() { const hashContext = deserializeHash(); switchSectionTo(hashContext.section); if (hashContext.path) { - window.app.currentPath = hashContext.path; - window.loadFiles({ insertHistory: false }); + app.currentPath = hashContext.path; + loadFiles({ insertHistory: false }); } } else { // change is from history, data provided in event switchSectionTo(e.state.section); - window.app.currentPath = e.state.id; - window.loadFiles({ insertHistory: false }); + app.currentPath = e.state.id; + loadFiles({ insertHistory: false }); } }); @@ -512,19 +524,19 @@ function setupEventListeners() { const query = elements.searchInput.value.trim(); // In shared view, filter locally - if (app.isSharedView && window.sharedView) { - window.sharedView.filterAndSortItems(); + if (app.isSharedView && sharedView) { + sharedView.filterAndSortItems(); return; } if (query) { - window.performSearch(query); + performSearch(query); } else if (app.isSearchMode) { // If search is empty and we're in search mode, return to normal view app.isSearchMode = false; app.currentPath = ''; ui.updateBreadcrumb(''); - window.loadFiles(); + loadFiles(); } } }); @@ -536,7 +548,7 @@ function setupEventListeners() { if (query.length >= SEARCH_MIN_CHARS) { searchDebounceTimer = setTimeout(() => { - window.performSearch(query); + performSearch(query); }, SEARCH_DEBOUNCE_MS); } else if (query.length === 0 && app.isSearchMode) { // User cleared the search input — return to normal view @@ -544,7 +556,7 @@ function setupEventListeners() { app.isSearchMode = false; app.currentPath = ''; ui.updateBreadcrumb(''); - window.loadFiles(); + loadFiles(); }, SEARCH_DEBOUNCE_MS); } }); @@ -554,7 +566,7 @@ function setupEventListeners() { if (searchDebounceTimer) clearTimeout(searchDebounceTimer); const query = elements.searchInput.value.trim(); if (query) { - window.performSearch(query); + performSearch(query); } }); @@ -627,12 +639,12 @@ function setupEventListeners() { default: // Use the proper switchToFilesView function which handles all UI restoration - window.switchToFilesSection(); + switchToFilesSection(); // FIXME: because fileview handles it: need to converge code _updateHistory = false; } - document.title = `OxiCloud: ${window.i18n.t(itemI18nKey)}`; + document.title = `OxiCloud: ${i18n.t(itemI18nKey)}`; if (_updateHistory) { updateHistory(true); @@ -649,7 +661,7 @@ function setupEventListeners() { } // User menu - window.setupUserMenu(); + setupUserMenu(); // Global events to close context menus and deselect cards document.addEventListener('click', (e) => { @@ -666,18 +678,19 @@ function setupEventListeners() { }); } -// Expose needed functions to global scope -window.setActionsBarMode = setActionsBarMode; +// View-switching actions moved to app/navigation.js -// Set up global selectFolder function for navigation -window.selectFolder = (id, name) => { +/** + * Navigate into a folder and refresh the file list. + * @param {string} id + * @param {string} name + */ +export function selectFolder(id, name) { app.breadcrumbPath.push({ id, name }); app.currentPath = id; ui.updateBreadcrumb(); - window.loadFiles(); -}; - -// View-switching actions moved to app/navigation.js + loadFiles(); +} /** * Update the storage usage display with the user's actual storage usage @@ -719,8 +732,8 @@ function updateStorageUsageDisplay(userData) { storageInfo.removeAttribute('data-i18n'); // Use i18n if available - if (window.i18n?.t) { - storageInfo.textContent = window.i18n.t('storage.used', { + if (i18n?.t) { + storageInfo.textContent = i18n.t('storage.used', { percentage: usagePercentage, used: usedFormatted, total: quotaFormatted @@ -733,9 +746,4 @@ function updateStorageUsageDisplay(userData) { console.log(`Updated storage display: ${usagePercentage}% (${usedFormatted} / ${quotaFormatted})`); } -window.updateStorageUsageDisplay = updateStorageUsageDisplay; - -// Initialize app when DOM is ready -window.initApp = initApp; -window.updateHistory = updateHistory; -window.deserializeHash = deserializeHash; +export { deserializeHash, initApp, setActionsBarMode, updateHistory, updateStorageUsageDisplay }; diff --git a/static/js/app/navigation.js b/static/js/app/navigation.js index 14269646..6884eff3 100644 --- a/static/js/app/navigation.js +++ b/static/js/app/navigation.js @@ -3,6 +3,19 @@ * Extracted from main.js to keep navigation concerns isolated. */ +import { i18n } from '../core/i18n.js'; +import { multiSelect } from '../features/files/multiSelect.js'; +import { favorites } from '../features/library/favorites.js'; +import { musicView } from '../features/library/music.js'; +import { photosView } from '../features/library/photos.js'; +import { recent } from '../features/library/recent.js'; +import { sharedView } from '../views/shared/sharedView.js'; +import { loadFiles } from './filesView.js'; +import { setActionsBarMode } from './main.js'; +import { app, appElements } from './state.js'; +import { loadTrashItems } from './trashView.js'; +import { ui } from './ui.js'; + /** * Sync the hidden class and inline display for the grid/list containers * based on the current view preference. @@ -12,7 +25,7 @@ function syncViewContainers() { const gridViewBtn = document.getElementById('grid-view-btn'); const listViewBtn = document.getElementById('list-view-btn'); - const isGrid = window.app.currentView === 'grid'; + const isGrid = app.currentView === 'grid'; if (isGrid) { filesList.classList.remove('files-list-view'); filesList.classList.add('files-grid-view'); @@ -89,13 +102,6 @@ function initSidebarToggle() { } }); }); - - // Expose functions globally - window.sidebarToggle = { - open: openSidebar, - close: closeSidebar, - toggle: toggleSidebar - }; } // Initialize sidebar toggle when DOM is ready @@ -128,17 +134,17 @@ function getSectionFromNavItem(navItem) { * @returns {boolean} true if the section changed */ function setCurrentSection(section) { - if (window.app.currentSection === section) return false; + if (app.currentSection === section) return false; // Set all view flags - true for active section, false for others Object.entries(VIEW_FLAGS).forEach(([key, flag]) => { - window.app[flag] = key === section; + app[flag] = key === section; }); - window.app.currentSection = section; + app.currentSection = section; // Update nav item active classes by finding matching item from DOM - window.appElements.navItems.forEach((item) => { + appElements.navItems.forEach((item) => { const itemSection = getSectionFromNavItem(item); item.classList.toggle('active', itemSection === section); }); @@ -146,22 +152,22 @@ function setCurrentSection(section) { // Update page title const titleKey = `nav.${section}`; const defaultTitle = section.charAt(0).toUpperCase() + section.slice(1); - window.appElements.pageTitle.textContent = window.i18n ? window.i18n.t(titleKey) : defaultTitle; - window.appElements.pageTitle.setAttribute('data-i18n', titleKey); + appElements.pageTitle.textContent = i18n ? i18n.t(titleKey) : defaultTitle; + appElements.pageTitle.setAttribute('data-i18n', titleKey); // Hide sharedView when switching to any other section - if (section !== 'shared' && window.sharedView) { - window.sharedView.hide(); + if (section !== 'shared' && sharedView) { + sharedView.hide(); } // Hide photosView when switching to any other section - if (section !== 'photos' && window.photosView) { - window.photosView.hide(); + if (section !== 'photos' && photosView) { + photosView.hide(); } // Hide musicView when switching to any other section - if (section !== 'music' && window.musicView) { - window.musicView.hide(); + if (section !== 'music' && musicView) { + musicView.hide(); } return true; @@ -175,27 +181,27 @@ function switchToSharedSection() { breadcrumb?.classList.add('hidden'); // Hide actions-bar for shared view - window.setActionsBarMode('hidden'); + setActionsBarMode('hidden'); //reset files view + remove any error - window.ui.resetFilesList(); + ui.resetFilesList(); // Hide file containers toggleFileContainer(false); // Show shared view - if (window.sharedView) { - window.sharedView.init(); - window.sharedView.show(); + if (sharedView) { + sharedView.init(); + sharedView.show(); } - if (window.multiSelect) window.multiSelect.clear(); + if (multiSelect) multiSelect.clear(); } function switchToFilesSection() { if (!setCurrentSection('files')) return; // Set actions bar mode - window.setActionsBarMode('files', true); + setActionsBarMode('files', true); // Show breadcrumb (only in Files view) const breadcrumb = document.querySelector('.breadcrumb'); @@ -208,22 +214,22 @@ function switchToFilesSection() { syncViewContainers(); //reset files view + remove any error - window.ui.resetFilesList(); + ui.resetFilesList(); // Reset to home folder and update breadcrumb - window.app.currentPath = window.app.userHomeFolderId || ''; - window.app.breadcrumbPath = []; - window.ui.updateBreadcrumb(); - if (window.multiSelect) window.multiSelect.clear(); + app.currentPath = app.userHomeFolderId || ''; + app.breadcrumbPath = []; + ui.updateBreadcrumb(); + if (multiSelect) multiSelect.clear(); - window.loadFiles(); + loadFiles(); } function switchToFavoritesSection() { if (!setCurrentSection('favorites')) return; // Set actions bar mode - window.setActionsBarMode('favorites'); + setActionsBarMode('favorites'); // Hide breadcrumb (only shown in Files view) const breadcrumb = document.querySelector('.breadcrumb'); @@ -236,26 +242,26 @@ function switchToFavoritesSection() { syncViewContainers(); //reset files view + remove any error - window.ui.resetFilesList(); + ui.resetFilesList(); - if (window.favorites) { - window.favorites.displayFavorites(); + if (favorites) { + favorites.displayFavorites(); } else { console.error('Favorites module not loaded or initialized'); - window.ui.showError(` + ui.showError(`

Error loading the favorites module

`); } - if (window.multiSelect) window.multiSelect.clear(); + if (multiSelect) multiSelect.clear(); } function switchToRecentFilesSection() { if (!setCurrentSection('recent')) return; // Set actions bar mode - window.setActionsBarMode('recent'); + setActionsBarMode('recent'); // Hide breadcrumb (only shown in Files view) const breadcrumb = document.querySelector('.breadcrumb'); @@ -268,18 +274,18 @@ function switchToRecentFilesSection() { syncViewContainers(); //reset files view + remove any error - window.ui.resetFilesList(); + ui.resetFilesList(); - if (window.recent) { - window.recent.displayRecentFiles(); + if (recent) { + recent.displayRecentFiles(); } else { console.error('Recent files module not loaded or initialized'); - window.ui.showError(` + ui.showError(`

Error loading the recent module

`); } - if (window.multiSelect) window.multiSelect.clear(); + if (multiSelect) multiSelect.clear(); } function switchToPhotosSection() { @@ -290,19 +296,19 @@ function switchToPhotosSection() { breadcrumb?.classList.add('hidden'); // Hide actions-bar (photos has its own upload via selection bar) - window.setActionsBarMode('hidden'); + setActionsBarMode('hidden'); //reset files view + remove any error - window.ui.resetFilesList(); + ui.resetFilesList(); // Hide file containers toggleFileContainer(false); // Show photos view - if (window.photosView) { - window.photosView.show(); + if (photosView) { + photosView.show(); } - if (window.multiSelect) window.multiSelect.clear(); + if (multiSelect) multiSelect.clear(); } function switchToTrashSection() { @@ -319,15 +325,15 @@ function switchToTrashSection() { setActionsBarMode('trash'); //reset files view + remove any error - window.ui.resetFilesList(); + ui.resetFilesList(); //ensure buttons match the current view syncViewContainers(); // Load trash items - window.loadTrashItems(); + loadTrashItems(); - if (window.multiSelect) window.multiSelect.clear(); + if (multiSelect) multiSelect.clear(); } function switchToMusicSection() { @@ -341,27 +347,29 @@ function switchToMusicSection() { toggleFileContainer(false); // Hide actions-bar - window.setActionsBarMode('hidden'); + setActionsBarMode('hidden'); // Reset files view + remove any error - window.ui.resetFilesList(); + ui.resetFilesList(); // Hide list header (created by resetFilesList) const listHeader = document.querySelector('.list-header'); listHeader?.classList.add('hidden'); // Show music view - if (window.musicView) { - window.musicView.show(); + if (musicView) { + musicView.show(); } - if (window.multiSelect) window.multiSelect.clear(); + if (multiSelect) multiSelect.clear(); } -window.switchToFilesSection = switchToFilesSection; -window.switchToSharedSection = switchToSharedSection; -window.switchToFavoritesSection = switchToFavoritesSection; -window.switchToRecentFilesSection = switchToRecentFilesSection; -window.switchToPhotosSection = switchToPhotosSection; -window.switchToTrashSection = switchToTrashSection; -window.switchToMusicSection = switchToMusicSection; -window.syncViewContainers = syncViewContainers; +export { + switchToFavoritesSection, + switchToFilesSection, + switchToMusicSection, + switchToPhotosSection, + switchToRecentFilesSection, + switchToSharedSection, + switchToTrashSection, + syncViewContainers +}; diff --git a/static/js/app/searchView.js b/static/js/app/searchView.js index 2dfcfc85..2ace946f 100644 --- a/static/js/app/searchView.js +++ b/static/js/app/searchView.js @@ -2,16 +2,23 @@ * Search view orchestration logic */ -async function performSearch(query, sortBy) { - const app = window.app; +import { search } from '../features/files/search.js'; +import { resolveHomeFolder } from './authSession.js'; +import { app } from './state.js'; +import { ui } from './ui.js'; +/** + * @param {string} query + * @param {string} [sortBy] + */ +async function performSearch(query, sortBy) { console.log(`Performing search for: "${query}" (sort: ${sortBy || 'relevance'})`); try { app.isSearchMode = true; - window.ui.updateBreadcrumb(`Search: "${query}"`); + ui.updateBreadcrumb(`Search: "${query}"`); - window.ui.showError(`

Searching for "${query}"...

`); + ui.showError(`

Searching for "${query}"...

`); const options = { recursive: true, @@ -22,7 +29,7 @@ async function performSearch(query, sortBy) { if (!app.isTrashView) { // Ensure we have a valid folder_id before searching if (!app.currentPath || app.currentPath === '') { - await window.resolveHomeFolder(); + await resolveHomeFolder(); } // Only set folder_id if we have a valid value @@ -32,11 +39,11 @@ async function performSearch(query, sortBy) { // If still no valid folder_id, search will be global (without folder_id) } - const searchResults = await window.search.searchFiles(query, options); - window.search.displaySearchResults(searchResults); + const searchResults = await search.searchFiles(query, options); + search.displaySearchResults(searchResults); } catch (error) { console.error('Search error:', error); - window.ui.showNotification('Error', 'Error performing search'); + ui.showNotification('Error', 'Error performing search'); } } @@ -47,4 +54,4 @@ document.addEventListener('search-resort', (e) => { } }); -window.performSearch = performSearch; +export { performSearch }; diff --git a/static/js/app/state.js b/static/js/app/state.js index 1e2bfa7a..9397fcdc 100644 --- a/static/js/app/state.js +++ b/static/js/app/state.js @@ -3,7 +3,7 @@ * Centralized mutable state for app and cached DOM references. */ -window.app = { +export const app = { currentView: 'grid', currentPath: '', currentFolder: null, @@ -29,4 +29,4 @@ window.app = { viewFile: null // current file in inline view }; -window.appElements = {}; +export const appElements = {}; diff --git a/static/js/app/trashView.js b/static/js/app/trashView.js index 9ad63788..102d8e56 100644 --- a/static/js/app/trashView.js +++ b/static/js/app/trashView.js @@ -2,13 +2,20 @@ * Trash view loading and rendering logic */ +import { escapeHtml, formatDateTime } from '../core/formatters.js'; +import { i18n } from '../core/i18n.js'; +import { fileOps } from '../features/files/fileOperations.js'; +import { multiSelect } from '../features/files/multiSelect.js'; +import { appElements } from './state.js'; +import { ui } from './ui.js'; + async function loadTrashItems() { - const elements = window.appElements; + const elements = appElements; try { - if (window.multiSelect) window.multiSelect.clear(); - window.ui.resetFilesList(); // ensure also list visible & error hidden - const _tt = window.i18n?.t ? window.i18n.t : (k) => k.split('.').pop(); + if (multiSelect) multiSelect.clear(); + ui.resetFilesList(); // ensure also list visible & error hidden + const _tt = i18n?.t ? i18n.t : (k) => k.split('.').pop(); elements.filesList.innerHTML = `
${_tt('files.name')}
@@ -19,14 +26,14 @@ async function loadTrashItems() {
`; - window.ui.updateBreadcrumb(''); + ui.updateBreadcrumb(''); - const trashItems = await window.fileOps.getTrashItems(); + const trashItems = await fileOps.getTrashItems(); if (trashItems.length === 0) { - window.ui.showError(` + ui.showError(` -

${window.i18n ? window.i18n.t('trash.empty_state') : 'The trash is empty'}

+

${i18n ? i18n.t('trash.empty_state') : 'The trash is empty'}

`); return; } @@ -36,33 +43,27 @@ async function loadTrashItems() { }); } catch (error) { console.error('Error loading trash items:', error); - window.ui.showNotification('Error', 'Error loading trash items'); + ui.showNotification('Error', 'Error loading trash items'); } } function addTrashItemToView(item) { - const elements = window.appElements; + const elements = appElements; const isFile = item.item_type === 'file'; - const formattedDate = window.formatDateTime(item.trashed_at); + const formattedDate = formatDateTime(item.trashed_at); let iconClass; let typeLabel; let iconSpecialClass = ''; if (!isFile) { iconClass = item.icon_class || 'fas fa-folder'; - typeLabel = window.i18n ? window.i18n.t('files.file_types.folder') : 'Folder'; + typeLabel = i18n ? i18n.t('files.file_types.folder') : 'Folder'; } else { - iconClass = item.icon_class || (window.ui?.getIconClass ? window.ui.getIconClass(item.name) : 'fas fa-file'); - iconSpecialClass = window.ui?.getIconSpecialClass ? window.ui.getIconSpecialClass(item.name) : ''; + iconClass = item.icon_class || (ui?.getIconClass ? ui.getIconClass(item.name) : 'fas fa-file'); + iconSpecialClass = ui?.getIconSpecialClass ? ui.getIconSpecialClass(item.name) : ''; const cat = item.category || ''; - typeLabel = cat - ? window.i18n - ? window.i18n.t(`files.file_types.${cat.toLowerCase()}`) || cat - : cat - : window.i18n - ? window.i18n.t('files.file_types.document') - : 'Document'; + typeLabel = cat ? (i18n ? i18n.t(`files.file_types.${cat.toLowerCase()}`) || cat : cat) : i18n ? i18n.t('files.file_types.document') : 'Document'; } const isFolder = !isFile; @@ -85,10 +86,10 @@ function addTrashItemToView(item) {
${escapeHtml(item.original_path || '--')}
${escapeHtml(formattedDate)}
- -
@@ -96,19 +97,19 @@ function addTrashItemToView(item) { listElement.querySelector('.btn-restore').addEventListener('click', async (e) => { e.stopPropagation(); - if (await window.fileOps.restoreFromTrash(item.id)) { - window.loadTrashItems(); + if (await fileOps.restoreFromTrash(item.id)) { + loadTrashItems(); } }); listElement.querySelector('.btn-delete').addEventListener('click', async (e) => { e.stopPropagation(); - if (await window.fileOps.deletePermanently(item.id)) { - window.loadTrashItems(); + if (await fileOps.deletePermanently(item.id)) { + loadTrashItems(); } }); elements.filesList.appendChild(listElement); } -window.loadTrashItems = loadTrashItems; +export { loadTrashItems }; diff --git a/static/js/app/ui.js b/static/js/app/ui.js index 1323207c..1c3e7f83 100644 --- a/static/js/app/ui.js +++ b/static/js/app/ui.js @@ -5,6 +5,26 @@ // @ts-check +import { escapeHtml, formatDateTime, formatFileSize } from '../core/formatters.js'; +import { i18n } from '../core/i18n.js'; +import { OxiIcons, replaceIconsInElement } from '../core/icons.js'; +import { contextMenus } from '../features/files/contextMenus.js'; +import { fileOps } from '../features/files/fileOperations.js'; +import { inlineViewer } from '../features/files/inlineViewer.js'; +import { multiSelect } from '../features/files/multiSelect.js'; +import { wopiEditor } from '../features/files/wopiEditor.js'; +import { favorites } from '../features/library/favorites.js'; +import { recent } from '../features/library/recent.js'; +import { fileSharing } from '../features/sharing/fileSharing.js'; +import { loadFiles } from './filesView.js'; +import { updateHistory } from './main.js'; +import { syncViewContainers } from './navigation.js'; +import { app } from './state.js'; +import { uiFileTypes } from './uiFileTypes.js'; +import { uiNotifications } from './uiNotifications.js'; + +let __rubberBandJustFinished = false; + // UI Module const ui = { /** @type {HTMLDListElement | null} */ @@ -297,13 +317,13 @@ const ui = { document.body.appendChild(playlistDialog); document.getElementById('playlist-cancel-btn').addEventListener('click', () => { - if (window.contextMenus) window.contextMenus.closePlaylistDialog(); + if (contextMenus) contextMenus.closePlaylistDialog(); }); } // Assign events to menu items - if (window.contextMenus) { - window.contextMenus.assignMenuEvents(); + if (contextMenus) { + contextMenus.assignMenuEvents(); } else { console.warn('contextMenus module not loaded'); } @@ -478,10 +498,10 @@ const ui = { switchToGridView() { this._hydrateViewIfNeeded(); - window.app.currentView = 'grid'; + app.currentView = 'grid'; localStorage.setItem('oxicloud-view', 'grid'); - window.syncViewContainers(); + syncViewContainers(); }, /** @@ -490,10 +510,10 @@ const ui = { switchToListView() { this._hydrateViewIfNeeded(); - window.app.currentView = 'list'; + app.currentView = 'list'; localStorage.setItem('oxicloud-view', 'list'); - window.syncViewContainers(); + syncViewContainers(); }, /** @@ -504,12 +524,12 @@ const ui = { updateBreadcrumb() { const breadcrumb = document.querySelector('.breadcrumb'); breadcrumb.innerHTML = ''; - const path = window.app.breadcrumbPath; // [{id, name}, ...] + const path = app.breadcrumbPath; // [{id, name}, ...] // Helper function to safely get translation text const getTranslatedText = (key, defaultValue) => { - if (!window.i18n?.t) return defaultValue; - return window.i18n.t(key); + if (!i18n?.t) return defaultValue; + return i18n.t(key); }; // -- Home icon (always present, clickable to go to root) -- @@ -519,24 +539,24 @@ const ui = { homeIcon.title = getTranslatedText('breadcrumb.home', 'Home'); // Home is always clickable if we have a home folder - if (window.app.userHomeFolderId) { + if (app.userHomeFolderId) { homeIcon.classList.add('breadcrumb-link'); homeIcon.addEventListener('click', () => { - window.app.breadcrumbPath = []; - window.app.currentPath = window.app.userHomeFolderId; + app.breadcrumbPath = []; + app.currentPath = app.userHomeFolderId; this.updateBreadcrumb(); - window.loadFiles(); + loadFiles(); }); } breadcrumb.appendChild(homeIcon); // -- Root/Home folder name (if available) is always the first element of the breadcrumb -- // TODO clarify the difference between homeIcon & this first element - if (window.app.userHomeFolderName) { - if (path.length === 0 || path[0].id !== window.app.userHomeFolderId) { + if (app.userHomeFolderName) { + if (path.length === 0 || path[0].id !== app.userHomeFolderId) { path.unshift({ - name: window.app.userHomeFolderName, - id: window.app.userHomeFolderId + name: app.userHomeFolderName, + id: app.userHomeFolderId }); } } @@ -561,10 +581,10 @@ const ui = { // Intermediate segment: clickable – truncate path to this level item.classList.add('breadcrumb-link'); item.addEventListener('click', () => { - window.app.breadcrumbPath = path.slice(0, index + 1); - window.app.currentPath = segment.id; + app.breadcrumbPath = path.slice(0, index + 1); + app.currentPath = segment.id; this.updateBreadcrumb(); - window.loadFiles(); + loadFiles(); }); // can drag files on this folder @@ -611,7 +631,7 @@ const ui = { * @returns {boolean} */ isViewableFile(file) { - return window.uiFileTypes.isViewableFile(file); + return uiFileTypes.isViewableFile(file); }, /** @@ -620,7 +640,7 @@ const ui = { * (e.g. trash items). */ getIconClass(fileName) { - return window.uiFileTypes.getIconClass(fileName); + return uiFileTypes.getIconClass(fileName); }, /** @@ -628,7 +648,7 @@ const ui = { * Used as fallback when the backend DTO doesn't include icon_special_class. */ getIconSpecialClass(fileName) { - return window.uiFileTypes.getIconSpecialClass(fileName); + return uiFileTypes.getIconSpecialClass(fileName); }, /** @@ -637,7 +657,7 @@ const ui = { * @param {string} message - Notification message */ showNotification(title, message) { - window.uiNotifications.show(title, message); + uiNotifications.show(title, message); }, /** @@ -647,7 +667,7 @@ const ui = { const menu = document.getElementById('folder-context-menu'); if (menu) { menu.style.display = 'none'; - window.app.contextMenuTargetFolder = null; + app.contextMenuTargetFolder = null; } }, @@ -658,7 +678,7 @@ const ui = { const menu = document.getElementById('file-context-menu'); if (menu) { menu.style.display = 'none'; - window.app.contextMenuTargetFile = null; + app.contextMenuTargetFile = null; } }, @@ -679,8 +699,8 @@ const ui = { _delegationReady: false, _getActiveView() { - if (window.app && window.app.currentView === 'list') return 'list'; - if (window.app && window.app.currentView === 'grid') return 'grid'; + if (app && app.currentView === 'list') return 'list'; + if (app && app.currentView === 'grid') return 'grid'; const stored = localStorage.getItem('oxicloud-view'); return stored === 'list' ? 'list' : 'grid'; @@ -737,9 +757,9 @@ const ui = { * @param {any} dataTransfer fallback if nothing is selected */ async _dropToFolder(action, targetFolderId, dataTransfer) { - const selection = window.multiSelect.getSelection(targetFolderId); + const selection = multiSelect.getSelection(targetFolderId); - window.multiSelect.clear(); + multiSelect.clear(); if (selection.fileIds.length === 0 && selection.folderIds.length === 0) { // try to use dataTransfer (direct move without selection) @@ -770,20 +790,20 @@ const ui = { let result; switch (action) { case 'copy': - result = await window.fileOps.batchCopy(selection.fileIds, selection.folderIds, targetFolderId); + result = await fileOps.batchCopy(selection.fileIds, selection.folderIds, targetFolderId); break; case 'move': - result = await window.fileOps.batchMove(selection.fileIds, selection.folderIds, targetFolderId); + result = await fileOps.batchMove(selection.fileIds, selection.folderIds, targetFolderId); // redraw directory - if (result.success > 0) window.loadFiles(); + if (result.success > 0) loadFiles(); break; default: console.error(`drag and drop: action ${action} unknown`); return; } - window.multiSelect.showBatchResult(action, result); + multiSelect.showBatchResult(action, result); console.log(result); }, @@ -837,7 +857,7 @@ const ui = { const openFile = async (file) => { if (!file) return; - if (window.recent) { + if (recent) { document.dispatchEvent(new CustomEvent('file-accessed', { detail: { file } })); } // WOPI editor intercept: open Office documents in the WOPI editor @@ -846,8 +866,8 @@ const ui = { const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'bmp', 'ico', 'heic', 'heif', 'avif', 'tiff']; const isImage = file.mime_type?.startsWith('image/') || imageExts.includes(ext); try { - if (!isImage && window.wopiEditor && (await window.wopiEditor.canEdit(file.name))) { - await window.wopiEditor.openInModal(file.id, file.name, 'edit'); + if (!isImage && wopiEditor && (await wopiEditor.canEdit(file.name))) { + await wopiEditor.openInModal(file.id, file.name, 'edit'); return; } } catch (e) { @@ -855,38 +875,38 @@ const ui = { } if (this.isViewableFile(file) || isImage) { - if (window.inlineViewer) { - window.inlineViewer.openFile(file); + if (inlineViewer) { + inlineViewer.openFile(file); // update history - window.app.viewFile = file.id; - window.updateHistory(false); + app.viewFile = file.id; + updateHistory(false); } else { - window.fileOps.downloadFile(file.id, file.name); + fileOps.downloadFile(file.id, file.name); } } else { - window.fileOps.downloadFile(file.id, file.name); + fileOps.downloadFile(file.id, file.name); } }; const navigateFolder = (card) => { const folderId = card.dataset.folderId; const folderName = card.dataset.folderName; - window.app.breadcrumbPath.push({ id: folderId, name: folderName }); - window.app.currentPath = folderId; + app.breadcrumbPath.push({ id: folderId, name: folderName }); + app.currentPath = folderId; this.updateBreadcrumb(); - window.loadFiles(); + loadFiles(); }; const setContextTarget = (card, info) => { if (info.type === 'folder') { - window.app.contextMenuTargetFolder = { + app.contextMenuTargetFolder = { id: info.id, name: card.dataset.folderName, parent_id: card.dataset.parentId || '' }; } else { const fileData = info.data || self._items.get(info.id); - window.app.contextMenuTargetFile = { + app.contextMenuTargetFile = { id: info.id, name: card.dataset.fileName, folder_id: card.dataset.folderId || '', @@ -932,8 +952,8 @@ const ui = { } // shiftkey is used to complete selection - if (e.shiftKey && window.multiSelect) { - window.multiSelect.handleToggleItem(card, e); + if (e.shiftKey && multiSelect) { + multiSelect.handleToggleItem(card, e); return; } @@ -962,14 +982,14 @@ const ui = { setContextTarget(card, info); const menuId = info.type === 'folder' ? 'folder-context-menu' : 'file-context-menu'; const menu = document.getElementById(menuId); - if (window.contextMenus && typeof window.contextMenus.syncFavoriteOptionLabels === 'function') { - window.contextMenus.syncFavoriteOptionLabels(); + if (contextMenus && typeof contextMenus.syncFavoriteOptionLabels === 'function') { + contextMenus.syncFavoriteOptionLabels(); } - if (window.contextMenus && typeof window.contextMenus.syncWopiOptionVisibility === 'function') { - window.contextMenus.syncWopiOptionVisibility().catch(() => {}); + if (contextMenus && typeof contextMenus.syncWopiOptionVisibility === 'function') { + contextMenus.syncWopiOptionVisibility().catch(() => {}); } - if (window.contextMenus && typeof window.contextMenus.syncAddToPlaylistOption === 'function') { - window.contextMenus.syncAddToPlaylistOption(); + if (contextMenus && typeof contextMenus.syncAddToPlaylistOption === 'function') { + contextMenus.syncAddToPlaylistOption(); } menu.style.left = `${e.pageX}px`; menu.style.top = `${e.pageY}px`; @@ -1105,7 +1125,7 @@ const ui = { e.stopImmediatePropagation(); e.preventDefault(); - if (!window.favorites) return; + if (!favorites) return; const itemId = star.dataset.itemId; const itemType = star.dataset.itemType; @@ -1115,15 +1135,15 @@ const ui = { if (isActive) { this.setFavoriteVisualState(itemId, itemType, false); - window.favorites.removeFromFavorites(itemId, itemType); + favorites.removeFromFavorites(itemId, itemType); } else { this.setFavoriteVisualState(itemId, itemType, true); - window.favorites.addToFavorites(itemId, itemName, itemType); + favorites.addToFavorites(itemId, itemName, itemType); } // Keep context-menu label in sync if available - if (window.contextMenus && typeof window.contextMenus.syncFavoriteOptionLabels === 'function') { - window.contextMenus.syncFavoriteOptionLabels(); + if (contextMenus && typeof contextMenus.syncFavoriteOptionLabels === 'function') { + contextMenus.syncFavoriteOptionLabels(); } }); }, @@ -1142,8 +1162,8 @@ const ui = { // SVG icon path (after icons.js replacement) const svg = starBtn.querySelector('svg'); - const filledPath = window.OxiIcons?.star; - const outlinePath = window.OxiIcons?.['star-outline']; + const filledPath = OxiIcons?.star; + const outlinePath = OxiIcons?.['star-outline']; const targetPath = isFavorite ? filledPath : outlinePath; if (svg && targetPath) { const p = svg.querySelector('path'); @@ -1168,9 +1188,7 @@ const ui = { inlineStar = document.createElement('i'); inlineStar.className = 'fas fa-star favorite-star-inline'; nameCell.appendChild(inlineStar); - if (window.OxiIcons && typeof window.OxiIcons.replaceIconsInElement === 'function') { - window.OxiIcons.replaceIconsInElement(nameCell); - } + replaceIconsInElement(nameCell); } else if (!isFavorite && inlineStar) { inlineStar.remove(); } @@ -1190,8 +1208,8 @@ const ui = { el.dataset.folderName = folder.name; el.dataset.parentId = folder.parent_id || ''; - const isFav = window.favorites?.isFavorite(folder.id, 'folder'); - const formattedDate = window.formatDateTime(folder.modified_at); + const isFav = favorites?.isFavorite(folder.id, 'folder'); + const formattedDate = formatDateTime(folder.modified_at); el.innerHTML = `
@@ -1202,7 +1220,7 @@ const ui = { ${escapeHtml(folder.name)} ${isFav ? '' : ''} -
${window.i18n ? window.i18n.t('files.file_types.folder') : 'Folder'}
+
${i18n ? i18n.t('files.file_types.folder') : 'Folder'}
--
${formattedDate}
@@ -1213,7 +1231,7 @@ const ui = {
`; - if (window.app.currentPath !== '') { + if (app.currentPath !== '') { el.setAttribute('draggable', 'true'); } this._bindStarClick(el); @@ -1225,16 +1243,10 @@ const ui = { const iconClass = file.icon_class || this.getIconClass(file.name); const iconSpecialClass = file.icon_special_class || this.getIconSpecialClass(file.name); const cat = file.category || ''; - const typeLabel = cat - ? window.i18n - ? window.i18n.t(`files.file_types.${cat.toLowerCase()}`) || cat - : cat - : window.i18n - ? window.i18n.t('files.file_types.document') - : 'Document'; - const fileSize = file.size_formatted || window.formatFileSize(file.size); - const formattedDate = window.formatDateTime(file.modified_at); - const isFav = window.favorites?.isFavorite(file.id, 'file'); + const typeLabel = cat ? (i18n ? i18n.t(`files.file_types.${cat.toLowerCase()}`) || cat : cat) : i18n ? i18n.t('files.file_types.document') : 'Document'; + const fileSize = file.size_formatted || formatFileSize(file.size); + const formattedDate = formatDateTime(file.modified_at); + const isFav = favorites?.isFavorite(file.id, 'file'); const el = document.createElement('div'); el.className = 'file-item'; @@ -1293,7 +1305,7 @@ const ui = {
`; - if (window.i18n?.translateElement) window.i18n.translateElement(filesList); + if (i18n?.translateElement) i18n.translateElement(filesList); filesList.classList.remove('hidden'); filesContainerError?.classList.add('hidden'); @@ -1317,7 +1329,7 @@ const ui = { const filesList = document.getElementById('files-list'); if (filesContainerError) filesContainerError.innerHTML = content; - if (window.i18n?.translateElement) window.i18n.translateElement(filesContainerError); + if (i18n?.translateElement) i18n.translateElement(filesContainerError); filesContainerError?.classList.remove('hidden'); filesList?.classList.add('hidden'); @@ -1403,8 +1415,8 @@ const ui = { * Routes through the multiSelect module so batch actions know about selected items. */ function toggleCardSelection(card, event) { - if (window.multiSelect) { - window.multiSelect.handleToggleItem(card, event); + if (multiSelect) { + multiSelect.handleToggleItem(card, event); } else { card.classList.toggle('selected'); } @@ -1435,14 +1447,14 @@ function showContextMenuAtElement(triggerElement, menuId) { top = rect.top - 4 + window.scrollY; // flip above if no room } - if (window.contextMenus && typeof window.contextMenus.syncFavoriteOptionLabels === 'function') { - window.contextMenus.syncFavoriteOptionLabels(); + if (contextMenus && typeof contextMenus.syncFavoriteOptionLabels === 'function') { + contextMenus.syncFavoriteOptionLabels(); } - if (window.contextMenus && typeof window.contextMenus.syncWopiOptionVisibility === 'function') { - window.contextMenus.syncWopiOptionVisibility().catch(() => {}); + if (contextMenus && typeof contextMenus.syncWopiOptionVisibility === 'function') { + contextMenus.syncWopiOptionVisibility().catch(() => {}); } - if (window.contextMenus && typeof window.contextMenus.syncAddToPlaylistOption === 'function') { - window.contextMenus.syncAddToPlaylistOption(); + if (contextMenus && typeof contextMenus.syncAddToPlaylistOption === 'function') { + contextMenus.syncAddToPlaylistOption(); } menu.style.left = `${left}px`; @@ -1532,16 +1544,16 @@ function initRubberBandSelection() { card.classList.add('selected'); // Sync with multiSelect module - if (window.multiSelect) { - const info = window.multiSelect._extractInfo(card); - if (info) window.multiSelect.select(info.id, info.name, info.type, info.parentId); + if (multiSelect) { + const info = multiSelect._extractInfo(card); + if (info) multiSelect.select(info.id, info.name, info.type, info.parentId); } } else { card.classList.remove('selected'); // Deselect from multiSelect module - if (window.multiSelect) { - const info = window.multiSelect._extractInfo(card); - if (info) window.multiSelect.deselect(info.id); + if (multiSelect) { + const info = multiSelect._extractInfo(card); + if (info) multiSelect.deselect(info.id); } } }); @@ -1553,13 +1565,13 @@ function initRubberBandSelection() { const hadSelection = selRect.style.display === 'block'; selRect.style.display = 'none'; // Update the batch bar after rubber band selection completes - if (window.multiSelect) window.multiSelect._syncUI(); + if (multiSelect) multiSelect._syncUI(); // Suppress the click event that follows mouseup so the global // deselect handler doesn't immediately clear the selection. if (hadSelection) { - window.__rubberBandJustFinished = true; + __rubberBandJustFinished = true; requestAnimationFrame(() => { - window.__rubberBandJustFinished = false; + __rubberBandJustFinished = false; }); } }); @@ -1572,11 +1584,6 @@ if (document.readyState === 'loading') { initRubberBandSelection(); } -// Expose helpers globally -window.toggleCardSelection = toggleCardSelection; -window.showContextMenuAtElement = showContextMenuAtElement; -window.initRubberBandSelection = initRubberBandSelection; - /** * Show a modern confirm dialog (replaces native confirm()) * @param {Object} options @@ -1588,9 +1595,9 @@ window.initRubberBandSelection = initRubberBandSelection; * @returns {Promise} true if confirmed, false if cancelled */ function showConfirmDialog({ title, message, confirmText, cancelText, danger = true } = {}) { - const ct = confirmText || (window.i18n ? window.i18n.t('actions.delete') : 'Delete'); - const cc = cancelText || (window.i18n ? window.i18n.t('actions.cancel') : 'Cancel'); - const t = title || (window.i18n ? window.i18n.t('dialogs.confirm_title') : 'Confirm action'); + const ct = confirmText || (i18n ? i18n.t('actions.delete') : 'Delete'); + const cc = cancelText || (i18n ? i18n.t('actions.cancel') : 'Cancel'); + const t = title || (i18n ? i18n.t('dialogs.confirm_title') : 'Confirm action'); return new Promise((resolve) => { // Remove any previous confirm dialog @@ -1633,7 +1640,5 @@ function showConfirmDialog({ title, message, confirmText, cancelText, danger = t }); }); } -window.showConfirmDialog = showConfirmDialog; -// Expose UI module globally -window.ui = ui; +export { initRubberBandSelection, showConfirmDialog, showContextMenuAtElement, toggleCardSelection, ui }; diff --git a/static/js/app/uiFileTypes.js b/static/js/app/uiFileTypes.js index dd1f5761..d547df46 100644 --- a/static/js/app/uiFileTypes.js +++ b/static/js/app/uiFileTypes.js @@ -3,6 +3,8 @@ * Isolated icon and preview classification helpers used by ui.js. */ +import { isTextViewable } from '../core/formatters.js'; + const uiFileTypes = { // TODO: 'd better to use a canViw() method in inlineViewer isViewableFile(file) { @@ -11,7 +13,7 @@ const uiFileTypes = { if (file.mime_type === 'application/pdf') return true; if (file.mime_type.startsWith('audio/')) return true; if (file.mime_type.startsWith('video/')) return true; - return window.isTextViewable ? window.isTextViewable(file.mime_type) : false; + return isTextViewable(file.mime_type); }, getIconClass(fileName) { @@ -170,4 +172,4 @@ const uiFileTypes = { } }; -window.uiFileTypes = uiFileTypes; +export { uiFileTypes }; diff --git a/static/js/app/uiNotifications.js b/static/js/app/uiNotifications.js index 76244219..70473ddc 100644 --- a/static/js/app/uiNotifications.js +++ b/static/js/app/uiNotifications.js @@ -3,9 +3,11 @@ * Isolates notification rendering policy from ui.js. */ +import { notifications } from '../core/notifications.js'; + const uiNotifications = { show(title, message) { - if (window.notifications && typeof window.notifications.addNotification === 'function') { + if (notifications && typeof notifications.addNotification === 'function') { const normalizedTitle = String(title || '').toLowerCase(); let icon = 'fa-info-circle'; let iconClass = 'upload'; @@ -27,7 +29,7 @@ const uiNotifications = { iconClass = 'success'; } - window.notifications.addNotification({ + notifications.addNotification({ icon, iconClass, title: title || '', @@ -58,4 +60,4 @@ const uiNotifications = { } }; -window.uiNotifications = uiNotifications; +export { uiNotifications }; diff --git a/static/js/app/userMenu.js b/static/js/app/userMenu.js index 9096c26f..84dadeb9 100644 --- a/static/js/app/userMenu.js +++ b/static/js/app/userMenu.js @@ -2,6 +2,11 @@ * User menu, profile modal and logout logic */ +import { getCsrfHeaders } from '../core/csrf.js'; +import { formatFileSize, formatQuotaSize } from '../core/formatters.js'; +import { i18n } from '../core/i18n.js'; +import { ui } from './ui.js'; + function setupUserMenu() { const wrapper = document.getElementById('user-menu-wrapper'); const avatarBtn = document.getElementById('user-avatar-btn'); @@ -102,7 +107,7 @@ function setupUserMenu() { } } - window.ui.showNotification(newIsDark ? '🌙' : '☀️', newIsDark ? 'Dark mode enabled' : 'Light mode enabled'); + ui.showNotification(newIsDark ? '🌙' : '☀️', newIsDark ? 'Dark mode enabled' : 'Light mode enabled'); }); } @@ -173,8 +178,8 @@ function updateUserMenuData() { if (storageFill) storageFill.style.width = `${percentage}%`; if (storageText) { - const used = window.formatFileSize(usedBytes); - const total = window.formatQuotaSize(quotaBytes); + const used = formatFileSize(usedBytes); + const total = formatQuotaSize(quotaBytes); storageText.textContent = `${quotaBytes > 0 ? `${percentage}% · ` : ''}${used} / ${total}`; } } @@ -206,7 +211,7 @@ function showUserProfileModal() { const percentage = quotaBytes > 0 ? Math.min(Math.round((usedBytes / quotaBytes) * 100), 100) : 0; const barColor = percentage > 90 ? '#ef4444' : percentage > 70 ? '#f59e0b' : '#22c55e'; - const t = (key, fallback) => (window.i18n?.t ? window.i18n.t(key) || fallback : fallback); + const t = (key, fallback) => (i18n?.t ? i18n.t(key) || fallback : fallback); const existing = document.getElementById('profile-modal-overlay'); if (existing) existing.remove(); @@ -229,7 +234,7 @@ function showUserProfileModal() {
-
${percentage}% · ${window.formatFileSize(usedBytes)} / ${window.formatQuotaSize(quotaBytes)}
+
${percentage}% · ${formatFileSize(usedBytes)} / ${formatQuotaSize(quotaBytes)}