/**
* OxiCloud - Main Application
* This file contains the core functionality, initialization and state management
*/
/**
* Escape HTML special characters to prevent XSS attacks.
* Use this whenever inserting user-provided text (file names, folder names, etc.) into HTML.
* @param {string} str - The string to escape
* @returns {string} The escaped string safe for HTML insertion
*/
function escapeHtml(str) {
if (typeof str !== 'string') return '';
return str
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
window.escapeHtml = escapeHtml;
// Global state
const app = {
currentView: 'grid', // Current view mode: 'grid' or 'list'
currentPath: '', // Current folder path
currentFolder: null, // Current folder object
contextMenuTargetFolder: null, // Target folder for context menu
contextMenuTargetFile: null, // Target file for context menu
selectedTargetFolderId: "", // Selected target folder for move operations
moveDialogMode: 'file', // Move dialog mode: 'file' or 'folder'
isTrashView: false, // Whether we're in trash view
isSharedView: false, // Whether we're in shared view
isFavoritesView: false, // Whether we're in favorites view
isRecentView: false, // Whether we're in recent files view
currentSection: 'files', // Current section: 'files', 'trash', 'shared', 'favorites' or 'recent'
isSearchMode: false, // Whether we're in search mode
// File sharing related properties
shareDialogItem: null, // Item being shared in share dialog
shareDialogItemType: null, // Type of item being shared ('file' or 'folder')
notificationShareUrl: null // URL for notification dialog
};
// DOM elements
const elements = {
// Will be populated on initialization
};
// Upload dropdown listener state (prevents accumulated listeners)
let uploadDropdownDocumentClickHandler = null;
let uploadDropdownBindingsController = null;
let actionsBarDelegationBound = false;
const ACTIONS_BAR_TEMPLATES = {
files: `
`,
trash: `
`,
favorites: `
`,
recent: `
`
};
function setActionsBarMode(mode, force = false) {
if (!elements.actionsBar) return;
if (mode === 'hidden') {
elements.actionsBar.style.display = 'none';
elements.actionsBar.dataset.mode = 'hidden';
return;
}
if (!force && elements.actionsBar.dataset.mode === mode) {
return;
}
const html = ACTIONS_BAR_TEMPLATES[mode];
if (!html) return;
elements.actionsBar.innerHTML = html;
elements.actionsBar.style.display = 'flex';
elements.actionsBar.dataset.mode = mode;
// Refresh cached action elements after rebuild
elements.uploadBtn = document.getElementById('upload-btn');
elements.newFolderBtn = document.getElementById('new-folder-btn');
elements.gridViewBtn = document.getElementById('grid-view-btn');
elements.listViewBtn = document.getElementById('list-view-btn');
if (window.i18n && window.i18n.translateElement) {
window.i18n.translateElement(elements.actionsBar);
}
if (mode === 'files') {
setupUploadDropdown();
}
}
function setupActionsBarDelegation() {
if (actionsBarDelegationBound || !elements.actionsBar) return;
actionsBarDelegationBound = true;
elements.actionsBar.addEventListener('click', async (e) => {
const btn = e.target.closest('button');
if (!btn) return;
switch (btn.id) {
case 'upload-files-btn': {
e.stopPropagation();
const menu = document.getElementById('upload-dropdown-menu');
if (menu) menu.classList.remove('show');
if (elements.fileInput) elements.fileInput.click();
break;
}
case 'upload-folder-btn': {
e.stopPropagation();
const menu = document.getElementById('upload-dropdown-menu');
if (menu) menu.classList.remove('show');
const folderInput = document.getElementById('folder-input');
if (folderInput) folderInput.click();
break;
}
case 'new-folder-btn': {
const folderName = await window.Modal.promptNewFolder();
if (folderName) {
fileOps.createFolder(folderName);
}
break;
}
case 'grid-view-btn':
ui.switchToGridView();
break;
case 'list-view-btn':
ui.switchToListView();
break;
case 'empty-trash-btn':
if (await fileOps.emptyTrash()) {
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');
}
break;
default:
break;
}
});
}
/**
* Initialize the application
*/
function initApp() {
// Cache DOM elements
cacheElements();
// Initialize file sharing module first
if (window.fileSharing && window.fileSharing.init) {
window.fileSharing.init();
} else {
console.warn('fileSharing module not fully initialized');
}
// Then create menus and dialogs after modules have initialized
setTimeout(() => {
ui.initializeContextMenus();
}, 100);
// 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 && window.favorites.init) {
console.log('Initializing favorites module');
window.favorites.init();
} else {
console.warn('Favorites module not available or not initializable');
}
// Initialize recent files module if available
if (window.recent && window.recent.init) {
console.log('Initializing recent files module');
window.recent.init();
} else {
console.warn('Recent files module not available or not initializable');
}
// Initialize multi-select / batch actions
if (window.multiSelect && window.multiSelect.init) {
console.log('Initializing multi-select module');
window.multiSelect.init();
}
// Wait for translations to load before checking authentication
if (window.i18n && window.i18n.isLoaded && window.i18n.isLoaded()) {
// Translations already loaded, proceed with authentication
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');
checkAuthentication();
});
// Set a timeout as a fallback in case translations take too long
setTimeout(() => {
if (!window.i18n || !window.i18n.isLoaded || !window.i18n.isLoaded()) {
console.warn('Translations loading timeout, proceeding with authentication anyway');
checkAuthentication();
}
}, 3000); // 3 second timeout
}
}
/**
* Cache DOM elements for faster access
*/
function cacheElements() {
elements.uploadBtn = document.getElementById('upload-btn');
elements.dropzone = document.getElementById('dropzone');
elements.fileInput = document.getElementById('file-input');
elements.filesGrid = document.getElementById('files-grid');
elements.filesListView = document.getElementById('files-list-view');
elements.newFolderBtn = document.getElementById('new-folder-btn');
elements.gridViewBtn = document.getElementById('grid-view-btn');
elements.listViewBtn = document.getElementById('list-view-btn');
elements.breadcrumb = document.querySelector('.breadcrumb');
elements.pageTitle = document.querySelector('.page-title');
elements.actionsBar = document.querySelector('.actions-bar');
elements.navItems = document.querySelectorAll('.nav-item');
elements.trashBtn = document.querySelector('.nav-item:nth-child(5)'); // The trash nav item
elements.searchInput = document.querySelector('.search-container input');
}
/**
* Setup the user menu (avatar dropdown with profile, storage, theme, about, logout)
*/
function setupUserMenu() {
const wrapper = document.getElementById('user-menu-wrapper');
const avatarBtn = document.getElementById('user-avatar-btn');
const menu = document.getElementById('user-menu');
const logoutBtn = document.getElementById('user-menu-logout');
const themeBtn = document.getElementById('user-menu-theme');
const aboutBtn = document.getElementById('user-menu-about');
const adminBtn = document.getElementById('user-menu-admin');
const adminDivider = document.getElementById('user-menu-admin-divider');
const profileBtn = document.getElementById('user-menu-profile');
const roleBadge = document.getElementById('user-menu-role-badge');
if (!wrapper || !avatarBtn || !menu) return;
// Toggle menu
avatarBtn.addEventListener('click', (e) => {
e.stopPropagation();
const isOpen = wrapper.classList.contains('open');
wrapper.classList.toggle('open');
// Close notification bell if open
const notifWrapper = document.getElementById('notif-wrapper');
const notifBtn = document.getElementById('notif-bell-btn');
if (notifWrapper) notifWrapper.classList.remove('open');
if (notifBtn) notifBtn.classList.remove('active');
if (!isOpen) {
updateUserMenuData();
// Show/hide admin panel button based on user role
const USER_DATA_KEY = 'oxicloud_user';
const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}');
const isAdmin = userData.role === 'admin';
if (adminBtn) adminBtn.style.display = isAdmin ? 'flex' : 'none';
if (adminDivider) adminDivider.style.display = isAdmin ? 'block' : 'none';
if (roleBadge) roleBadge.style.display = isAdmin ? 'block' : 'none';
}
});
// Close menu on outside click
document.addEventListener('click', (e) => {
if (wrapper.classList.contains('open') && !wrapper.contains(e.target)) {
wrapper.classList.remove('open');
}
});
// Logout
if (logoutBtn) {
logoutBtn.addEventListener('click', () => {
wrapper.classList.remove('open');
logout();
});
}
// Theme toggle (dark mode)
if (themeBtn) {
const pill = document.getElementById('theme-toggle-pill');
const isDark = localStorage.getItem('oxicloud_theme') === 'dark';
if (isDark) {
if (pill) pill.classList.add('active');
document.documentElement.setAttribute('data-theme', 'dark');
}
themeBtn.addEventListener('click', (e) => {
e.stopPropagation();
if (pill) {
pill.classList.toggle('active');
const dark = pill.classList.contains('active');
localStorage.setItem('oxicloud_theme', dark ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light');
window.ui.showNotification(
dark ? 'π' : 'βοΈ',
dark ? 'Dark mode enabled' : 'Light mode enabled'
);
}
});
}
// Admin panel link
if (adminBtn) {
adminBtn.addEventListener('click', () => {
wrapper.classList.remove('open');
window.location.href = '/admin';
});
}
// Profile button β navigates to profile page
if (profileBtn) {
profileBtn.addEventListener('click', () => {
wrapper.classList.remove('open');
window.location.href = '/profile';
});
}
// About modal
if (aboutBtn) {
aboutBtn.addEventListener('click', () => {
wrapper.classList.remove('open');
const overlay = document.getElementById('about-modal-overlay');
if (overlay) overlay.classList.add('show');
});
}
// About modal close
const aboutCloseBtn = document.getElementById('about-close-btn');
const aboutOverlay = document.getElementById('about-modal-overlay');
if (aboutCloseBtn) {
aboutCloseBtn.addEventListener('click', () => {
aboutOverlay.classList.remove('show');
});
}
if (aboutOverlay) {
aboutOverlay.addEventListener('click', (e) => {
if (e.target === aboutOverlay) {
aboutOverlay.classList.remove('show');
}
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && aboutOverlay.classList.contains('show')) {
aboutOverlay.classList.remove('show');
}
});
}
// Fetch version from backend (centralized in Cargo.toml)
fetchAppVersion();
}
/**
* Update user menu data (name, email, storage) from localStorage
*/
function updateUserMenuData() {
const USER_DATA_KEY = 'oxicloud_user';
const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}');
const nameEl = document.getElementById('user-menu-name');
const emailEl = document.getElementById('user-menu-email');
const avatarEl = document.getElementById('user-menu-avatar');
const storageFill = document.getElementById('user-menu-storage-fill');
const storageText = document.getElementById('user-menu-storage-text');
if (userData.username) {
if (nameEl) nameEl.textContent = userData.username;
if (emailEl) emailEl.textContent = userData.email || '';
if (avatarEl) avatarEl.textContent = userData.username.substring(0, 2).toUpperCase();
}
// Storage info
const usedBytes = userData.storage_used_bytes || 0;
const quotaBytes = userData.storage_quota_bytes || (10 * 1024 * 1024 * 1024); // 10 GB default
const percentage = quotaBytes > 0 ? Math.min(Math.round((usedBytes / quotaBytes) * 100), 100) : 0;
if (storageFill) storageFill.style.width = percentage + '%';
if (storageText) {
const used = formatFileSize(usedBytes);
const total = formatFileSize(quotaBytes);
storageText.textContent = `${percentage}% Β· ${used} / ${total}`;
}
}
/**
* Fetch app version from backend (centralized in Cargo.toml)
* Updates the about modal version display
*/
async function fetchAppVersion() {
try {
const response = await fetch('/api/version');
if (response.ok) {
const data = await response.json();
const versionEl = document.getElementById('about-version');
if (versionEl && data.version) {
versionEl.textContent = `v${data.version}`;
}
}
} catch (err) {
console.warn('Could not fetch app version:', err);
// Fallback: leave placeholder
}
}
/**
* Setup the upload dropdown button and menu
* Handles opening/closing the dropdown and triggering file/folder inputs
*/
function setupUploadDropdown() {
const uploadBtn = document.getElementById('upload-btn');
const menu = document.getElementById('upload-dropdown-menu');
if (!uploadBtn || !menu) return;
// Abort any previous local bindings (safe across repeated/rebuilt UI)
if (uploadDropdownBindingsController) {
uploadDropdownBindingsController.abort();
}
uploadDropdownBindingsController = new AbortController();
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 });
// Close dropdown when clicking outside
// remove+add stable handler: guarantees exactly one global listener
if (uploadDropdownDocumentClickHandler) {
document.removeEventListener('click', uploadDropdownDocumentClickHandler);
}
uploadDropdownDocumentClickHandler = (e) => {
if (e.target.closest('#upload-dropdown')) return;
document.querySelectorAll('.upload-dropdown-menu.show').forEach(m => m.classList.remove('show'));
};
document.addEventListener('click', uploadDropdownDocumentClickHandler);
}
/**
* Setup event listeners for main UI elements
*/
function setupEventListeners() {
// Set up drag and drop
ui.setupDragAndDrop();
// Debounce timer for live search
let searchDebounceTimer = null;
const SEARCH_DEBOUNCE_MS = 300;
const SEARCH_MIN_CHARS = 3;
// Search input β Enter key
elements.searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
// Cancel any pending debounce
if (searchDebounceTimer) clearTimeout(searchDebounceTimer);
const query = elements.searchInput.value.trim();
if (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('');
loadFiles();
}
}
});
// Search input β Live search (debounced, after 3+ chars)
elements.searchInput.addEventListener('input', () => {
if (searchDebounceTimer) clearTimeout(searchDebounceTimer);
const query = elements.searchInput.value.trim();
if (query.length >= SEARCH_MIN_CHARS) {
searchDebounceTimer = setTimeout(() => {
performSearch(query);
}, SEARCH_DEBOUNCE_MS);
} else if (query.length === 0 && app.isSearchMode) {
// User cleared the search input β return to normal view
searchDebounceTimer = setTimeout(() => {
app.isSearchMode = false;
app.currentPath = '';
ui.updateBreadcrumb('');
loadFiles();
}, SEARCH_DEBOUNCE_MS);
}
});
// Search button
document.getElementById('search-button').addEventListener('click', () => {
if (searchDebounceTimer) clearTimeout(searchDebounceTimer);
const query = elements.searchInput.value.trim();
if (query) {
performSearch(query);
}
});
// Upload dropdown
setupUploadDropdown();
setupActionsBarDelegation();
if (elements.actionsBar) {
elements.actionsBar.dataset.mode = 'files';
}
// File input
elements.fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
fileOps.uploadFiles(e.target.files);
e.target.value = ''; // reset so same file can be re-uploaded
}
});
// Folder input
const folderInput = document.getElementById('folder-input');
if (folderInput) {
folderInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
fileOps.uploadFolderFiles(e.target.files);
e.target.value = '';
}
});
}
// Sidebar navigation
elements.navItems.forEach(item => {
item.addEventListener('click', () => {
// Remove active class from all nav items
elements.navItems.forEach(navItem => navItem.classList.remove('active'));
// Add active class to clicked item
item.classList.add('active');
// Check if this is the shared item
if (item.querySelector('span').getAttribute('data-i18n') === 'nav.shared') {
// Switch to shared view
switchToSharedView();
return;
}
// Check if this is the favorites item
if (item.querySelector('span').getAttribute('data-i18n') === 'nav.favorites') {
// Switch to favorites view
switchToFavoritesView();
return;
}
// Check if this is the recent files item
if (item.querySelector('span').getAttribute('data-i18n') === 'nav.recent') {
// Switch to recent files view
switchToRecentFilesView();
return;
}
// Check if this is the trash item
if (item === elements.trashBtn) {
// Hide shared view if active
if (app.isSharedView) {
// Hide shared view
if (window.sharedView) {
window.sharedView.hide();
}
// Reset shared view flag
app.isSharedView = false;
// Clean up shared containers if they exist
const sharedContainer = document.getElementById('shared-container');
if (sharedContainer) {
sharedContainer.style.display = 'none';
}
}
// Show trash view
app.isTrashView = true;
app.currentSection = 'trash';
// Show files containers (to be filled with trash)
const filesGrid = document.getElementById('files-grid');
const filesListView = document.getElementById('files-list-view');
if (filesGrid) filesGrid.style.display = app.currentView === 'grid' ? 'grid' : 'none';
if (filesListView) filesListView.style.display = app.currentView === 'list' ? 'block' : 'none';
// Update UI
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.trash') : 'Trash';
elements.pageTitle.setAttribute('data-i18n', 'nav.trash');
setActionsBarMode('trash');
// Load trash items
loadTrashItems();
} else {
// Check if we need to reset shared view
if (app.isSharedView) {
// Hide shared view
if (window.sharedView) {
window.sharedView.hide();
}
// Reset shared view flag
app.isSharedView = false;
// Clean up shared containers if they exist
const sharedContainer = document.getElementById('shared-container');
if (sharedContainer) {
sharedContainer.style.display = 'none';
}
}
// Show regular files view
app.isTrashView = false;
app.currentSection = 'files';
// Reset UI
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.files') : 'Files';
setActionsBarMode('files');
// Show files containers
const filesGrid = document.getElementById('files-grid');
const filesListView = document.getElementById('files-list-view');
if (filesGrid) filesGrid.style.display = app.currentView === 'grid' ? 'grid' : 'none';
if (filesListView) filesListView.style.display = app.currentView === 'list' ? 'block' : 'none';
// Load regular files
app.currentPath = '';
ui.updateBreadcrumb('');
loadFiles();
}
});
});
// Load saved view preference
const savedView = localStorage.getItem('oxicloud-view');
if (savedView === 'list') {
ui.switchToListView();
}
// User menu
setupUserMenu();
// Global events to close context menus and deselect cards
document.addEventListener('click', (e) => {
const folderMenu = document.getElementById('folder-context-menu');
const fileMenu = document.getElementById('file-context-menu');
if (folderMenu && folderMenu.style.display === 'block' &&
!folderMenu.contains(e.target)) {
ui.closeContextMenu();
}
if (fileMenu && fileMenu.style.display === 'block' &&
!fileMenu.contains(e.target)) {
ui.closeFileContextMenu();
}
// Deselect all cards when clicking empty area (not on a card, menu, or modal)
// Note: multiSelect._hookGlobalDeselect() handles clearing the internal
// selection state; this handler only covers the legacy CSS class removal.
if (!e.target.closest('.file-card') && !e.target.closest('.file-item') && !e.target.closest('.context-menu') && !e.target.closest('.about-modal') && !e.target.closest('.batch-action-bar') && !e.target.closest('.list-header.selection-mode')) {
document.querySelectorAll('.file-card.selected').forEach(c => c.classList.remove('selected'));
document.querySelectorAll('.file-item.selected').forEach(c => c.classList.remove('selected'));
}
});
}
/**
* Load files and folders for the current path
*/
async function loadFiles(options = {}) {
try {
console.log("Starting loadFiles() - loading files...", options);
// Flag to force complete refresh ignoring cache
const forceRefresh = options.forceRefresh || false;
// Prevent multiple simultaneous load requests
if (window.isLoadingFiles) {
console.log("A file load is already in progress, ignoring request");
return;
}
window.isLoadingFiles = true;
// Show loading spinner
elements.filesGrid.innerHTML = `
`;
// Always ensure a userHomeFolderId is set
if (!app.userHomeFolderId) {
await resolveHomeFolder();
}
// Build the listing URL β single endpoint returns folders + files together
const timestamp = new Date().getTime();
let url;
// ALWAYS use the userHomeFolderId (current folder or home folder) to avoid showing root
if (!app.currentPath || app.currentPath === '') {
// If at root, force user to their home folder
if (app.userHomeFolderId) {
url = `/api/folders/${app.userHomeFolderId}/listing?t=${timestamp}`;
app.currentPath = app.userHomeFolderId;
ui.updateBreadcrumb(app.userHomeFolderName || 'Home');
console.log(`Loading user folder: ${app.userHomeFolderName} (${app.userHomeFolderId})`);
} else {
// Emergency fallback - this should rarely happen but prevents errors
url = `/api/folders?t=${timestamp}`;
console.warn("Emergency fallback to root folder - this should not normally happen");
}
} else {
// Normal case - viewing subfolder contents
url = `/api/folders/${app.currentPath}/listing?t=${timestamp}`;
console.log(`Loading subfolder content: ${app.currentPath}`);
}
const token = localStorage.getItem('oxicloud_token');
const headers = {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache'
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const requestOptions = {
headers,
cache: 'no-store' // Instruct the browser not to use cache
};
// If forceRefresh is specified, add an additional parameter to avoid cache
if (forceRefresh) {
url += `&force_refresh=true`;
requestOptions.headers['X-Force-Refresh'] = 'true';
console.log('Forcing complete refresh ignoring cache');
}
console.log(`Loading listing from ${url}`);
const response = await fetch(url, requestOptions);
// Critical error handling
if (response.status === 401 || response.status === 403) {
console.warn("Auth error when loading files, showing empty list");
elements.filesGrid.innerHTML = '
Could not load files
';
elements.filesListView.innerHTML = `
Name
Type
Size
Modified
`;
return;
}
if (!response.ok) {
throw new Error(`Server responded with status: ${response.status}`);
}
// Single response contains both folders and files
const listing = await response.json();
// Clear existing content in both views
if (window.multiSelect) window.multiSelect.clear();
ui._items.clear();
elements.filesGrid.innerHTML = '';
const _t = (window.i18n && window.i18n.t) ? window.i18n.t : k => k.split('.').pop();
elements.filesListView.innerHTML = `
${_t('files.name')}
${_t('files.type')}
${_t('files.size')}
${_t('files.modified')}
`;
// Re-wire select-all checkbox after DOM rebuild
const selectAllCb = document.getElementById('select-all-checkbox');
if (selectAllCb && window.multiSelect) {
selectAllCb.addEventListener('change', () => window.multiSelect.toggleAll());
}
// Render folders and files from the combined listing
const folderList = Array.isArray(listing.folders) ? listing.folders : [];
const fileList = Array.isArray(listing.files) ? listing.files : [];
ui.renderFolders(folderList);
ui.renderFiles(fileList);
console.log(`Loaded ${folderList.length} folders and ${fileList.length} files`);
} catch (error) {
console.error('Error loading folders:', error);
ui.showNotification('Error', 'Could not load files and folders');
} finally {
// Mark that we are no longer loading files to allow future requests
window.isLoadingFiles = false;
}
}
/**
* Format file size in human-readable format
* @param {number} bytes - Size in bytes
* @return {string} Formatted size
*/
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
/**
* Load trash items
*/
async function loadTrashItems() {
try {
// Clear existing content
if (window.multiSelect) window.multiSelect.clear();
elements.filesGrid.innerHTML = '';
const _tt = (window.i18n && window.i18n.t) ? window.i18n.t : k => k.split('.').pop();
elements.filesListView.innerHTML = `
${_tt('files.name')}
${_tt('files.type')}
${_tt('trash.original_location')}
${_tt('trash.deleted_date')}
${_tt('trash.actions')}
`;
// Update breadcrumb - just show Home
ui.updateBreadcrumb('');
// Get trash items
const trashItems = await fileOps.getTrashItems();
if (trashItems.length === 0) {
// Show empty state
const emptyState = document.createElement('div');
emptyState.className = 'empty-state';
emptyState.innerHTML = `
${window.i18n ? window.i18n.t('trash.empty_state') : 'The trash is empty'}
`;
elements.filesGrid.appendChild(emptyState);
return;
}
// Process each trash item
trashItems.forEach(item => {
addTrashItemToView(item);
});
} catch (error) {
console.error('Error loading trash items:', error);
window.ui.showNotification('Error', 'Error loading trash items');
}
}
/**
* Add a trash item to the view
* @param {Object} item - Trash item object
*/
function addTrashItemToView(item) {
const isFile = item.item_type === 'file';
// Format date - backend sends trashed_at as ISO 8601 string
const formattedDate = window.formatDateTime(item.trashed_at);
// Use icon_class from the trash DTO if available, otherwise fall back to
// the comprehensive icon map exposed by ui.js.
let iconClass;
let typeLabel;
if (!isFile) {
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');
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');
}
// Grid view element
const gridElement = document.createElement('div');
gridElement.className = 'file-card trash-item';
gridElement.dataset.trashId = item.id;
gridElement.dataset.originalId = item.original_id;
gridElement.dataset.itemType = item.item_type;
gridElement.innerHTML = `