style: apply biome CSS/JS format

This commit is contained in:
Edouard Vanbelle
2026-04-07 22:48:59 +02:00
parent 786a778546
commit 1ce29c101d
86 changed files with 18032 additions and 15166 deletions
+9 -5
View File
@@ -25,7 +25,6 @@ async function refreshUserData() {
localStorage.setItem(USER_DATA_KEY, JSON.stringify(userData));
window.updateStorageUsageDisplay(userData);
return userData;
} catch (error) {
console.error('Error refreshing user data:', error);
@@ -82,7 +81,7 @@ async function checkAuthentication() {
if (userData.username) {
// We have cached user data — render immediately, refresh in background
const userInitials = userData.username.substring(0, 2).toUpperCase();
document.querySelectorAll('.user-avatar, .user-menu-avatar').forEach(el => {
document.querySelectorAll('.user-avatar, .user-menu-avatar').forEach((el) => {
el.textContent = userInitials;
});
const menuName = document.getElementById('user-menu-name');
@@ -103,7 +102,10 @@ async function checkAuthentication() {
const r = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json', ...getCsrfHeaders() },
headers: {
'Content-Type': 'application/json',
...getCsrfHeaders()
},
body: '{}'
});
if (r.ok) {
@@ -128,7 +130,7 @@ async function checkAuthentication() {
const freshData = await refreshUserData();
if (freshData && freshData.username) {
const userInitials = freshData.username.substring(0, 2).toUpperCase();
document.querySelectorAll('.user-avatar, .user-menu-avatar').forEach(el => el.textContent = userInitials);
document.querySelectorAll('.user-avatar, .user-menu-avatar').forEach((el) => (el.textContent = userInitials));
window.updateStorageUsageDisplay(freshData);
resolveHomeFolder().then(() => window.loadFiles());
} else {
@@ -154,7 +156,9 @@ async function resolveHomeFolder() {
if (app.userHomeFolderId) return;
try {
const response = await fetch('/api/folders', { credentials: 'same-origin' });
const response = await fetch('/api/folders', {
credentials: 'same-origin'
});
if (!response.ok) {
console.warn(`Could not fetch home folder: ${response.status}`);
return;
+31 -30
View File
@@ -21,12 +21,11 @@
* @param {string} id the id of the folder
* @returns {Promise<FolderInfo>}
*/
async function getFolder( id) {
async function getFolder(id) {
/** @type {HeadersInit} */
const headers = {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache'
Pragma: 'no-cache'
};
/** @type {RequestInit} */
@@ -35,12 +34,11 @@ async function getFolder( id) {
credentials: 'same-origin',
cache: 'no-store'
};
let folderInformations = await fetch( `/api/folders/${id}`, requestOptions);
let folderInformations = await fetch(`/api/folders/${id}`, requestOptions);
if (folderInformations.ok) {
return folderInformations.json()
}
else {
return folderInformations.json();
} else {
console.warn(`Error fetching folder ${id}`);
return Promise.reject(null);
}
@@ -50,15 +48,15 @@ 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}
* @type {FolderInfo | null}
*/
let currentFolderInfo = null;
// rebuild full breadcrumb,
// rebuild full breadcrumb,
// TODO: to optimize, data may already be known / or ETAG could be interesting to reduce load
app.breadcrumbPath = [];
@@ -77,61 +75,64 @@ async function rebuildBreadCrumb() {
}
// XXX do not enter root into bread crumb updateBreadcrumb() method always display it
if(!folderInfo.is_root) {
app.breadcrumbPath.unshift( {id: folderInfo.id, name: folderInfo.name});
if (!folderInfo.is_root) {
app.breadcrumbPath.unshift({
id: folderInfo.id,
name: folderInfo.name
});
}
// iterate to parent folder
id = folderInfo.parent_id;
}
catch( e) {
} 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'
'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;
}
}
// store informations on the current folder
// store informations on the current folder
app.currentFolderInfo = currentFolderInfo;
}
/**
* Files view loading logic
*
*
* @param {Object} options
* @param {boolean} [options.insertHistory] add browser history (default true)
* @param {boolean} [options.forceRefresh] force refresh of content
*/
async function loadFiles(options = { insertHistory: true}) {
async function loadFiles(options = { insertHistory: true }) {
const app = window.app;
const elements = window.appElements;
try {
console.log("Starting loadFiles() - loading files...", options);
console.log('Starting loadFiles() - loading files...', options);
const forceRefresh = options.forceRefresh || false;
if (window.isLoadingFiles) {
console.log("A file load is already in progress, ignoring request");
console.log('A file load is already in progress, ignoring request');
return;
}
window.isLoadingFiles = true;
// This to avoid blinking page, a better solution would be to put loading on an overlay and remove timeout
let loadingFiles = setTimeout( () => {
let loadingFiles = setTimeout(() => {
// display loader after few delay (will be canceled if result take less time)
window.ui.showError(`
<div class="files-loading-spinner">
<div class="spinner"></div>
<span>${window.i18n ? window.i18n.t('files.loading') : 'Loading files…'}</span>
</div>
`)
`);
}, 100);
if (!app.userHomeFolderId) {
@@ -145,7 +146,7 @@ async function loadFiles(options = { insertHistory: true}) {
// request a breadcrumb paint
window.ui.updateBreadcrumb();
window.updateHistory( options.insertHistory || false);
window.updateHistory(options.insertHistory || false);
let url;
@@ -158,7 +159,7 @@ async function loadFiles(options = { insertHistory: true}) {
console.log(`Loading user folder: ${app.userHomeFolderName} (${app.userHomeFolderId})`);
} else {
url = `/api/folders?t=${timestamp}`;
console.warn("Emergency fallback to root folder - this should not normally happen");
console.warn('Emergency fallback to root folder - this should not normally happen');
}
} else {
url = `/api/folders/${app.currentPath}/listing?t=${timestamp}`;
@@ -168,7 +169,7 @@ async function loadFiles(options = { insertHistory: true}) {
/** @type {HeadersInit} */
const headers = {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache'
Pragma: 'no-cache'
};
/** @type {RequestInit} */
@@ -189,10 +190,10 @@ async function loadFiles(options = { insertHistory: true}) {
const response = await fetch(url, requestOptions);
// not required anymore
clearTimeout( loadingFiles);
clearTimeout(loadingFiles);
if (response.status === 401 || response.status === 403) {
console.warn("Auth error when loading files, showing empty list");
console.warn('Auth error when loading files, showing empty list');
// FIXME: i18n
window.ui.showError(`<p>Could not load files</p>`);
return;
+47 -49
View File
@@ -103,9 +103,9 @@ function setActionsBarMode(mode, force = false) {
if (!elements.actionsBar) return;
if (mode === 'hidden') {
elements.actionsBar.classList.add("hidden");
elements.actionsBar.classList.add('hidden');
elements.actionsBar.dataset.mode = 'hidden';
console.log("......setup actions bar to hidden");
console.log('......setup actions bar to hidden');
return;
}
@@ -117,7 +117,7 @@ function setActionsBarMode(mode, force = false) {
if (!html) return;
elements.actionsBar.innerHTML = html;
elements.actionsBar.classList.remove("hidden");
elements.actionsBar.classList.remove('hidden');
elements.actionsBar.dataset.mode = mode;
// Refresh cached action elements after rebuild
@@ -220,7 +220,7 @@ function deserializeHash() {
// FIXME rename files into drive ?
hashContext.section = 'files';
const hash_elements = window.location.hash.split("/");
const hash_elements = window.location.hash.split('/');
const section = hash_elements[1];
@@ -245,13 +245,13 @@ function deserializeHash() {
*
* @param {boolean} insertHistory true to change url and browser's history, false to change url only
*/
function updateHistory( insertHistory) {
function updateHistory(insertHistory) {
const app = window.app;
let historyData = {
section: app.currentSection,
id: app.currentFolder,
file: app.viewFile,
file: app.viewFile
};
let historyUrl = `#/${app.currentSection}`;
@@ -268,12 +268,11 @@ function updateHistory( insertHistory) {
}
if (insertHistory) {
console.log(`adding history with ${historyUrl}`)
window.history.pushState(historyData, "", historyUrl);
}
else {
console.log(`replace history with ${historyUrl}`)
window.history.replaceState(historyData, "", historyUrl);
console.log(`adding history with ${historyUrl}`);
window.history.pushState(historyData, '', historyUrl);
} else {
console.log(`replace history with ${historyUrl}`);
window.history.replaceState(historyData, '', historyUrl);
}
}
@@ -283,33 +282,32 @@ function updateHistory( insertHistory) {
* @returns
*/
function switchSectionTo(section) {
if (window.app.currentSection === section)
// no change ...
return;
switch (section) {
case "files":
case 'files':
switchToFilesSection();
break;
case "shared":
case 'shared':
switchToSharedSection();
break;
case "recent":
case 'recent':
switchToRecentFilesSection();
break;
case "favorites":
case 'favorites':
switchToFavoritesSection();
break;
case "photos":
case 'photos':
switchToPhotosSection();
break;
case "trash":
case 'trash':
switchToTrashSection();
break;
@@ -375,8 +373,8 @@ function initApp() {
window.addEventListener('authenticationDone', () => {
// Check if a context was provided in the URL
let hashContext = deserializeHash();
switchSectionTo( hashContext.section);
if (hashContext.section === "files") {
switchSectionTo(hashContext.section);
if (hashContext.section === 'files') {
if (hashContext.path) {
console.log(`init: reusing folder from hash URL: ${hashContext.path}`);
window.app.currentPath = hashContext.path;
@@ -387,7 +385,6 @@ function initApp() {
}
window.loadFiles();
}
});
// Wait for translations to load before checking authentication
@@ -448,15 +445,19 @@ function setupUploadDropdown() {
const signal = uploadDropdownBindingsController.signal;
// Toggle dropdown on button click
uploadBtn.addEventListener('click', (e) => {
e.stopPropagation();
const isOpen = menu.classList.contains('show');
// Close any other open dropdowns
document.querySelectorAll('.upload-dropdown-menu.show').forEach(m => m.classList.remove('show'));
if (!isOpen) {
menu.classList.add('show');
}
}, { signal });
uploadBtn.addEventListener(
'click',
(e) => {
e.stopPropagation();
const isOpen = menu.classList.contains('show');
// Close any other open dropdowns
document.querySelectorAll('.upload-dropdown-menu.show').forEach((m) => m.classList.remove('show'));
if (!isOpen) {
menu.classList.add('show');
}
},
{ signal }
);
// Close dropdown when clicking outside
// remove+add stable handler: guarantees exactly one global listener
@@ -466,7 +467,7 @@ function setupUploadDropdown() {
}
uploadDropdownDocumentClickHandler = (e) => {
if (e.target.closest('#upload-dropdown')) return;
document.querySelectorAll('.upload-dropdown-menu.show').forEach(m => m.classList.remove('show'));
document.querySelectorAll('.upload-dropdown-menu.show').forEach((m) => m.classList.remove('show'));
};
// @ts-ignore
document.addEventListener('click', uploadDropdownDocumentClickHandler);
@@ -485,21 +486,20 @@ function setupEventListeners() {
const SEARCH_MIN_CHARS = 3;
// handle history / url change
window.addEventListener("popstate", (e) => {
window.addEventListener('popstate', (e) => {
if (e.state === null) {
// change is from user (url explicitely change, read information from hash)
let hashContext = deserializeHash();
switchSectionTo( hashContext.section);
const hashContext = deserializeHash();
switchSectionTo(hashContext.section);
if (hashContext.path) {
window.app.currentPath = hashContext.path;
window.loadFiles({insertHistory: false});
window.loadFiles({ insertHistory: false });
}
}
else {
} else {
// change is from history, data provided in event
switchSectionTo( e.state.section);
switchSectionTo(e.state.section);
window.app.currentPath = e.state.id;
window.loadFiles({insertHistory: false});
window.loadFiles({ insertHistory: false });
}
});
@@ -584,17 +584,17 @@ function setupEventListeners() {
}
// Sidebar navigation
elements.navItems.forEach(item => {
elements.navItems.forEach((item) => {
item.addEventListener('click', () => {
// Remove active class from all nav items
elements.navItems.forEach(navItem => navItem.classList.remove('active'));
elements.navItems.forEach((navItem) => navItem.classList.remove('active'));
// Add active class to clicked item
item.classList.add('active');
let _updateHistory = true;
let itemI18nKey=item.querySelector('span').getAttribute('data-i18n')
switch(itemI18nKey) {
let itemI18nKey = item.querySelector('span').getAttribute('data-i18n');
switch (itemI18nKey) {
case 'nav.shared':
// Switch to shared view
switchToSharedSection();
@@ -628,7 +628,7 @@ function setupEventListeners() {
document.title = `OxiCloud: ${window.i18n.t(itemI18nKey)}`;
if (_updateHistory) {
updateHistory( true);
updateHistory(true);
}
});
});
@@ -649,13 +649,11 @@ function setupEventListeners() {
const folderMenu = document.getElementById('folder-context-menu');
const fileMenu = document.getElementById('file-context-menu');
if (folderMenu && folderMenu.style.display === 'block' &&
!folderMenu.contains(e.target)) {
if (folderMenu && folderMenu.style.display === 'block' && !folderMenu.contains(e.target)) {
ui.closeContextMenu();
}
if (fileMenu && fileMenu.style.display === 'block' &&
!fileMenu.contains(e.target)) {
if (fileMenu && fileMenu.style.display === 'block' && !fileMenu.contains(e.target)) {
ui.closeFileContextMenu();
}
});
+70 -78
View File
@@ -14,15 +14,14 @@ function syncViewContainers() {
const isGrid = window.app.currentView === 'grid';
if (isGrid) {
filesList.classList.remove("files-list-view");
filesList.classList.add("files-grid-view");
filesList.classList.remove('files-list-view');
filesList.classList.add('files-grid-view');
gridViewBtn?.classList.add('active');
listViewBtn?.classList.remove('active');
}
else {
filesList.classList.add("files-list-view");
filesList.classList.remove("files-grid-view");
} else {
filesList.classList.add('files-list-view');
filesList.classList.remove('files-grid-view');
gridViewBtn?.classList.remove('active');
listViewBtn?.classList.add('active');
@@ -42,61 +41,61 @@ function toggleFileContainer(show) {
* Mobile sidebar toggle functionality
*/
function initSidebarToggle() {
const sidebarToggle = document.getElementById('sidebar-toggle');
const sidebar = document.getElementById('sidebar');
const sidebarOverlay = document.getElementById('sidebar-overlay');
const sidebarToggle = document.getElementById('sidebar-toggle');
const sidebar = document.getElementById('sidebar');
const sidebarOverlay = document.getElementById('sidebar-overlay');
if (!sidebarToggle || !sidebar || !sidebarOverlay) return;
if (!sidebarToggle || !sidebar || !sidebarOverlay) return;
function openSidebar() {
sidebar.classList.add('open');
sidebarOverlay.classList.add('active');
document.body.style.overflow = 'hidden';
}
function openSidebar() {
sidebar.classList.add('open');
sidebarOverlay.classList.add('active');
document.body.style.overflow = 'hidden';
}
function closeSidebar() {
sidebar.classList.remove('open');
sidebarOverlay.classList.remove('active');
document.body.style.overflow = '';
}
function closeSidebar() {
sidebar.classList.remove('open');
sidebarOverlay.classList.remove('active');
document.body.style.overflow = '';
}
function toggleSidebar() {
if (sidebar.classList.contains('open')) {
closeSidebar();
} else {
openSidebar();
}
}
function toggleSidebar() {
if (sidebar.classList.contains('open')) {
closeSidebar();
} else {
openSidebar();
}
}
// Toggle button click
sidebarToggle.addEventListener('click', toggleSidebar);
// Toggle button click
sidebarToggle.addEventListener('click', toggleSidebar);
// Close sidebar when clicking overlay
sidebarOverlay.addEventListener('click', closeSidebar);
// Close sidebar when clicking overlay
sidebarOverlay.addEventListener('click', closeSidebar);
// Close sidebar on escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && sidebar.classList.contains('open')) {
closeSidebar();
}
});
// Close sidebar on escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && sidebar.classList.contains('open')) {
closeSidebar();
}
});
// Close sidebar when navigating (nav item click on mobile)
const navItems = sidebar.querySelectorAll('.nav-item');
navItems.forEach(item => {
item.addEventListener('click', () => {
if (window.innerWidth <= 768) {
closeSidebar();
}
});
});
// Close sidebar when navigating (nav item click on mobile)
const navItems = sidebar.querySelectorAll('.nav-item');
navItems.forEach((item) => {
item.addEventListener('click', () => {
if (window.innerWidth <= 768) {
closeSidebar();
}
});
});
// Expose functions globally
window.sidebarToggle = {
open: openSidebar,
close: closeSidebar,
toggle: toggleSidebar
};
// Expose functions globally
window.sidebarToggle = {
open: openSidebar,
close: closeSidebar,
toggle: toggleSidebar
};
}
// Initialize sidebar toggle when DOM is ready
@@ -104,12 +103,12 @@ document.addEventListener('DOMContentLoaded', initSidebarToggle);
// Mapping of section names to their corresponding view flags
const VIEW_FLAGS = {
'files': 'isFilesView',
'shared': 'isSharedView',
'recent': 'isRecentView',
'favorites': 'isFavoritesView',
'trash': 'isTrashView',
'photos': 'isPhotosView'
files: 'isFilesView',
shared: 'isSharedView',
recent: 'isRecentView',
favorites: 'isFavoritesView',
trash: 'isTrashView',
photos: 'isPhotosView'
};
/**
@@ -128,18 +127,17 @@ function getSectionFromNavItem(navItem) {
* @returns {boolean} true if the section changed
*/
function setCurrentSection(section) {
if (window.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);
window.app[flag] = key === section;
});
window.app.currentSection = section;
// Update nav item active classes by finding matching item from DOM
window.appElements.navItems.forEach(item => {
window.appElements.navItems.forEach((item) => {
const itemSection = getSectionFromNavItem(item);
item.classList.toggle('active', itemSection === section);
});
@@ -164,12 +162,11 @@ function setCurrentSection(section) {
}
function switchToSharedSection() {
if (!setCurrentSection('shared')) return;
// Hide breadcrumb (only shown in Files view)
const breadcrumb = document.querySelector('.breadcrumb');
breadcrumb?.classList.add("hidden");
breadcrumb?.classList.add('hidden');
// Hide actions-bar for shared view
window.setActionsBarMode('hidden');
@@ -178,14 +175,13 @@ function switchToSharedSection() {
//reset files view + remove any error
window.ui.resetFilesList();
// Show shared view
if (window.sharedView) {
window.sharedView.init();
window.sharedView.show();
}
if (window.multiSelect) window.multiSelect.clear();
}
function switchToFilesSection() {
@@ -196,7 +192,7 @@ function switchToFilesSection() {
// Show breadcrumb (only in Files view)
const breadcrumb = document.querySelector('.breadcrumb');
breadcrumb?.classList.remove("hidden");
breadcrumb?.classList.remove('hidden');
// show files container
toggleFileContainer(true);
@@ -224,14 +220,14 @@ function switchToFavoritesSection() {
// Hide breadcrumb (only shown in Files view)
const breadcrumb = document.querySelector('.breadcrumb');
breadcrumb?.classList.add("hidden");
breadcrumb?.classList.add('hidden');
// show files container
toggleFileContainer(true);
// ensure correct view
syncViewContainers();
//reset files view + remove any error
window.ui.resetFilesList();
@@ -242,12 +238,10 @@ function switchToFavoritesSection() {
window.ui.showError(`
<i class="fas fa-exclamation-circle empty-state-icon error"></i>
<p>Error loading the favorites module</p>
`
);
`);
}
if (window.multiSelect) window.multiSelect.clear();
if (window.multiSelect) window.multiSelect.clear();
}
function switchToRecentFilesSection() {
@@ -258,7 +252,7 @@ function switchToRecentFilesSection() {
// Hide breadcrumb (only shown in Files view)
const breadcrumb = document.querySelector('.breadcrumb');
breadcrumb?.classList.add("hidden");
breadcrumb?.classList.add('hidden');
// show files container
toggleFileContainer(true);
@@ -276,8 +270,7 @@ function switchToRecentFilesSection() {
window.ui.showError(`
<i class="fas fa-exclamation-circle empty-state-icon error"></i>
<p>Error loading the recent module</p>
`
);
`);
}
if (window.multiSelect) window.multiSelect.clear();
}
@@ -287,7 +280,7 @@ function switchToPhotosSection() {
// Hide breadcrumb
const breadcrumb = document.querySelector('.breadcrumb');
breadcrumb?.classList.add("hidden");
breadcrumb?.classList.add('hidden');
// Hide file containers
toggleFileContainer(false);
@@ -303,7 +296,6 @@ function switchToPhotosSection() {
window.photosView.show();
}
if (window.multiSelect) window.multiSelect.clear();
}
function switchToTrashSection() {
@@ -311,7 +303,7 @@ function switchToTrashSection() {
// Hide breadcrumb (only shown in Files view)
const breadcrumb = document.querySelector('.breadcrumb');
breadcrumb?.classList.add("hidden");
breadcrumb?.classList.add('hidden');
// Show files containers (to be filled with trash)
const filesList = document.getElementById('files-list');
+1 -5
View File
@@ -11,10 +11,7 @@ async function performSearch(query, sortBy) {
app.isSearchMode = true;
window.ui.updateBreadcrumb(`Search: "${query}"`);
window.ui.showError(
`<h3><i class="fas fa-spinner fa-spin search-spinner"></i> Searching for "${query}"...</h3>`
);
window.ui.showError(`<h3><i class="fas fa-spinner fa-spin search-spinner"></i> Searching for "${query}"...</h3>`);
const options = {
recursive: true,
@@ -37,7 +34,6 @@ async function performSearch(query, sortBy) {
const searchResults = await window.search.searchFiles(query, options);
window.search.displaySearchResults(searchResults);
} catch (error) {
console.error('Search error:', error);
window.ui.showNotification('Error', 'Error performing search');
+1 -2
View File
@@ -29,5 +29,4 @@ window.app = {
viewFile: null // current file in inline view
};
window.appElements = {
};
window.appElements = {};
+11 -14
View File
@@ -8,7 +8,7 @@ async function loadTrashItems() {
try {
if (window.multiSelect) window.multiSelect.clear();
window.ui.resetFilesList(); // ensure also list visible & error hidden
const _tt = (window.i18n && window.i18n.t) ? window.i18n.t : k => k.split('.').pop();
const _tt = window.i18n && window.i18n.t ? window.i18n.t : (k) => k.split('.').pop();
elements.filesList.innerHTML = `
<div class="list-header trash-header">
<div data-i18n="files.name">${_tt('files.name')}</div>
@@ -31,10 +31,9 @@ async function loadTrashItems() {
return;
}
trashItems.forEach(item => {
trashItems.forEach((item) => {
addTrashItemToView(item);
});
} catch (error) {
console.error('Error loading trash items:', error);
window.ui.showNotification('Error', 'Error loading trash items');
@@ -54,22 +53,20 @@ function addTrashItemToView(item) {
iconClass = item.icon_class || 'fas fa-folder';
typeLabel = window.i18n ? window.i18n.t('files.file_types.folder') : 'Folder';
} else {
iconClass = item.icon_class || (window.ui && window.ui.getIconClass
? window.ui.getIconClass(item.name)
: 'fas fa-file');
iconSpecialClass = (window.ui && window.ui.getIconSpecialClass)
? window.ui.getIconSpecialClass(item.name)
: '';
iconClass = item.icon_class || (window.ui && window.ui.getIconClass ? window.ui.getIconClass(item.name) : 'fas fa-file');
iconSpecialClass = window.ui && window.ui.getIconSpecialClass ? window.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');
? window.i18n
? window.i18n.t(`files.file_types.${cat.toLowerCase()}`) || cat
: cat
: window.i18n
? window.i18n.t('files.file_types.document')
: 'Document';
}
const isFolder = !isFile;
const iconWrapClass = isFolder
? 'file-icon folder-icon'
: `file-icon ${iconSpecialClass}`.trim();
const iconWrapClass = isFolder ? 'file-icon folder-icon' : `file-icon ${iconSpecialClass}`.trim();
const listElement = document.createElement('div');
listElement.className = 'file-item trash-item';
+155 -144
View File
@@ -7,7 +7,6 @@
// UI Module
const ui = {
/** @type {HTMLDListElement | null} */
//dragPreview,
@@ -285,8 +284,8 @@ const ui = {
*/
setupDragAndDrop() {
// prepare area to build dragged elements
this.dragPreview = document.createElement("div");
this.dragPreview.className="drag-preview";
this.dragPreview = document.createElement('div');
this.dragPreview.className = 'drag-preview';
document.body.appendChild(this.dragPreview);
this.draggedItems = null;
@@ -294,9 +293,7 @@ const ui = {
const collectDroppedEntries = async (dataTransfer) => {
const items = Array.from(dataTransfer?.items || []);
const rootEntries = items
.map(it => (typeof it.webkitGetAsEntry === 'function' ? it.webkitGetAsEntry() : null))
.filter(Boolean);
const rootEntries = items.map((it) => (typeof it.webkitGetAsEntry === 'function' ? it.webkitGetAsEntry() : null)).filter(Boolean);
if (rootEntries.length === 0) return null;
@@ -356,17 +353,17 @@ const ui = {
dropzone.addEventListener('drop', async (e) => {
e.preventDefault();
e.stopPropagation(); // Prevent bubbling to document's drop handler (avoids double upload)
e._oxiHandled = true; // Mark as handled for document-level fallback
e._oxiHandled = true; // Mark as handled for document-level fallback
dropzone.classList.remove('active');
if (e.dataTransfer.files.length > 0) {
// First try directory-aware extraction (Finder folder drag & drop)
const droppedEntries = await collectDroppedEntries(e.dataTransfer);
if (droppedEntries && droppedEntries.length > 0) {
const hasFolderStructure = droppedEntries.some(x => x.relativePath && x.relativePath.includes('/'));
const hasFolderStructure = droppedEntries.some((x) => x.relativePath && x.relativePath.includes('/'));
if (hasFolderStructure) {
fileOps.uploadFolderEntries(droppedEntries);
} else {
fileOps.uploadFiles(droppedEntries.map(x => x.file));
fileOps.uploadFiles(droppedEntries.map((x) => x.file));
}
setTimeout(() => {
dropzone.style.display = 'none';
@@ -375,9 +372,7 @@ const ui = {
}
// Detect folder drops: files from folder drops have webkitRelativePath set
const hasRelativePaths = Array.from(e.dataTransfer.files).some(
f => f.webkitRelativePath && f.webkitRelativePath.includes('/')
);
const hasRelativePaths = Array.from(e.dataTransfer.files).some((f) => f.webkitRelativePath && f.webkitRelativePath.includes('/'));
if (hasRelativePaths) {
fileOps.uploadFolderFiles(e.dataTransfer.files);
} else {
@@ -399,8 +394,7 @@ const ui = {
});
document.addEventListener('dragleave', (e) => {
if (e.clientX <= 0 || e.clientY <= 0 ||
e.clientX >= window.innerWidth || e.clientY >= window.innerHeight) {
if (e.clientX <= 0 || e.clientY <= 0 || e.clientX >= window.innerWidth || e.clientY >= window.innerHeight) {
dropzone.classList.remove('active');
setTimeout(() => {
if (!dropzone.classList.contains('active')) {
@@ -421,11 +415,11 @@ const ui = {
// First try directory-aware extraction (Finder folder drag & drop)
const droppedEntries = await collectDroppedEntries(e.dataTransfer);
if (droppedEntries && droppedEntries.length > 0) {
const hasFolderStructure = droppedEntries.some(x => x.relativePath && x.relativePath.includes('/'));
const hasFolderStructure = droppedEntries.some((x) => x.relativePath && x.relativePath.includes('/'));
if (hasFolderStructure) {
fileOps.uploadFolderEntries(droppedEntries);
} else {
fileOps.uploadFiles(droppedEntries.map(x => x.file));
fileOps.uploadFiles(droppedEntries.map((x) => x.file));
}
setTimeout(() => {
dropzone.style.display = 'none';
@@ -434,9 +428,7 @@ const ui = {
}
// Detect folder drops: files from folder drops have webkitRelativePath set
const hasRelativePaths = Array.from(e.dataTransfer.files).some(
f => f.webkitRelativePath && f.webkitRelativePath.includes('/')
);
const hasRelativePaths = Array.from(e.dataTransfer.files).some((f) => f.webkitRelativePath && f.webkitRelativePath.includes('/'));
if (hasRelativePaths) {
fileOps.uploadFolderFiles(e.dataTransfer.files);
} else {
@@ -514,7 +506,10 @@ const ui = {
// TODO clarify the difference between homeIcon & this first element
if (window.app.userHomeFolderName) {
if (path.length === 0 || path[0].id !== window.app.userHomeFolderId) {
path.unshift({ name: window.app.userHomeFolderName, id: window.app.userHomeFolderId});
path.unshift({
name: window.app.userHomeFolderName,
id: window.app.userHomeFolderId
});
}
}
@@ -547,7 +542,7 @@ const ui = {
// can drag files on this folder
// dragover – only folders are valid drop targets
item.addEventListener('dragover', (e) => {
const card = e.target.closest("span");
const card = e.target.closest('span');
if (!card || !card.dataset.folderId) return;
e.preventDefault();
card.classList.add('drop-target');
@@ -555,15 +550,15 @@ const ui = {
// dragleave
item.addEventListener('dragleave', (e) => {
console.log( "dragleave ",e );
const card = e.target.closest("span");
console.log('dragleave ', e);
const card = e.target.closest('span');
if (!card || !card.dataset.folderId) return;
card.classList.remove('drop-target');
});
// drop – only folders accept drops
item.addEventListener('drop', async (e) => {
const card = e.target.closest("span");
const card = e.target.closest('span');
if (!card) return;
const targetFolderId = card.dataset.folderId;
if (!targetFolderId) return;
@@ -572,7 +567,7 @@ const ui = {
card.classList.remove('drop-target');
const action = e.dataTransfer?.dropEffect;
await self._dropToFolder( action, targetFolderId, e.dataTransfer );
await self._dropToFolder(action, targetFolderId, e.dataTransfer);
});
} else {
// Last segment: current location, not clickable
@@ -639,8 +634,6 @@ const ui = {
}
},
/* ================================================================
* Data store + event delegation (replaces per-item listeners)
* ================================================================ */
@@ -666,42 +659,42 @@ const ui = {
},
/**
*
* @param {Object[]} folders
* @returns
*
* @param {Object[]} folders
* @returns
*/
_renderFoldersToView(folders) {
if (!Array.isArray(folders) || folders.length === 0) return;
const target = document.getElementById('files-list')
const target = document.getElementById('files-list');
if (!target) return;
const frag = document.createDocumentFragment();
for (const folder of folders) {
frag.appendChild( this._createFolderItem(folder));
frag.appendChild(this._createFolderItem(folder));
}
target.appendChild(frag);
},
/**
*
* @param {Object[]} files
* @returns
/**
*
* @param {Object[]} files
* @returns
*/
_renderFilesToView(files) {
if (!Array.isArray(files) || files.length === 0) return;
const target = document.getElementById('files-list')
const target = document.getElementById('files-list');
if (!target) return;
const frag = document.createDocumentFragment();
for (const file of files) {
frag.appendChild( this._createFileItem(file))
frag.appendChild(this._createFileItem(file));
}
target.appendChild(frag);
},
_upsertById(arr, item) {
if (!Array.isArray(arr) || !item || !item.id) return;
const idx = arr.findIndex(x => x && x.id === item.id);
const idx = arr.findIndex((x) => x && x.id === item.id);
if (idx >= 0) {
arr[idx] = item;
} else {
@@ -721,21 +714,19 @@ const ui = {
window.multiSelect.clear();
if (selection.fileIds.length == 0 && selection.folderIds.length == 0) {
// try to use dataTransfer (direct move without selection)
const id = dataTransfer.getData('text/plain');
const isFolder = dataTransfer.getData('application/oxicloud-folder') === 'true';
if (isFolder && id === targetFolderId) {
console.log("nothing to do");
console.log('nothing to do');
return; //nothing to do
}
// append current item to selection
if (isFolder) {
console.log(`adding ${id} as folder`);
selection.folderIds.push(id);
}
else {
} else {
console.log(`adding ${id} as file`);
selection.fileIds.push(id);
}
@@ -750,15 +741,14 @@ const ui = {
let result;
switch (action) {
case "copy":
case 'copy':
result = await window.fileOps.batchCopy(selection.fileIds, selection.folderIds, targetFolderId);
break;
case "move":
case 'move':
result = await window.fileOps.batchMove(selection.fileIds, selection.folderIds, targetFolderId);
// redraw directory
if (result.success > 0)
window.loadFiles();
if (result.success > 0) window.loadFiles();
break;
default:
@@ -766,7 +756,7 @@ const ui = {
return;
}
window.multiSelect.showBatchResult(action, result);
console.log( result);
console.log(result);
},
_hydrateViewIfNeeded() {
@@ -801,9 +791,21 @@ const ui = {
const itemInfo = (card) => {
if (!card) return null;
const fileId = card.dataset.fileId;
if (fileId) return { type: 'file', id: fileId, name: card.dataset.fileName, data: self._items.get(fileId) };
if (fileId)
return {
type: 'file',
id: fileId,
name: card.dataset.fileName,
data: self._items.get(fileId)
};
const folderId = card.dataset.folderId;
if (folderId) return { type: 'folder', id: folderId, name: card.dataset.folderName, data: self._items.get(folderId) };
if (folderId)
return {
type: 'folder',
id: folderId,
name: card.dataset.folderName,
data: self._items.get(folderId)
};
return null;
};
@@ -815,7 +817,7 @@ const ui = {
// WOPI editor intercept: open Office documents in the WOPI editor
// But NOT image files - those should be previewed in the inline viewer
const ext = (file.name || '').split('.').pop().toLowerCase();
const imageExts = ['jpg','jpeg','png','gif','svg','webp','bmp','ico','heic','heif','avif','tiff'];
const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'bmp', 'ico', 'heic', 'heif', 'avif', 'tiff'];
const isImage = (file.mime_type && file.mime_type.startsWith('image/')) || imageExts.includes(ext);
try {
if (!isImage && window.wopiEditor && await window.wopiEditor.canEdit(file.name)) {
@@ -856,13 +858,13 @@ const ui = {
window.app.contextMenuTargetFolder = {
id: info.id,
name: card.dataset.folderName,
parent_id: card.dataset.parentId || ""
parent_id: card.dataset.parentId || ''
};
} else {
window.app.contextMenuTargetFile = {
id: info.id,
name: card.dataset.fileName,
folder_id: card.dataset.folderId || ""
folder_id: card.dataset.folderId || ''
};
}
};
@@ -878,10 +880,8 @@ const ui = {
const info = itemInfo(card);
if (!info) return;
setContextTarget(card, info);
const menuId = info.type === 'folder'
? 'folder-context-menu' : 'file-context-menu';
showContextMenuAtElement(
e.target.closest('.file-actions'), menuId);
const menuId = info.type === 'folder' ? 'folder-context-menu' : 'file-context-menu';
showContextMenuAtElement(e.target.closest('.file-actions'), menuId);
return;
}
@@ -926,7 +926,7 @@ const ui = {
});
// ── shared events ──────────────────────
filesList.addEventListener('contextmenu', (e) => {
const card = e.target.closest('.file-item');
if (!card) return;
@@ -934,97 +934,100 @@ const ui = {
const info = itemInfo(card);
if (!info) return;
setContextTarget(card, info);
const menuId = info.type === 'folder'
? 'folder-context-menu' : 'file-context-menu';
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 (window.contextMenus && typeof window.contextMenus.syncWopiOptionVisibility === 'function') {
window.contextMenus.syncWopiOptionVisibility().catch(function(){});
window.contextMenus.syncWopiOptionVisibility().catch(function () {});
}
menu.style.left = `${e.pageX}px`;
menu.style.top = `${e.pageY}px`;
menu.style.top = `${e.pageY}px`;
menu.style.display = 'block';
});
// dragstart
filesList.addEventListener('dragstart', (e) => {
let card = e.target.closest('.file-item');
if (!card) { e.preventDefault(); return; }
if (!card) {
e.preventDefault();
return;
}
const info = itemInfo(card);
if (!info) { e.preventDefault(); return; }
if (!info) {
e.preventDefault();
return;
}
e.dataTransfer.setData('text/plain', info.id);
if (info.type === 'folder') {
e.dataTransfer.setData(
'application/oxicloud-folder', 'true');
if (info.type === 'folder') {
e.dataTransfer.setData('application/oxicloud-folder', 'true');
}
// allow copy or move (handled by the browser)
e.dataTransfer.effectAllowed = "copyMove";
self.draggedItems = document.createElement("div");
self.draggedItems.className = "dragged-items";
e.dataTransfer.effectAllowed = 'copyMove';
self.draggedItems = document.createElement('div');
self.draggedItems.className = 'dragged-items';
let selectedCardFromList = filesList.querySelectorAll(`div.selected > div.name-cell`);
if (selectedCardFromList.length == 0) {
// fallback to current element
selectedCardFromList = card.querySelectorAll('div.name-cell');
}
let index = 0;
const maxElements = 4;
let lastItemDiv = null;
while (index < selectedCardFromList.length && index < maxElements) {
let iconCell = document.createElement("div");
let icon = selectedCardFromList[index].getElementsByClassName("file-icon").item(0)?.cloneNode(true);
let iconCell = document.createElement('div');
let icon = selectedCardFromList[index].getElementsByClassName('file-icon').item(0)?.cloneNode(true);
if (icon) {
iconCell.appendChild( icon);
iconCell.querySelectorAll('img')?.forEach((img) => { img.loading="eager"; } );
iconCell.appendChild(icon);
iconCell.querySelectorAll('img')?.forEach((img) => {
img.loading = 'eager';
});
}
let nameCell = document.createElement("div");
let name = selectedCardFromList[index].getElementsByTagName("span").item(0)?.cloneNode(true);
let nameCell = document.createElement('div');
let name = selectedCardFromList[index].getElementsByTagName('span').item(0)?.cloneNode(true);
if (name) {
nameCell.appendChild(name);
}
let div = document.createElement("div");
div.className="file-item";
let div = document.createElement('div');
div.className = 'file-item';
div.appendChild(iconCell);
div.appendChild(nameCell);
self.draggedItems.appendChild( div);
self.draggedItems.appendChild(div);
index += 1;
lastItemDiv = div;
}
// if more than 1 item, display the badge
if (selectedCardFromList.length > 1) {
let badge = document.createElement("span");
badge.className="dragged-items-badge";
badge.innerText=`${selectedCardFromList.length}`;
self.draggedItems.appendChild( badge);
let badge = document.createElement('span');
badge.className = 'dragged-items-badge';
badge.innerText = `${selectedCardFromList.length}`;
self.draggedItems.appendChild(badge);
}
// if more than maxElements display the fading
if (selectedCardFromList.length > maxElements) {
lastItemDiv.classList.add("fading");
lastItemDiv.classList.add('fading');
}
self.dragPreview.appendChild( self.draggedItems);
self.dragPreview.appendChild(self.draggedItems);
e.dataTransfer.setDragImage(self.draggedItems, 0, 0);
});
// dragend
filesList.addEventListener('dragend', (e) => {
self.dragPreview.removeChild( self.draggedItems);
document.querySelectorAll('.drop-target')
.forEach(el => el.classList.remove('drop-target'));
self.dragPreview.removeChild(self.draggedItems);
document.querySelectorAll('.drop-target').forEach((el) => el.classList.remove('drop-target'));
});
// dragover – only folders are valid drop targets
@@ -1054,9 +1057,8 @@ const ui = {
card.classList.remove('drop-target');
const action = e.dataTransfer.dropEffect;
await self._dropToFolder( action, targetFolderId, e.dataTransfer);
await self._dropToFolder(action, targetFolderId, e.dataTransfer);
});
},
/* ================================================================
@@ -1074,7 +1076,7 @@ const ui = {
if (!window.favorites) return;
const itemId = star.dataset.itemId;
const itemId = star.dataset.itemId;
const itemType = star.dataset.itemType;
const itemName = star.dataset.itemName;
@@ -1099,9 +1101,7 @@ const ui = {
* Sync favorite visuals for a file/folder across grid and list views.
*/
setFavoriteVisualState(itemId, itemType, isFavorite) {
const selector = itemType === 'folder'
? `#files-list .file-item[data-folder-id="${itemId}"]`
: `#files-list .file-item[data-file-id="${itemId}"]`;
const selector = itemType === 'folder' ? `#files-list .file-item[data-folder-id="${itemId}"]` : `#files-list .file-item[data-file-id="${itemId}"]`;
const card = document.querySelector(selector);
const starBtn = card ? card.querySelector('.favorite-star') : null;
@@ -1127,7 +1127,7 @@ const ui = {
i.classList.add(isFavorite ? 'fas' : 'far');
}
}
const listItem = document.querySelector(selector);
if (listItem) {
const nameCell = listItem.querySelector('.name-cell');
@@ -1155,9 +1155,9 @@ const ui = {
_createFolderItem(folder) {
const el = document.createElement('div');
el.className = 'file-item';
el.dataset.folderId = folder.id;
el.dataset.folderId = folder.id;
el.dataset.folderName = folder.name;
el.dataset.parentId = folder.parent_id || "";
el.dataset.parentId = folder.parent_id || '';
const isFav = window.favorites && window.favorites.isFavorite(folder.id, 'folder');
const formattedDate = window.formatDateTime(folder.modified_at);
@@ -1182,7 +1182,7 @@ const ui = {
</div>
`;
if (window.app.currentPath !== "") {
if (window.app.currentPath !== '') {
el.setAttribute('draggable', 'true');
}
this._bindStarClick(el);
@@ -1195,17 +1195,21 @@ const ui = {
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');
? 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 && window.favorites.isFavorite(file.id, 'file');
const el = document.createElement('div');
el.className = 'file-item';
el.dataset.fileId = file.id;
el.dataset.fileId = file.id;
el.dataset.fileName = file.name;
el.dataset.folderId = file.folder_id || "";
el.dataset.folderId = file.folder_id || '';
el.setAttribute('draggable', 'true');
el.innerHTML = `
@@ -1230,7 +1234,10 @@ const ui = {
</div>
`;
var thumb = el.querySelector('.file-thumb');
if (thumb) thumb.addEventListener('error', function() { this.style.display = 'none'; });
if (thumb)
thumb.addEventListener('error', function () {
this.style.display = 'none';
});
this._bindStarClick(el);
return el;
},
@@ -1241,11 +1248,11 @@ const ui = {
resetFilesList() {
const filesList = document.getElementById('files-list');
const filesContainerError=document.getElementById("files-container-error");
const filesContainerError = document.getElementById('files-container-error');
if (!filesList) return;
filesList.innerHTML=`
filesList.innerHTML = `
<div class="list-header">
<div class="list-header-checkbox"><input type="checkbox" id="select-all-checkbox" title="Select all"></div>
<div data-i18n="files.name">Name</div>
@@ -1254,12 +1261,11 @@ const ui = {
<div data-i18n="files.modified">Modified</div>
<div></div><!-- actions -->
</div>`;
if (window.i18n && window.i18n.translateElement)
window.i18n.translateElement( filesList);
filesList.classList.remove("hidden");
filesContainerError?.classList.add("hidden");
if (window.i18n && window.i18n.translateElement) window.i18n.translateElement(filesList);
filesList.classList.remove('hidden');
filesContainerError?.classList.add('hidden');
},
showEmptyList() {
@@ -1267,8 +1273,7 @@ const ui = {
<i class="fas fa-folder-open empty-state-icon"></i>
<p data-i18n="files.no_files"></p>
<p data-i18n="files.empty_hint"></p>
`
);
`);
},
/**
@@ -1277,16 +1282,14 @@ const ui = {
*
*/
showError(content) {
const filesContainerError=document.getElementById("files-container-error");
const filesList=document.getElementById("files-list");
if (filesContainerError)
filesContainerError.innerHTML=content;
const filesContainerError = document.getElementById('files-container-error');
const filesList = document.getElementById('files-list');
if (filesContainerError) filesContainerError.innerHTML = content;
if (window.i18n && window.i18n.translateElement)
window.i18n.translateElement( filesContainerError);
if (window.i18n && window.i18n.translateElement) window.i18n.translateElement(filesContainerError);
filesContainerError?.classList.remove("hidden");
filesList?.classList.add("hidden");
filesContainerError?.classList.remove('hidden');
filesList?.classList.add('hidden');
},
/**
@@ -1381,7 +1384,7 @@ function toggleCardSelection(card, event) {
*/
function showContextMenuAtElement(triggerElement, menuId) {
// Hide any open menus first
document.querySelectorAll('.context-menu').forEach(m => m.style.display = 'none');
document.querySelectorAll('.context-menu').forEach((m) => (m.style.display = 'none'));
const menu = document.getElementById(menuId);
if (!menu) return;
@@ -1403,7 +1406,7 @@ function showContextMenuAtElement(triggerElement, menuId) {
window.contextMenus.syncFavoriteOptionLabels();
}
if (window.contextMenus && typeof window.contextMenus.syncWopiOptionVisibility === 'function') {
window.contextMenus.syncWopiOptionVisibility().catch(function(){});
window.contextMenus.syncWopiOptionVisibility().catch(function () {});
}
menu.style.left = `${left}px`;
@@ -1426,7 +1429,8 @@ function initRubberBandSelection() {
}
let active = false;
let startX = 0, startY = 0;
let startX = 0,
startY = 0;
// We listen on the whole files-container (covers grid + empty space)
const container = document.querySelector('.files-container') || document.getElementById('files-list');
@@ -1435,11 +1439,16 @@ function initRubberBandSelection() {
container.addEventListener('mousedown', (e) => {
// Only start if clicking empty area (not on a card, button, menu, input…)
if (e.button !== 0) return; // left click only
if (e.target.closest('.file-item') ||
if (
e.target.closest('.file-item') ||
e.target.closest('.context-menu') ||
e.target.closest('.upload-dropdown') || e.target.closest('button') ||
e.target.closest('input') || e.target.closest('.breadcrumb') ||
e.target.closest('.list-header')) return;
e.target.closest('.upload-dropdown') ||
e.target.closest('button') ||
e.target.closest('input') ||
e.target.closest('.breadcrumb') ||
e.target.closest('.list-header')
)
return;
active = true;
startX = e.clientX;
@@ -1478,13 +1487,10 @@ function initRubberBandSelection() {
// Highlight cards that intersect with the rectangle
const rectBounds = { left, top, right: left + width, bottom: top + height };
document.querySelectorAll('#files-list .file-item').forEach(card => {
document.querySelectorAll('#files-list .file-item').forEach((card) => {
const cardRect = card.getBoundingClientRect();
const intersects =
cardRect.left < rectBounds.right &&
cardRect.right > rectBounds.left &&
cardRect.top < rectBounds.bottom &&
cardRect.bottom > rectBounds.top;
cardRect.left < rectBounds.right && cardRect.right > rectBounds.left && cardRect.top < rectBounds.bottom && cardRect.bottom > rectBounds.top;
if (intersects) {
card.classList.add('selected');
@@ -1498,7 +1504,7 @@ function initRubberBandSelection() {
card.classList.remove('selected');
// Deselect from multiSelect module
if (window.multiSelect) {
const info = window.multiSelect._extractInfo(card)
const info = window.multiSelect._extractInfo(card);
if (info) window.multiSelect.deselect(info.id);
}
}
@@ -1516,7 +1522,9 @@ function initRubberBandSelection() {
// deselect handler doesn't immediately clear the selection.
if (hadSelection) {
window.__rubberBandJustFinished = true;
requestAnimationFrame(() => { window.__rubberBandJustFinished = false; });
requestAnimationFrame(() => {
window.__rubberBandJustFinished = false;
});
}
});
}
@@ -1533,7 +1541,6 @@ window.toggleCardSelection = toggleCardSelection;
window.showContextMenuAtElement = showContextMenuAtElement;
window.initRubberBandSelection = initRubberBandSelection;
/**
* Show a modern confirm dialog (replaces native confirm())
* @param {Object} options
@@ -1573,7 +1580,9 @@ function showConfirmDialog({ title, message, confirmText, cancelText, danger = t
document.body.appendChild(overlay);
// Force layout then show
requestAnimationFrame(() => { overlay.classList.add('active'); });
requestAnimationFrame(() => {
overlay.classList.add('active');
});
const cleanup = (result) => {
overlay.classList.remove('active');
@@ -1583,7 +1592,9 @@ function showConfirmDialog({ title, message, confirmText, cancelText, danger = t
overlay.querySelector('.confirm-dialog-cancel').addEventListener('click', () => cleanup(false));
overlay.querySelector('.confirm-dialog-ok').addEventListener('click', () => cleanup(true));
overlay.addEventListener('click', (e) => { if (e.target === overlay) cleanup(false); });
overlay.addEventListener('click', (e) => {
if (e.target === overlay) cleanup(false);
});
});
}
window.showConfirmDialog = showConfirmDialog;
+139 -47
View File
@@ -18,24 +18,57 @@ const uiFileTypes = {
if (!fileName) return 'fas fa-file';
const ext = (fileName.split('.').pop() || '').toLowerCase();
const map = {
pdf:'fas fa-file-pdf', doc:'fas fa-file-word', docx:'fas fa-file-word',
txt:'fas fa-file-alt', rtf:'fas fa-file-alt', odt:'fas fa-file-alt',
xls:'fas fa-file-excel', xlsx:'fas fa-file-excel', csv:'fas fa-file-excel', ods:'fas fa-file-excel',
ppt:'fas fa-file-powerpoint', pptx:'fas fa-file-powerpoint', odp:'fas fa-file-powerpoint',
jpg:'fas fa-file-image', jpeg:'fas fa-file-image', png:'fas fa-file-image',
gif:'fas fa-file-image', svg:'fas fa-file-image', webp:'fas fa-file-image',
bmp:'fas fa-file-image', ico:'fas fa-file-image',
mp4:'fas fa-file-video', avi:'fas fa-file-video', mov:'fas fa-file-video',
mkv:'fas fa-file-video', webm:'fas fa-file-video', flv:'fas fa-file-video',
mp3:'fas fa-file-audio', wav:'fas fa-file-audio', ogg:'fas fa-file-audio',
flac:'fas fa-file-audio', aac:'fas fa-file-audio', m4a:'fas fa-file-audio',
zip:'fas fa-file-archive', rar:'fas fa-file-archive', '7z':'fas fa-file-archive',
tar:'fas fa-file-archive', gz:'fas fa-file-archive',
js:'fas fa-file-code', ts:'fas fa-file-code', py:'fas fa-file-code',
rs:'fas fa-file-code', java:'fas fa-file-code', html:'fas fa-file-code',
css:'fas fa-file-code', json:'fas fa-file-code', xml:'fas fa-file-code',
sh:'fas fa-terminal', bash:'fas fa-terminal', bat:'fas fa-terminal',
md:'fas fa-file-alt',
pdf: 'fas fa-file-pdf',
doc: 'fas fa-file-word',
docx: 'fas fa-file-word',
txt: 'fas fa-file-alt',
rtf: 'fas fa-file-alt',
odt: 'fas fa-file-alt',
xls: 'fas fa-file-excel',
xlsx: 'fas fa-file-excel',
csv: 'fas fa-file-excel',
ods: 'fas fa-file-excel',
ppt: 'fas fa-file-powerpoint',
pptx: 'fas fa-file-powerpoint',
odp: 'fas fa-file-powerpoint',
jpg: 'fas fa-file-image',
jpeg: 'fas fa-file-image',
png: 'fas fa-file-image',
gif: 'fas fa-file-image',
svg: 'fas fa-file-image',
webp: 'fas fa-file-image',
bmp: 'fas fa-file-image',
ico: 'fas fa-file-image',
mp4: 'fas fa-file-video',
avi: 'fas fa-file-video',
mov: 'fas fa-file-video',
mkv: 'fas fa-file-video',
webm: 'fas fa-file-video',
flv: 'fas fa-file-video',
mp3: 'fas fa-file-audio',
wav: 'fas fa-file-audio',
ogg: 'fas fa-file-audio',
flac: 'fas fa-file-audio',
aac: 'fas fa-file-audio',
m4a: 'fas fa-file-audio',
zip: 'fas fa-file-archive',
rar: 'fas fa-file-archive',
'7z': 'fas fa-file-archive',
tar: 'fas fa-file-archive',
gz: 'fas fa-file-archive',
js: 'fas fa-file-code',
ts: 'fas fa-file-code',
py: 'fas fa-file-code',
rs: 'fas fa-file-code',
java: 'fas fa-file-code',
html: 'fas fa-file-code',
css: 'fas fa-file-code',
json: 'fas fa-file-code',
xml: 'fas fa-file-code',
sh: 'fas fa-terminal',
bash: 'fas fa-terminal',
bat: 'fas fa-terminal',
md: 'fas fa-file-alt'
};
return map[ext] || 'fas fa-file';
},
@@ -44,35 +77,94 @@ const uiFileTypes = {
if (!fileName) return '';
const ext = (fileName.split('.').pop() || '').toLowerCase();
const map = {
pdf:'pdf-icon',
doc:'doc-icon', docx:'doc-icon', odt:'doc-icon', rtf:'doc-icon',
xls:'spreadsheet-icon', xlsx:'spreadsheet-icon', ods:'spreadsheet-icon', csv:'spreadsheet-icon',
ppt:'presentation-icon', pptx:'presentation-icon', odp:'presentation-icon', key:'presentation-icon',
jpg:'image-icon', jpeg:'image-icon', png:'image-icon', gif:'image-icon',
svg:'image-icon', webp:'image-icon', bmp:'image-icon', ico:'image-icon',
heic:'image-icon', heif:'image-icon', avif:'image-icon', tiff:'image-icon',
mp4:'video-icon', avi:'video-icon', mkv:'video-icon', mov:'video-icon',
wmv:'video-icon', flv:'video-icon', webm:'video-icon', m4v:'video-icon',
mp3:'audio-icon', wav:'audio-icon', ogg:'audio-icon', flac:'audio-icon',
aac:'audio-icon', wma:'audio-icon', m4a:'audio-icon', opus:'audio-icon',
zip:'archive-icon', rar:'archive-icon', '7z':'archive-icon',
tar:'archive-icon', gz:'archive-icon', bz2:'archive-icon', xz:'archive-icon',
exe:'installer-icon', msi:'installer-icon', dmg:'installer-icon',
deb:'installer-icon', rpm:'installer-icon', appimage:'installer-icon',
py:'code-icon py-icon', rs:'code-icon rust-icon', go:'code-icon go-icon',
js:'code-icon js-icon', jsx:'code-icon js-icon', mjs:'code-icon js-icon',
ts:'code-icon ts-icon', tsx:'code-icon ts-icon',
java:'code-icon java-icon', c:'code-icon c-icon', cpp:'code-icon c-icon',
cs:'code-icon cs-icon', rb:'code-icon ruby-icon', php:'code-icon php-icon',
swift:'code-icon swift-icon',
html:'code-icon html-icon', htm:'code-icon html-icon',
css:'code-icon css-icon', scss:'code-icon css-icon',
json:'code-icon json-icon', xml:'code-icon html-icon',
yaml:'code-icon config-icon', yml:'code-icon config-icon',
toml:'code-icon config-icon', ini:'code-icon config-icon',
sql:'code-icon sql-icon', vue:'code-icon js-icon', svelte:'code-icon js-icon',
sh:'script-icon', bash:'script-icon', zsh:'script-icon', bat:'script-icon',
md:'code-icon md-icon', txt:'doc-icon',
pdf: 'pdf-icon',
doc: 'doc-icon',
docx: 'doc-icon',
odt: 'doc-icon',
rtf: 'doc-icon',
xls: 'spreadsheet-icon',
xlsx: 'spreadsheet-icon',
ods: 'spreadsheet-icon',
csv: 'spreadsheet-icon',
ppt: 'presentation-icon',
pptx: 'presentation-icon',
odp: 'presentation-icon',
key: 'presentation-icon',
jpg: 'image-icon',
jpeg: 'image-icon',
png: 'image-icon',
gif: 'image-icon',
svg: 'image-icon',
webp: 'image-icon',
bmp: 'image-icon',
ico: 'image-icon',
heic: 'image-icon',
heif: 'image-icon',
avif: 'image-icon',
tiff: 'image-icon',
mp4: 'video-icon',
avi: 'video-icon',
mkv: 'video-icon',
mov: 'video-icon',
wmv: 'video-icon',
flv: 'video-icon',
webm: 'video-icon',
m4v: 'video-icon',
mp3: 'audio-icon',
wav: 'audio-icon',
ogg: 'audio-icon',
flac: 'audio-icon',
aac: 'audio-icon',
wma: 'audio-icon',
m4a: 'audio-icon',
opus: 'audio-icon',
zip: 'archive-icon',
rar: 'archive-icon',
'7z': 'archive-icon',
tar: 'archive-icon',
gz: 'archive-icon',
bz2: 'archive-icon',
xz: 'archive-icon',
exe: 'installer-icon',
msi: 'installer-icon',
dmg: 'installer-icon',
deb: 'installer-icon',
rpm: 'installer-icon',
appimage: 'installer-icon',
py: 'code-icon py-icon',
rs: 'code-icon rust-icon',
go: 'code-icon go-icon',
js: 'code-icon js-icon',
jsx: 'code-icon js-icon',
mjs: 'code-icon js-icon',
ts: 'code-icon ts-icon',
tsx: 'code-icon ts-icon',
java: 'code-icon java-icon',
c: 'code-icon c-icon',
cpp: 'code-icon c-icon',
cs: 'code-icon cs-icon',
rb: 'code-icon ruby-icon',
php: 'code-icon php-icon',
swift: 'code-icon swift-icon',
html: 'code-icon html-icon',
htm: 'code-icon html-icon',
css: 'code-icon css-icon',
scss: 'code-icon css-icon',
json: 'code-icon json-icon',
xml: 'code-icon html-icon',
yaml: 'code-icon config-icon',
yml: 'code-icon config-icon',
toml: 'code-icon config-icon',
ini: 'code-icon config-icon',
sql: 'code-icon sql-icon',
vue: 'code-icon js-icon',
svelte: 'code-icon js-icon',
sh: 'script-icon',
bash: 'script-icon',
zsh: 'script-icon',
bat: 'script-icon',
md: 'code-icon md-icon',
txt: 'doc-icon'
};
return map[ext] || '';
}
+7 -1
View File
@@ -16,7 +16,13 @@ const uiNotifications = {
} else if (normalizedTitle.includes('favorite') || normalizedTitle.includes('favorit') || normalizedTitle.includes('fav')) {
icon = 'fa-star';
iconClass = 'success';
} else if (normalizedTitle.includes('delete') || normalizedTitle.includes('removed') || normalizedTitle.includes('trash') || normalizedTitle.includes('rename') || normalizedTitle.includes('complete')) {
} else if (
normalizedTitle.includes('delete') ||
normalizedTitle.includes('removed') ||
normalizedTitle.includes('trash') ||
normalizedTitle.includes('rename') ||
normalizedTitle.includes('complete')
) {
icon = 'fa-check-circle';
iconClass = 'success';
}
+18 -11
View File
@@ -31,9 +31,15 @@ function setupUserMenu() {
const USER_DATA_KEY = 'oxicloud_user';
const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}');
const isAdmin = userData.role === 'admin';
if (adminBtn) { isAdmin ? adminBtn.classList.remove('hidden') : adminBtn.classList.add('hidden'); }
if (adminDivider) { isAdmin ? adminDivider.classList.remove('hidden') : adminDivider.classList.add('hidden'); }
if (roleBadge) { isAdmin ? roleBadge.classList.remove('hidden') : roleBadge.classList.add('hidden'); }
if (adminBtn) {
isAdmin ? adminBtn.classList.remove('hidden') : adminBtn.classList.add('hidden');
}
if (adminDivider) {
isAdmin ? adminDivider.classList.remove('hidden') : adminDivider.classList.add('hidden');
}
if (roleBadge) {
isAdmin ? roleBadge.classList.remove('hidden') : roleBadge.classList.add('hidden');
}
}
});
@@ -96,10 +102,7 @@ function setupUserMenu() {
}
}
window.ui.showNotification(
newIsDark ? '🌙' : '☀️',
newIsDark ? 'Dark mode enabled' : 'Light mode enabled'
);
window.ui.showNotification(newIsDark ? '🌙' : '☀️', newIsDark ? 'Dark mode enabled' : 'Light mode enabled');
});
}
@@ -165,7 +168,7 @@ function updateUserMenuData() {
}
const usedBytes = userData.storage_used_bytes || 0;
const quotaBytes = userData.storage_quota_bytes == null ? (10 * 1024 * 1024 * 1024) : userData.storage_quota_bytes;
const quotaBytes = userData.storage_quota_bytes == null ? 10 * 1024 * 1024 * 1024 : userData.storage_quota_bytes;
const percentage = quotaBytes > 0 ? Math.min(Math.round((usedBytes / quotaBytes) * 100), 100) : 0;
if (storageFill) storageFill.style.width = percentage + '%';
@@ -199,11 +202,11 @@ function showUserProfileModal() {
const role = userData.role || 'user';
const initials = username.substring(0, 2).toUpperCase();
const usedBytes = userData.storage_used_bytes || 0;
const quotaBytes = userData.storage_quota_bytes == null ? (10 * 1024 * 1024 * 1024) : userData.storage_quota_bytes;
const quotaBytes = userData.storage_quota_bytes == null ? 10 * 1024 * 1024 * 1024 : userData.storage_quota_bytes;
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 && window.i18n.t) ? window.i18n.t(key) || fallback : fallback;
const t = (key, fallback) => (window.i18n && window.i18n.t ? window.i18n.t(key) || fallback : fallback);
const existing = document.getElementById('profile-modal-overlay');
if (existing) existing.remove();
@@ -268,7 +271,11 @@ async function logout() {
// cleared before redirecting, otherwise the login page's session probe
// will refresh the token and redirect back to the app).
try {
await fetch('/api/auth/logout', { method: 'POST', credentials: 'same-origin', headers: getCsrfHeaders() });
await fetch('/api/auth/logout', {
method: 'POST',
credentials: 'same-origin',
headers: getCsrfHeaders()
});
} catch (_) {
// Best-effort
}