`;
// Always ensure a userHomeFolderId is set
if (!app.userHomeFolderId) {
await resolveHomeFolder();
}
// Add timestamp to avoid cache
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}/contents?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}/contents?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 files 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");
// Just show empty state instead of causing redirect loops
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}`);
}
const folders = await response.json();
// Clear existing files in both views
if (window.multiSelect) window.multiSelect.clear();
elements.filesGrid.innerHTML = '';
elements.filesListView.innerHTML = `
Name
Type
Size
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());
}
// Translate the header if i18n is available
if (window.i18n && window.i18n.translatePage) {
window.i18n.translatePage();
}
// Add folders (check if it's an array)
const folderList = Array.isArray(folders) ? folders : [];
// Backend already scopes folders to the authenticated user,
// so no client-side filtering is needed.
folderList.forEach(folder => {
ui.addFolderToView(folder);
});
// Also load files in this folder
const cacheTimestamp = new Date().getTime();
let filesUrl = `/api/files?t=${cacheTimestamp}`; // Add timestamp to avoid cache issues
if (app.currentPath) {
filesUrl += `&folder_id=${app.currentPath}`;
}
console.log(`Loading files from: ${filesUrl}`);
try {
console.log(`Fetching files from: ${filesUrl}`);
const filesResponse = await fetch(filesUrl, requestOptions); // Use same auth token
console.log(`Files response status: ${filesResponse.status}`);
// Handle auth errors for files too
if (filesResponse.status === 401 || filesResponse.status === 403) {
console.warn("Auth error when loading files");
return; // Already showing folders, just stop here
}
if (filesResponse.ok) {
const files = await filesResponse.json();
console.log(`Files received:`, files);
// Add files (check if it's an array)
const fileList = Array.isArray(files) ? files : [];
console.log(`Processing ${fileList.length} files`);
fileList.forEach(file => {
console.log(`Adding file to view: ${file.name} (${file.id})`);
ui.addFileToView(file);
});
} else {
const errorText = await filesResponse.text();
console.error(`Error loading files: ${filesResponse.status} - ${errorText}`);
}
} catch (error) {
console.error('Error loading files:', error);
// File API may not be implemented yet, so we silently ignore this error
}
// Update file icons based on file type
ui.updateFileIcons();
} 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 = '';
elements.filesListView.innerHTML = `
Name
Type
Original location
Deletion date
Actions
`;
// Translate the header if i18n is available
if (window.i18n && window.i18n.translatePage) {
window.i18n.translatePage();
}
// 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'}
`;
// Add action buttons event listeners for list view
listElement.querySelector('.btn-restore').addEventListener('click', async (e) => {
e.stopPropagation();
if (await fileOps.restoreFromTrash(item.id)) {
loadTrashItems();
}
});
listElement.querySelector('.btn-delete').addEventListener('click', async (e) => {
e.stopPropagation();
if (await fileOps.deletePermanently(item.id)) {
loadTrashItems();
}
});
elements.filesListView.appendChild(listElement);
}
/**
* Perform search with the given query.
* All processing (filtering, scoring, sorting, categorization) is done
* server-side in Rust. This function only sends the request and renders.
*
* @param {string} query - Search query
* @param {string} [sortBy] - Sort order (relevance|name|name_desc|date|date_desc|size|size_desc)
*/
async function performSearch(query, sortBy) {
console.log(`Performing search for: "${query}" (sort: ${sortBy || 'relevance'})`);
try {
app.isSearchMode = true;
ui.updateBreadcrumb(`Search: "${query}"`);
// Show loading spinner
const filesGrid = document.getElementById('files-grid');
if (filesGrid) {
filesGrid.innerHTML = `
Searching for "${query}"...
`;
}
// All options β backend handles all processing
const options = {
recursive: true,
limit: 100,
sort_by: sortBy || 'relevance'
};
// Restrict search to user's folder context
if (!app.isTrashView) {
options.folder_id = app.currentPath;
if (!options.folder_id || options.folder_id === '') {
await resolveHomeFolder();
options.folder_id = app.currentPath;
}
}
// Send search request β backend does all processing
const searchResults = await window.search.searchFiles(query, options);
// Render enriched results from the server
window.search.displaySearchResults(searchResults);
} catch (error) {
console.error('Search error:', error);
window.ui.showNotification('Error', 'Error performing search');
}
}
// Listen for re-sort events from the search sort dropdown
document.addEventListener('search-resort', (e) => {
const searchInput = document.querySelector('.search-container input');
if (searchInput && searchInput.value.trim()) {
performSearch(searchInput.value.trim(), e.detail.sort_by);
}
});
// Expose needed functions to global scope
window.app = app;
window.loadFiles = loadFiles;
window.loadTrashItems = loadTrashItems;
window.formatFileSize = formatFileSize;
window.performSearch = performSearch;
// Set up global selectFolder function for navigation
window.selectFolder = (id, name) => {
app.currentPath = id;
ui.updateBreadcrumb(name);
loadFiles();
};
/**
* Switch to the shared view
*/
function switchToSharedView() {
// Hide trash view if active
app.isTrashView = false;
// Set shared view as active
app.isSharedView = true;
app.currentSection = 'shared';
// Remove active class from all nav items
elements.navItems.forEach(navItem => navItem.classList.remove('active'));
// Find shared nav item and make it active
const sharedNavItem = document.querySelector('.nav-item:nth-child(2)');
if (sharedNavItem) {
sharedNavItem.classList.add('active');
}
// Update UI
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.shared') : 'Shared';
// Clear breadcrumb and show root
ui.updateBreadcrumb('');
// Hide standard actions bar
if (elements.actionsBar) {
elements.actionsBar.style.display = 'none';
}
// Init and show shared view
if (window.sharedView) {
window.sharedView.init();
window.sharedView.show();
}
}
/**
* Switch back to the files view
*/
function switchToFilesView() {
// Reset view flags
app.isTrashView = false;
app.isSharedView = false;
app.isFavoritesView = false;
app.isRecentView = false;
app.currentSection = 'files';
// Update UI
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.files') : 'Files';
// Remove active class from all nav items
elements.navItems.forEach(navItem => navItem.classList.remove('active'));
// Make files nav item active
const filesNavItem = document.querySelector('.nav-item:first-child');
if (filesNavItem) {
filesNavItem.classList.add('active');
}
// Reset UI
elements.actionsBar.innerHTML = `
`;
elements.actionsBar.style.display = 'flex';
// Restore event listeners
setupUploadDropdown();
document.getElementById('new-folder-btn').addEventListener('click', async () => {
const folderName = await window.Modal.promptNewFolder();
if (folderName) {
fileOps.createFolder(folderName);
}
});
document.getElementById('grid-view-btn').addEventListener('click', ui.switchToGridView);
document.getElementById('list-view-btn').addEventListener('click', ui.switchToListView);
// Restore cached elements
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');
// Hide shared view if it exists
if (window.sharedView) {
window.sharedView.hide();
}
// Show standard files container
const filesGrid = document.getElementById('files-grid');
if (filesGrid) {
filesGrid.style.display = app.currentView === 'grid' ? 'grid' : 'none';
}
const filesListView = document.getElementById('files-list-view');
if (filesListView) {
filesListView.style.display = app.currentView === 'list' ? 'block' : 'none';
}
// Use user's home folder instead of root path
if (app.userHomeFolderId) {
app.currentPath = app.userHomeFolderId;
ui.updateBreadcrumb(app.userHomeFolderName || 'Home');
} else {
// If no home folder is set, this will trigger finding it in loadFiles()
app.currentPath = '';
}
loadFiles();
}
/**
* Switch to the favorites view
*/
function switchToFavoritesView() {
// Hide other views
app.isTrashView = false;
app.isSharedView = false;
// Set favorites view as active
app.isFavoritesView = true;
app.currentSection = 'favorites';
// Remove active class from all nav items
elements.navItems.forEach(navItem => navItem.classList.remove('active'));
// Find favorites nav item and make it active
const favoritesNavItem = document.querySelector('.nav-item:nth-child(4)');
if (favoritesNavItem) {
favoritesNavItem.classList.add('active');
}
// Update UI
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.favorites') : 'Favorites';
// Clear breadcrumb and show root
ui.updateBreadcrumb('');
// Hide shared view if it exists
if (window.sharedView) {
window.sharedView.hide();
}
// Configure actions bar for favorites view
elements.actionsBar.innerHTML = `
`;
elements.actionsBar.style.display = 'flex';
// Restore view toggle event listeners
document.getElementById('grid-view-btn').addEventListener('click', ui.switchToGridView);
document.getElementById('list-view-btn').addEventListener('click', ui.switchToListView);
// Update cached elements
elements.gridViewBtn = document.getElementById('grid-view-btn');
elements.listViewBtn = document.getElementById('list-view-btn');
// Show standard 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';
}
// Check if favorites module is initialized
if (window.favorites) {
// Display favorites
window.favorites.displayFavorites();
} else {
console.error('Favorites module not loaded or initialized');
// Show error in UI
const filesGrid = document.getElementById('files-grid');
if (filesGrid) {
filesGrid.innerHTML = `
Error loading the favorites module
`;
}
}
}
/**
* Switch to the recent files view
*/
function switchToRecentFilesView() {
// Hide other views
app.isTrashView = false;
app.isSharedView = false;
app.isFavoritesView = false;
// Set recent view as active
app.isRecentView = true;
app.currentSection = 'recent';
// Remove active class from all nav items
elements.navItems.forEach(navItem => navItem.classList.remove('active'));
// Find recent nav item and make it active
const recentNavItem = document.querySelector('.nav-item:nth-child(3)');
if (recentNavItem) {
recentNavItem.classList.add('active');
}
// Update UI
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.recent') : 'Recent';
// Clear breadcrumb and show root
ui.updateBreadcrumb('');
// Hide shared view if it exists
if (window.sharedView) {
window.sharedView.hide();
}
// Configure actions bar for recent view
elements.actionsBar.innerHTML = `
`;
elements.actionsBar.style.display = 'flex';
// Add event listener for clear button
document.getElementById('clear-recent-btn').addEventListener('click', () => {
if (window.recent) {
window.recent.clearRecentFiles();
window.recent.displayRecentFiles();
window.ui.showNotification('Cleanup completed', 'Recent files history has been cleared');
}
});
// Restore view toggle event listeners
document.getElementById('grid-view-btn').addEventListener('click', ui.switchToGridView);
document.getElementById('list-view-btn').addEventListener('click', ui.switchToListView);
// Update cached elements
elements.gridViewBtn = document.getElementById('grid-view-btn');
elements.listViewBtn = document.getElementById('list-view-btn');
// Show standard 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';
}
// Check if recent files module is initialized
if (window.recent) {
// Display recent files
window.recent.displayRecentFiles();
} else {
console.error('Recent files module not loaded or initialized');
// Show error in UI
const filesGrid = document.getElementById('files-grid');
if (filesGrid) {
filesGrid.innerHTML = `
Error loading the recent files module
`;
}
}
}
// Expose view switching functions globally
window.switchToFilesView = switchToFilesView;
window.switchToSharedView = switchToSharedView;
window.switchToFavoritesView = switchToFavoritesView;
window.switchToRecentFilesView = switchToRecentFilesView;
/**
* Fetch updated user data from the server (including storage usage)
* This calls the /api/auth/me endpoint which also triggers storage recalculation
*/
async function refreshUserData() {
const TOKEN_KEY = 'oxicloud_token';
const USER_DATA_KEY = 'oxicloud_user';
const token = localStorage.getItem(TOKEN_KEY);
console.log('refreshUserData called, token:', token ? token.substring(0, 20) + '...' : 'null');
if (!token) {
console.log('No valid token, skipping user data refresh');
return null;
}
try {
console.log('Fetching /api/auth/me...');
const response = await fetch('/api/auth/me', {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
console.log('/api/auth/me response status:', response.status);
if (!response.ok) {
console.warn('Failed to fetch user data:', response.status);
return null;
}
const userData = await response.json();
console.log('Refreshed user data from server:', userData);
console.log('Storage from server: used=', userData.storage_used_bytes, 'quota=', userData.storage_quota_bytes);
// Update local storage with fresh data
localStorage.setItem(USER_DATA_KEY, JSON.stringify(userData));
// Update storage display with actual values
updateStorageUsageDisplay(userData);
return userData;
} catch (error) {
console.error('Error refreshing user data:', error);
return null;
}
}
// Expose refreshUserData globally
window.refreshUserData = refreshUserData;
/**
* Show User Profile modal with account details
*/
function showUserProfileModal() {
const USER_DATA_KEY = 'oxicloud_user';
const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}');
const username = userData.username || 'User';
const email = userData.email || '';
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 || 0;
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;
// Remove existing modal if any
const existing = document.getElementById('profile-modal-overlay');
if (existing) existing.remove();
const overlay = document.createElement('div');
overlay.id = 'profile-modal-overlay';
overlay.className = 'about-modal-overlay';
overlay.innerHTML = `