2025-03-19 23:28:29 +01:00
|
|
|
/**
|
|
|
|
|
* OxiCloud - Main Application
|
|
|
|
|
* This file contains the core functionality, initialization and state management
|
|
|
|
|
*/
|
|
|
|
|
|
2026-02-15 17:53:25 +01:00
|
|
|
/**
|
|
|
|
|
* 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, '"')
|
|
|
|
|
.replace(/'/g, ''');
|
|
|
|
|
}
|
|
|
|
|
window.escapeHtml = escapeHtml;
|
|
|
|
|
|
2025-03-19 23:28:29 +01:00
|
|
|
// 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'
|
2025-03-24 17:49:53 +01:00
|
|
|
isTrashView: false, // Whether we're in trash view
|
2025-04-01 21:14:09 +02:00
|
|
|
isSharedView: false, // Whether we're in shared view
|
2025-04-02 03:43:44 +02:00
|
|
|
isFavoritesView: false, // Whether we're in favorites view
|
2025-04-02 05:08:30 +02:00
|
|
|
isRecentView: false, // Whether we're in recent files view
|
|
|
|
|
currentSection: 'files', // Current section: 'files', 'trash', 'shared', 'favorites' or 'recent'
|
2025-03-27 01:13:34 +01:00
|
|
|
isSearchMode: false, // Whether we're in search mode
|
2025-03-28 08:09:18 +01:00
|
|
|
// 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
|
2025-03-19 23:28:29 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// DOM elements
|
|
|
|
|
const elements = {
|
|
|
|
|
// Will be populated on initialization
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Initialize the application
|
|
|
|
|
*/
|
|
|
|
|
function initApp() {
|
|
|
|
|
// Cache DOM elements
|
|
|
|
|
cacheElements();
|
|
|
|
|
|
2025-03-28 08:09:18 +01:00
|
|
|
// 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);
|
2025-03-19 23:28:29 +01:00
|
|
|
|
|
|
|
|
// Setup event listeners
|
|
|
|
|
setupEventListeners();
|
|
|
|
|
|
|
|
|
|
// Initialize file renderer if available
|
|
|
|
|
if (window.fileRenderer) {
|
|
|
|
|
console.log('Using optimized file renderer');
|
|
|
|
|
} else {
|
|
|
|
|
console.log('Using standard file rendering');
|
|
|
|
|
}
|
2025-03-20 09:22:31 +01:00
|
|
|
|
2025-04-02 01:22:05 +02:00
|
|
|
// Check if inline viewer is initialized
|
|
|
|
|
if (window.inlineViewer) {
|
|
|
|
|
console.log('Inline viewer is available');
|
|
|
|
|
} else {
|
|
|
|
|
console.warn('Inline viewer not initialized yet, will initialize it now');
|
2025-04-02 03:43:44 +02:00
|
|
|
try {
|
|
|
|
|
// Create inline viewer if not already created and if the class exists
|
|
|
|
|
if (typeof InlineViewer !== 'undefined') {
|
|
|
|
|
window.inlineViewer = new InlineViewer();
|
|
|
|
|
} else {
|
|
|
|
|
console.warn('InlineViewer class is not defined, skipping initialization');
|
|
|
|
|
}
|
|
|
|
|
} 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');
|
2025-04-02 01:22:05 +02:00
|
|
|
}
|
|
|
|
|
|
2025-04-02 05:08:30 +02:00
|
|
|
// 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');
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-14 00:12:18 +01:00
|
|
|
// Initialize multi-select / batch actions
|
|
|
|
|
if (window.multiSelect && window.multiSelect.init) {
|
|
|
|
|
console.log('Initializing multi-select module');
|
|
|
|
|
window.multiSelect.init();
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-27 01:13:34 +01:00
|
|
|
// 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
|
|
|
|
|
}
|
2025-03-19 23:28:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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');
|
2025-03-24 17:49:53 +01:00
|
|
|
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
|
2025-03-27 01:13:34 +01:00
|
|
|
elements.searchInput = document.querySelector('.search-container input');
|
2025-03-19 23:28:29 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-08 22:44:42 +01:00
|
|
|
/**
|
|
|
|
|
* 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');
|
2026-02-11 12:26:29 +01:00
|
|
|
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');
|
2026-02-08 22:44:42 +01:00
|
|
|
|
|
|
|
|
if (!wrapper || !avatarBtn || !menu) return;
|
|
|
|
|
|
|
|
|
|
// Toggle menu
|
|
|
|
|
avatarBtn.addEventListener('click', (e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
const isOpen = wrapper.classList.contains('open');
|
|
|
|
|
wrapper.classList.toggle('open');
|
2026-02-14 10:46:23 +01:00
|
|
|
|
|
|
|
|
// 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');
|
|
|
|
|
|
2026-02-08 22:44:42 +01:00
|
|
|
if (!isOpen) {
|
|
|
|
|
updateUserMenuData();
|
2026-02-11 12:26:29 +01:00
|
|
|
// 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';
|
2026-02-08 22:44:42 +01:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-14 00:45:45 +01:00
|
|
|
// Theme toggle (dark mode)
|
2026-02-08 22:44:42 +01:00
|
|
|
if (themeBtn) {
|
|
|
|
|
const pill = document.getElementById('theme-toggle-pill');
|
|
|
|
|
const isDark = localStorage.getItem('oxicloud_theme') === 'dark';
|
2026-02-14 00:45:45 +01:00
|
|
|
if (isDark) {
|
|
|
|
|
if (pill) pill.classList.add('active');
|
|
|
|
|
document.documentElement.setAttribute('data-theme', 'dark');
|
|
|
|
|
}
|
2026-02-08 22:44:42 +01:00
|
|
|
|
|
|
|
|
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');
|
2026-02-14 00:45:45 +01:00
|
|
|
document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light');
|
2026-02-08 22:44:42 +01:00
|
|
|
window.ui.showNotification(
|
|
|
|
|
dark ? '🌙' : '☀️',
|
2026-02-14 00:45:45 +01:00
|
|
|
dark ? 'Dark mode enabled' : 'Light mode enabled'
|
2026-02-08 22:44:42 +01:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-11 12:26:29 +01:00
|
|
|
// Admin panel link
|
|
|
|
|
if (adminBtn) {
|
|
|
|
|
adminBtn.addEventListener('click', () => {
|
|
|
|
|
wrapper.classList.remove('open');
|
2026-02-13 22:44:20 +01:00
|
|
|
window.location.href = '/admin';
|
2026-02-11 12:26:29 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-11 14:09:40 +01:00
|
|
|
// Profile button — navigates to profile page
|
2026-02-11 12:26:29 +01:00
|
|
|
if (profileBtn) {
|
|
|
|
|
profileBtn.addEventListener('click', () => {
|
|
|
|
|
wrapper.classList.remove('open');
|
2026-02-13 22:44:20 +01:00
|
|
|
window.location.href = '/profile';
|
2026-02-11 12:26:29 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-08 22:44:42 +01:00
|
|
|
// 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');
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-02-13 22:38:26 +01:00
|
|
|
document.addEventListener('keydown', (e) => {
|
|
|
|
|
if (e.key === 'Escape' && aboutOverlay.classList.contains('show')) {
|
|
|
|
|
aboutOverlay.classList.remove('show');
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-02-08 22:44:42 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 || 10737418240;
|
|
|
|
|
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 dropdown = document.getElementById('upload-dropdown');
|
|
|
|
|
const uploadBtn = document.getElementById('upload-btn');
|
|
|
|
|
const menu = document.getElementById('upload-dropdown-menu');
|
|
|
|
|
const uploadFilesBtn = document.getElementById('upload-files-btn');
|
|
|
|
|
const uploadFolderBtn = document.getElementById('upload-folder-btn');
|
|
|
|
|
|
|
|
|
|
if (!uploadBtn || !menu) return;
|
|
|
|
|
|
|
|
|
|
// 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');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Upload files option
|
|
|
|
|
if (uploadFilesBtn) {
|
|
|
|
|
uploadFilesBtn.addEventListener('click', (e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
menu.classList.remove('show');
|
|
|
|
|
elements.fileInput.click();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Upload folder option
|
|
|
|
|
if (uploadFolderBtn) {
|
|
|
|
|
uploadFolderBtn.addEventListener('click', (e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
menu.classList.remove('show');
|
|
|
|
|
const folderInput = document.getElementById('folder-input');
|
|
|
|
|
if (folderInput) {
|
|
|
|
|
folderInput.click();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Close dropdown when clicking outside
|
|
|
|
|
document.addEventListener('click', () => {
|
|
|
|
|
document.querySelectorAll('.upload-dropdown-menu.show').forEach(m => m.classList.remove('show'));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-19 23:28:29 +01:00
|
|
|
/**
|
|
|
|
|
* Setup event listeners for main UI elements
|
|
|
|
|
*/
|
|
|
|
|
function setupEventListeners() {
|
|
|
|
|
// Set up drag and drop
|
|
|
|
|
ui.setupDragAndDrop();
|
|
|
|
|
|
2025-03-27 01:13:34 +01:00
|
|
|
// Search input
|
|
|
|
|
elements.searchInput.addEventListener('keydown', (e) => {
|
|
|
|
|
if (e.key === 'Enter') {
|
|
|
|
|
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 button
|
|
|
|
|
document.getElementById('search-button').addEventListener('click', () => {
|
|
|
|
|
const query = elements.searchInput.value.trim();
|
|
|
|
|
if (query) {
|
|
|
|
|
performSearch(query);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-08 22:44:42 +01:00
|
|
|
// Upload dropdown
|
|
|
|
|
setupUploadDropdown();
|
2025-03-19 23:28:29 +01:00
|
|
|
|
|
|
|
|
// File input
|
|
|
|
|
elements.fileInput.addEventListener('change', (e) => {
|
|
|
|
|
if (e.target.files.length > 0) {
|
|
|
|
|
fileOps.uploadFiles(e.target.files);
|
2026-02-08 22:44:42 +01:00
|
|
|
e.target.value = ''; // reset so same file can be re-uploaded
|
2025-03-19 23:28:29 +01:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-08 22:44:42 +01:00
|
|
|
// 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 = '';
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-19 23:28:29 +01:00
|
|
|
// New folder button
|
2026-02-03 17:59:04 +01:00
|
|
|
elements.newFolderBtn.addEventListener('click', async () => {
|
|
|
|
|
const folderName = await window.Modal.promptNewFolder();
|
2025-03-19 23:28:29 +01:00
|
|
|
if (folderName) {
|
|
|
|
|
fileOps.createFolder(folderName);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// View toggle
|
|
|
|
|
elements.gridViewBtn.addEventListener('click', ui.switchToGridView);
|
|
|
|
|
elements.listViewBtn.addEventListener('click', ui.switchToListView);
|
|
|
|
|
|
2025-03-24 17:49:53 +01:00
|
|
|
// 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');
|
|
|
|
|
|
2025-03-28 08:09:18 +01:00
|
|
|
// Check if this is the shared item
|
|
|
|
|
if (item.querySelector('span').getAttribute('data-i18n') === 'nav.shared') {
|
2025-04-01 21:14:09 +02:00
|
|
|
// Switch to shared view
|
|
|
|
|
switchToSharedView();
|
2025-03-28 08:09:18 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-02 03:43:44 +02:00
|
|
|
// Check if this is the favorites item
|
|
|
|
|
if (item.querySelector('span').getAttribute('data-i18n') === 'nav.favorites') {
|
|
|
|
|
// Switch to favorites view
|
|
|
|
|
switchToFavoritesView();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-02 05:08:30 +02:00
|
|
|
// Check if this is the recent files item
|
|
|
|
|
if (item.querySelector('span').getAttribute('data-i18n') === 'nav.recent') {
|
|
|
|
|
// Switch to recent files view
|
|
|
|
|
switchToRecentFilesView();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-24 17:49:53 +01:00
|
|
|
// Check if this is the trash item
|
|
|
|
|
if (item === elements.trashBtn) {
|
2025-04-01 21:14:09 +02:00
|
|
|
// 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';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-24 17:49:53 +01:00
|
|
|
// Show trash view
|
|
|
|
|
app.isTrashView = true;
|
|
|
|
|
app.currentSection = 'trash';
|
|
|
|
|
|
2025-04-01 21:14:09 +02:00
|
|
|
// 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';
|
|
|
|
|
|
2025-03-24 17:49:53 +01:00
|
|
|
// Update UI
|
2026-02-12 09:41:25 +01:00
|
|
|
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.trash') : 'Trash';
|
2025-03-24 17:49:53 +01:00
|
|
|
elements.actionsBar.innerHTML = `
|
|
|
|
|
<div class="action-buttons">
|
|
|
|
|
<button class="btn btn-danger" id="empty-trash-btn">
|
2026-02-03 17:59:04 +01:00
|
|
|
<i class="fas fa-trash-alt"></i>
|
2026-02-12 09:41:25 +01:00
|
|
|
<span>${window.i18n ? window.i18n.t('trash.empty_trash') : 'Empty trash'}</span>
|
2025-03-24 17:49:53 +01:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
2025-04-01 21:14:09 +02:00
|
|
|
elements.actionsBar.style.display = 'flex';
|
2025-03-24 17:49:53 +01:00
|
|
|
|
|
|
|
|
// Add event listener to empty trash button
|
|
|
|
|
document.getElementById('empty-trash-btn').addEventListener('click', async () => {
|
|
|
|
|
if (await fileOps.emptyTrash()) {
|
|
|
|
|
loadTrashItems();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Load trash items
|
|
|
|
|
loadTrashItems();
|
|
|
|
|
} else {
|
2025-04-01 21:14:09 +02:00
|
|
|
// 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';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-24 17:49:53 +01:00
|
|
|
// Show regular files view
|
|
|
|
|
app.isTrashView = false;
|
|
|
|
|
app.currentSection = 'files';
|
|
|
|
|
|
|
|
|
|
// Reset UI
|
2026-02-12 09:41:25 +01:00
|
|
|
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.files') : 'Files';
|
2025-03-24 17:49:53 +01:00
|
|
|
elements.actionsBar.innerHTML = `
|
|
|
|
|
<div class="action-buttons">
|
2026-02-08 22:44:42 +01:00
|
|
|
<div class="upload-dropdown" id="upload-dropdown">
|
|
|
|
|
<button class="btn btn-primary" id="upload-btn">
|
|
|
|
|
<i class="fas fa-cloud-upload-alt" style="margin-right: 5px;"></i>
|
2026-02-12 09:41:25 +01:00
|
|
|
<span data-i18n="actions.upload">Upload</span>
|
2026-02-08 22:44:42 +01:00
|
|
|
<i class="fas fa-caret-down" style="margin-left: 4px; font-size: 12px;"></i>
|
|
|
|
|
</button>
|
|
|
|
|
<div class="upload-dropdown-menu" id="upload-dropdown-menu">
|
|
|
|
|
<button class="upload-dropdown-item" id="upload-files-btn">
|
|
|
|
|
<i class="fas fa-file"></i>
|
2026-02-12 09:41:25 +01:00
|
|
|
<span data-i18n="actions.upload_files">Upload files</span>
|
2026-02-08 22:44:42 +01:00
|
|
|
</button>
|
|
|
|
|
<button class="upload-dropdown-item" id="upload-folder-btn">
|
|
|
|
|
<i class="fas fa-folder-open"></i>
|
2026-02-12 09:41:25 +01:00
|
|
|
<span data-i18n="actions.upload_folder">Upload folder</span>
|
2026-02-08 22:44:42 +01:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-03-24 17:49:53 +01:00
|
|
|
<button class="btn btn-secondary" id="new-folder-btn">
|
2026-02-12 09:41:25 +01:00
|
|
|
<i class="fas fa-folder-plus" style="margin-right: 5px;"></i> <span data-i18n="actions.new_folder">New folder</span>
|
2025-03-24 17:49:53 +01:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="view-toggle">
|
2026-02-12 09:41:25 +01:00
|
|
|
<button class="toggle-btn active" id="grid-view-btn" title="Grid view">
|
2025-03-24 17:49:53 +01:00
|
|
|
<i class="fas fa-th"></i>
|
|
|
|
|
</button>
|
2026-02-12 09:41:25 +01:00
|
|
|
<button class="toggle-btn" id="list-view-btn" title="List view">
|
2025-03-24 17:49:53 +01:00
|
|
|
<i class="fas fa-list"></i>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
2025-04-01 21:14:09 +02:00
|
|
|
elements.actionsBar.style.display = 'flex';
|
|
|
|
|
|
|
|
|
|
// 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';
|
2025-03-24 17:49:53 +01:00
|
|
|
|
|
|
|
|
// Restore event listeners
|
2026-02-08 22:44:42 +01:00
|
|
|
setupUploadDropdown();
|
2025-03-24 17:49:53 +01:00
|
|
|
|
2026-02-03 17:59:04 +01:00
|
|
|
document.getElementById('new-folder-btn').addEventListener('click', async () => {
|
|
|
|
|
const folderName = await window.Modal.promptNewFolder();
|
2025-03-24 17:49:53 +01:00
|
|
|
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');
|
|
|
|
|
|
|
|
|
|
// Load regular files
|
|
|
|
|
app.currentPath = '';
|
|
|
|
|
ui.updateBreadcrumb('');
|
|
|
|
|
loadFiles();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2025-03-19 23:28:29 +01:00
|
|
|
// Load saved view preference
|
|
|
|
|
const savedView = localStorage.getItem('oxicloud-view');
|
|
|
|
|
if (savedView === 'list') {
|
|
|
|
|
ui.switchToListView();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-08 22:44:42 +01:00
|
|
|
// User menu
|
|
|
|
|
setupUserMenu();
|
2025-03-20 09:22:31 +01:00
|
|
|
|
2026-02-08 22:44:42 +01:00
|
|
|
// Global events to close context menus and deselect cards
|
2025-03-19 23:28:29 +01:00
|
|
|
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();
|
|
|
|
|
}
|
2026-02-08 22:44:42 +01:00
|
|
|
|
|
|
|
|
// Deselect all cards when clicking empty area (not on a card, menu, or modal)
|
2026-02-14 00:12:18 +01:00
|
|
|
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')) {
|
2026-02-08 22:44:42 +01:00
|
|
|
document.querySelectorAll('.file-card.selected').forEach(c => c.classList.remove('selected'));
|
2026-02-14 00:12:18 +01:00
|
|
|
document.querySelectorAll('.file-item.selected').forEach(c => c.classList.remove('selected'));
|
|
|
|
|
if (window.multiSelect) window.multiSelect.clear();
|
2026-02-08 22:44:42 +01:00
|
|
|
}
|
2025-03-19 23:28:29 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Load files and folders for the current path
|
|
|
|
|
*/
|
2025-04-12 12:21:57 +02:00
|
|
|
async function loadFiles(options = {}) {
|
2025-03-19 23:28:29 +01:00
|
|
|
try {
|
2026-02-12 09:41:25 +01:00
|
|
|
console.log("Starting loadFiles() - loading files...", options);
|
2025-04-12 12:21:57 +02:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Flag to force complete refresh ignoring cache
|
2025-04-12 12:21:57 +02:00
|
|
|
const forceRefresh = options.forceRefresh || false;
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Prevent multiple simultaneous load requests
|
2025-04-12 12:21:57 +02:00
|
|
|
if (window.isLoadingFiles) {
|
2026-02-12 09:41:25 +01:00
|
|
|
console.log("A file load is already in progress, ignoring request");
|
2025-04-12 12:21:57 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
window.isLoadingFiles = true;
|
|
|
|
|
|
2026-02-08 22:44:42 +01:00
|
|
|
// Show loading spinner
|
|
|
|
|
elements.filesGrid.innerHTML = `
|
|
|
|
|
<div class="files-loading-spinner">
|
|
|
|
|
<div class="spinner"></div>
|
2026-02-12 09:41:25 +01:00
|
|
|
<span>${window.i18n ? window.i18n.t('files.loading') : 'Loading files…'}</span>
|
2026-02-08 22:44:42 +01:00
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
2025-04-04 04:30:49 +02:00
|
|
|
// Always ensure a userHomeFolderId is set
|
|
|
|
|
if (!app.userHomeFolderId) {
|
|
|
|
|
// If we don't have a home folder ID yet, try to get the user's username
|
|
|
|
|
const USER_DATA_KEY = 'oxicloud_user';
|
|
|
|
|
const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}');
|
|
|
|
|
if (userData.username) {
|
|
|
|
|
// Find user's home folder
|
2026-02-12 09:41:25 +01:00
|
|
|
console.log("Looking for user folder for", userData.username);
|
2025-04-04 04:30:49 +02:00
|
|
|
await findUserHomeFolder(userData.username);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Add timestamp to avoid cache
|
2025-04-12 12:21:57 +02:00
|
|
|
const timestamp = new Date().getTime();
|
2025-04-04 04:30:49 +02:00
|
|
|
let url;
|
2025-04-12 12:21:57 +02:00
|
|
|
|
2025-04-04 04:30:49 +02:00
|
|
|
// 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) {
|
2025-04-12 12:21:57 +02:00
|
|
|
url = `/api/folders/${app.userHomeFolderId}/contents?t=${timestamp}`;
|
2025-04-04 04:30:49 +02:00
|
|
|
app.currentPath = app.userHomeFolderId;
|
|
|
|
|
ui.updateBreadcrumb(app.userHomeFolderName || 'Home');
|
2026-02-12 09:41:25 +01:00
|
|
|
console.log(`Loading user folder: ${app.userHomeFolderName} (${app.userHomeFolderId})`);
|
2025-04-04 04:30:49 +02:00
|
|
|
} else {
|
|
|
|
|
// Emergency fallback - this should rarely happen but prevents errors
|
2025-04-12 12:21:57 +02:00
|
|
|
url = `/api/folders?t=${timestamp}`;
|
2025-04-04 04:30:49 +02:00
|
|
|
console.warn("Emergency fallback to root folder - this should not normally happen");
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Normal case - viewing subfolder contents
|
2025-04-12 12:21:57 +02:00
|
|
|
url = `/api/folders/${app.currentPath}/contents?t=${timestamp}`;
|
2026-02-12 09:41:25 +01:00
|
|
|
console.log(`Loading subfolder content: ${app.currentPath}`);
|
2025-03-19 23:28:29 +01:00
|
|
|
}
|
|
|
|
|
|
2025-03-31 06:20:15 +02:00
|
|
|
const token = localStorage.getItem('oxicloud_token');
|
2026-02-13 08:54:51 +01:00
|
|
|
const headers = {
|
|
|
|
|
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
|
|
|
'Pragma': 'no-cache'
|
|
|
|
|
};
|
|
|
|
|
if (token) {
|
|
|
|
|
headers['Authorization'] = `Bearer ${token}`;
|
|
|
|
|
}
|
2025-03-31 06:20:15 +02:00
|
|
|
const requestOptions = {
|
2026-02-13 08:54:51 +01:00
|
|
|
headers,
|
2026-02-12 09:41:25 +01:00
|
|
|
cache: 'no-store' // Instruct the browser not to use cache
|
2025-03-31 06:20:15 +02:00
|
|
|
};
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// If forceRefresh is specified, add an additional parameter to avoid cache
|
2025-04-12 12:21:57 +02:00
|
|
|
if (forceRefresh) {
|
|
|
|
|
url += `&force_refresh=true`;
|
|
|
|
|
requestOptions.headers['X-Force-Refresh'] = 'true';
|
2026-02-12 09:41:25 +01:00
|
|
|
console.log('Forcing complete refresh ignoring cache');
|
2025-04-12 12:21:57 +02:00
|
|
|
}
|
|
|
|
|
|
2025-03-31 06:20:15 +02:00
|
|
|
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
|
2026-02-12 09:41:25 +01:00
|
|
|
elements.filesGrid.innerHTML = '<div class="empty-state"><p>Could not load files</p></div>';
|
2025-03-31 06:20:15 +02:00
|
|
|
elements.filesListView.innerHTML = `
|
|
|
|
|
<div class="list-header">
|
2026-02-14 00:12:18 +01:00
|
|
|
<div class="list-header-checkbox"><input type="checkbox" id="select-all-checkbox" title="Select all"></div>
|
2026-02-12 09:41:25 +01:00
|
|
|
<div>Name</div>
|
|
|
|
|
<div>Type</div>
|
|
|
|
|
<div>Size</div>
|
|
|
|
|
<div>Modified</div>
|
2025-03-31 06:20:15 +02:00
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-19 23:28:29 +01:00
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(`Server responded with status: ${response.status}`);
|
|
|
|
|
}
|
|
|
|
|
const folders = await response.json();
|
|
|
|
|
|
|
|
|
|
// Clear existing files in both views
|
2026-02-14 00:12:18 +01:00
|
|
|
if (window.multiSelect) window.multiSelect.clear();
|
2025-03-19 23:28:29 +01:00
|
|
|
elements.filesGrid.innerHTML = '';
|
|
|
|
|
elements.filesListView.innerHTML = `
|
|
|
|
|
<div class="list-header">
|
2026-02-14 00:12:18 +01:00
|
|
|
<div class="list-header-checkbox"><input type="checkbox" id="select-all-checkbox" title="Select all"></div>
|
2026-02-12 09:41:25 +01:00
|
|
|
<div data-i18n="files.name">Name</div>
|
|
|
|
|
<div data-i18n="files.type">Type</div>
|
|
|
|
|
<div data-i18n="files.size">Size</div>
|
|
|
|
|
<div data-i18n="files.modified">Modified</div>
|
2025-03-19 23:28:29 +01:00
|
|
|
</div>
|
|
|
|
|
`;
|
2026-02-14 00:12:18 +01:00
|
|
|
|
|
|
|
|
// 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());
|
|
|
|
|
}
|
2025-03-19 23:28:29 +01:00
|
|
|
|
|
|
|
|
// 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 : [];
|
2025-04-04 04:30:49 +02:00
|
|
|
|
|
|
|
|
// Get user info for filtering
|
|
|
|
|
const USER_DATA_KEY = 'oxicloud_user';
|
|
|
|
|
const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}');
|
|
|
|
|
const username = userData.username || '';
|
|
|
|
|
|
|
|
|
|
// Filter folders before adding them to the view
|
|
|
|
|
const visibleFolders = folderList.filter(folder => {
|
|
|
|
|
// Skip system folders (starting with dot) when at root
|
|
|
|
|
if (!app.currentPath && folder.name.startsWith('.')) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-13 22:33:27 +01:00
|
|
|
// Skip other users' folders when at root
|
|
|
|
|
if (!app.currentPath && folder.name.startsWith('My Folder - ') && !folder.name.includes(username)) {
|
2025-04-04 04:30:49 +02:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Add filtered folders to the view
|
|
|
|
|
visibleFolders.forEach(folder => {
|
2025-03-19 23:28:29 +01:00
|
|
|
ui.addFolderToView(folder);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Also load files in this folder
|
2025-04-12 12:21:57 +02:00
|
|
|
const cacheTimestamp = new Date().getTime();
|
2026-02-12 09:41:25 +01:00
|
|
|
let filesUrl = `/api/files?t=${cacheTimestamp}`; // Add timestamp to avoid cache issues
|
2025-03-19 23:28:29 +01:00
|
|
|
if (app.currentPath) {
|
2025-04-12 12:21:57 +02:00
|
|
|
filesUrl += `&folder_id=${app.currentPath}`;
|
2025-03-19 23:28:29 +01:00
|
|
|
}
|
2026-02-12 09:41:25 +01:00
|
|
|
console.log(`Loading files from: ${filesUrl}`);
|
2025-03-19 23:28:29 +01:00
|
|
|
|
|
|
|
|
try {
|
2025-03-23 22:44:18 +01:00
|
|
|
console.log(`Fetching files from: ${filesUrl}`);
|
2025-03-31 06:20:15 +02:00
|
|
|
const filesResponse = await fetch(filesUrl, requestOptions); // Use same auth token
|
2025-03-23 22:44:18 +01:00
|
|
|
console.log(`Files response status: ${filesResponse.status}`);
|
|
|
|
|
|
2025-03-31 06:20:15 +02:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-19 23:28:29 +01:00
|
|
|
if (filesResponse.ok) {
|
|
|
|
|
const files = await filesResponse.json();
|
2025-03-23 22:44:18 +01:00
|
|
|
console.log(`Files received:`, files);
|
2025-03-19 23:28:29 +01:00
|
|
|
|
|
|
|
|
// Add files (check if it's an array)
|
|
|
|
|
const fileList = Array.isArray(files) ? files : [];
|
2025-03-23 22:44:18 +01:00
|
|
|
console.log(`Processing ${fileList.length} files`);
|
|
|
|
|
|
2025-03-19 23:28:29 +01:00
|
|
|
fileList.forEach(file => {
|
2025-03-23 22:44:18 +01:00
|
|
|
console.log(`Adding file to view: ${file.name} (${file.id})`);
|
2025-03-19 23:28:29 +01:00
|
|
|
ui.addFileToView(file);
|
|
|
|
|
});
|
2025-03-23 22:44:18 +01:00
|
|
|
} else {
|
|
|
|
|
const errorText = await filesResponse.text();
|
|
|
|
|
console.error(`Error loading files: ${filesResponse.status} - ${errorText}`);
|
2025-03-19 23:28:29 +01:00
|
|
|
}
|
|
|
|
|
} 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');
|
2025-04-12 12:21:57 +02:00
|
|
|
} finally {
|
2026-02-12 09:41:25 +01:00
|
|
|
// Mark that we are no longer loading files to allow future requests
|
2025-04-12 12:21:57 +02:00
|
|
|
window.isLoadingFiles = false;
|
2025-03-19 23:28:29 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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];
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-24 17:49:53 +01:00
|
|
|
/**
|
|
|
|
|
* Load trash items
|
|
|
|
|
*/
|
|
|
|
|
async function loadTrashItems() {
|
|
|
|
|
try {
|
|
|
|
|
// Clear existing content
|
2026-02-14 00:12:18 +01:00
|
|
|
if (window.multiSelect) window.multiSelect.clear();
|
2025-03-24 17:49:53 +01:00
|
|
|
elements.filesGrid.innerHTML = '';
|
|
|
|
|
elements.filesListView.innerHTML = `
|
2026-02-14 11:06:29 +01:00
|
|
|
<div class="list-header trash-header">
|
2026-02-12 09:41:25 +01:00
|
|
|
<div data-i18n="files.name">Name</div>
|
|
|
|
|
<div data-i18n="files.type">Type</div>
|
|
|
|
|
<div data-i18n="trash.original_location">Original location</div>
|
|
|
|
|
<div data-i18n="trash.deleted_date">Deletion date</div>
|
|
|
|
|
<div data-i18n="trash.actions">Actions</div>
|
2025-03-24 17:49:53 +01:00
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
2025-03-27 01:13:34 +01:00
|
|
|
// Translate the header if i18n is available
|
|
|
|
|
if (window.i18n && window.i18n.translatePage) {
|
|
|
|
|
window.i18n.translatePage();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-08 22:44:42 +01:00
|
|
|
// Update breadcrumb - just show Home
|
|
|
|
|
ui.updateBreadcrumb('');
|
2025-03-24 17:49:53 +01:00
|
|
|
|
|
|
|
|
// 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 = `
|
|
|
|
|
<i class="fas fa-trash" style="font-size: 48px; color: #ddd; margin-bottom: 16px;"></i>
|
2026-02-12 09:41:25 +01:00
|
|
|
<p>${window.i18n ? window.i18n.t('trash.empty_state') : 'The trash is empty'}</p>
|
2025-03-24 17:49:53 +01:00
|
|
|
`;
|
|
|
|
|
elements.filesGrid.appendChild(emptyState);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Process each trash item
|
|
|
|
|
trashItems.forEach(item => {
|
|
|
|
|
addTrashItemToView(item);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error loading trash items:', error);
|
2026-02-12 09:41:25 +01:00
|
|
|
window.ui.showNotification('Error', 'Error loading trash items');
|
2025-03-24 17:49:53 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add a trash item to the view
|
|
|
|
|
* @param {Object} item - Trash item object
|
|
|
|
|
*/
|
|
|
|
|
function addTrashItemToView(item) {
|
|
|
|
|
const isFile = item.item_type === 'file';
|
|
|
|
|
|
2026-02-14 11:06:29 +01:00
|
|
|
// Format date - backend sends trashed_at as ISO 8601 string
|
|
|
|
|
const deletedDate = new Date(item.trashed_at);
|
2025-03-24 17:49:53 +01:00
|
|
|
const formattedDate = deletedDate.toLocaleDateString() + ' ' +
|
|
|
|
|
deletedDate.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
|
|
|
|
|
|
2026-02-14 11:06:29 +01:00
|
|
|
// Determine type label and icon from extension (trash DTO has no mime_type)
|
|
|
|
|
let typeLabel;
|
|
|
|
|
let iconClass;
|
|
|
|
|
if (!isFile) {
|
|
|
|
|
iconClass = 'fas fa-folder';
|
|
|
|
|
typeLabel = window.i18n ? window.i18n.t('files.file_types.folder') : 'Folder';
|
|
|
|
|
} else {
|
|
|
|
|
const ext = (item.name.split('.').pop() || '').toLowerCase();
|
|
|
|
|
const imageExts = ['jpg','jpeg','png','gif','bmp','svg','webp','ico','tiff'];
|
|
|
|
|
const videoExts = ['mp4','avi','mkv','mov','wmv','flv','webm'];
|
|
|
|
|
const audioExts = ['mp3','wav','ogg','flac','aac','wma','m4a'];
|
|
|
|
|
const textExts = ['txt','md','csv','log','ini','cfg','conf'];
|
|
|
|
|
if (ext === 'pdf') {
|
|
|
|
|
iconClass = 'fas fa-file-pdf';
|
|
|
|
|
typeLabel = window.i18n ? window.i18n.t('files.file_types.pdf') : 'PDF';
|
|
|
|
|
} else if (imageExts.includes(ext)) {
|
|
|
|
|
iconClass = 'fas fa-file-image';
|
|
|
|
|
typeLabel = window.i18n ? window.i18n.t('files.file_types.image') : 'Image';
|
|
|
|
|
} else if (videoExts.includes(ext)) {
|
|
|
|
|
iconClass = 'fas fa-file-video';
|
|
|
|
|
typeLabel = window.i18n ? window.i18n.t('files.file_types.video') : 'Video';
|
|
|
|
|
} else if (audioExts.includes(ext)) {
|
|
|
|
|
iconClass = 'fas fa-file-audio';
|
|
|
|
|
typeLabel = window.i18n ? window.i18n.t('files.file_types.audio') : 'Audio';
|
|
|
|
|
} else if (textExts.includes(ext)) {
|
|
|
|
|
iconClass = 'fas fa-file-alt';
|
|
|
|
|
typeLabel = window.i18n ? window.i18n.t('files.file_types.text') : 'Text';
|
|
|
|
|
} else {
|
|
|
|
|
iconClass = 'fas fa-file';
|
|
|
|
|
typeLabel = window.i18n ? window.i18n.t('files.file_types.document') : 'Document';
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-03-24 17:49:53 +01:00
|
|
|
|
|
|
|
|
// 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 = `
|
|
|
|
|
<div class="file-icon">
|
|
|
|
|
<i class="${iconClass}"></i>
|
|
|
|
|
</div>
|
2026-02-15 17:53:25 +01:00
|
|
|
<div class="file-name">${escapeHtml(item.name)}</div>
|
|
|
|
|
<div class="file-info">${escapeHtml(typeLabel)} - ${escapeHtml(formattedDate)}</div>
|
2025-03-24 17:49:53 +01:00
|
|
|
<div class="trash-actions">
|
2026-02-12 09:41:25 +01:00
|
|
|
<button class="btn-restore" title="${window.i18n ? window.i18n.t('trash.restore') : 'Restore'}">
|
2025-03-24 17:49:53 +01:00
|
|
|
<i class="fas fa-undo"></i>
|
|
|
|
|
</button>
|
2026-02-12 09:41:25 +01:00
|
|
|
<button class="btn-delete" title="${window.i18n ? window.i18n.t('trash.delete_permanently') : 'Delete permanently'}">
|
2025-03-24 17:49:53 +01:00
|
|
|
<i class="fas fa-trash"></i>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
// Add action buttons event listeners
|
|
|
|
|
gridElement.querySelector('.btn-restore').addEventListener('click', async (e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
if (await fileOps.restoreFromTrash(item.id)) {
|
|
|
|
|
loadTrashItems();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
gridElement.querySelector('.btn-delete').addEventListener('click', async (e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
if (await fileOps.deletePermanently(item.id)) {
|
|
|
|
|
loadTrashItems();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
elements.filesGrid.appendChild(gridElement);
|
|
|
|
|
|
|
|
|
|
// List view element
|
|
|
|
|
const listElement = document.createElement('div');
|
|
|
|
|
listElement.className = 'file-item trash-item';
|
|
|
|
|
listElement.dataset.trashId = item.id;
|
|
|
|
|
listElement.dataset.originalId = item.original_id;
|
|
|
|
|
listElement.dataset.itemType = item.item_type;
|
|
|
|
|
|
|
|
|
|
listElement.innerHTML = `
|
|
|
|
|
<div class="name-cell">
|
|
|
|
|
<div class="file-icon">
|
|
|
|
|
<i class="${iconClass}"></i>
|
|
|
|
|
</div>
|
2026-02-15 17:53:25 +01:00
|
|
|
<span>${escapeHtml(item.name)}</span>
|
2025-03-24 17:49:53 +01:00
|
|
|
</div>
|
2026-02-15 17:53:25 +01:00
|
|
|
<div class="type-cell">${escapeHtml(typeLabel)}</div>
|
|
|
|
|
<div class="path-cell">${escapeHtml(item.original_path || '--')}</div>
|
|
|
|
|
<div class="date-cell">${escapeHtml(formattedDate)}</div>
|
2025-03-24 17:49:53 +01:00
|
|
|
<div class="actions-cell">
|
2026-02-12 09:41:25 +01:00
|
|
|
<button class="btn-restore" title="${window.i18n ? window.i18n.t('trash.restore') : 'Restore'}">
|
2025-03-24 17:49:53 +01:00
|
|
|
<i class="fas fa-undo"></i>
|
|
|
|
|
</button>
|
2026-02-12 09:41:25 +01:00
|
|
|
<button class="btn-delete" title="${window.i18n ? window.i18n.t('trash.delete_permanently') : 'Delete permanently'}">
|
2025-03-24 17:49:53 +01:00
|
|
|
<i class="fas fa-trash"></i>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-27 01:13:34 +01:00
|
|
|
/**
|
|
|
|
|
* Perform search with the given query
|
|
|
|
|
* @param {string} query - Search query
|
|
|
|
|
*/
|
|
|
|
|
async function performSearch(query) {
|
|
|
|
|
console.log(`Performing search for: "${query}"`);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Update UI to indicate search mode
|
|
|
|
|
app.isSearchMode = true;
|
|
|
|
|
|
|
|
|
|
// Set breadcrumb for search
|
2026-02-12 09:41:25 +01:00
|
|
|
ui.updateBreadcrumb(`Search: "${query}"`);
|
2025-03-27 01:13:34 +01:00
|
|
|
|
|
|
|
|
// Prepare search options
|
|
|
|
|
const options = {
|
|
|
|
|
recursive: true, // Search in all subfolders
|
|
|
|
|
limit: 100 // Limit results for performance
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Always restrict search to the user's current folder context
|
|
|
|
|
// This ensures users can't search outside their personal folder
|
|
|
|
|
if (!app.isTrashView) {
|
|
|
|
|
// If we're in a subfolder, search from there, otherwise use the user's home folder
|
|
|
|
|
options.folder_id = app.currentPath;
|
|
|
|
|
|
|
|
|
|
// Always include folder_id even if it's the root of user's home folder
|
|
|
|
|
// so user cannot search outside their allowed scope
|
|
|
|
|
if (!options.folder_id || options.folder_id === '') {
|
|
|
|
|
// Fall back to user's home folder - we should never be here
|
|
|
|
|
// because findUserHomeFolder should have set app.currentPath
|
|
|
|
|
console.warn("Search without folder_id - this shouldn't happen with proper user context");
|
|
|
|
|
|
|
|
|
|
// Try to get folder from localStorage if available
|
|
|
|
|
const USER_DATA_KEY = 'oxicloud_user';
|
|
|
|
|
const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}');
|
|
|
|
|
if (userData.username) {
|
|
|
|
|
console.log("Retrieving home folder for user before search");
|
|
|
|
|
await findUserHomeFolder(userData.username);
|
|
|
|
|
options.folder_id = app.currentPath;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log(`Searching with options:`, options);
|
|
|
|
|
|
|
|
|
|
// Perform the search
|
|
|
|
|
const searchResults = await window.search.searchFiles(query, options);
|
|
|
|
|
|
|
|
|
|
// Display search results
|
|
|
|
|
window.search.displaySearchResults(searchResults);
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Search error:', error);
|
2026-02-12 09:41:25 +01:00
|
|
|
window.ui.showNotification('Error', 'Error performing search');
|
2025-03-27 01:13:34 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-19 23:28:29 +01:00
|
|
|
// Expose needed functions to global scope
|
|
|
|
|
window.app = app;
|
|
|
|
|
window.loadFiles = loadFiles;
|
2025-03-24 17:49:53 +01:00
|
|
|
window.loadTrashItems = loadTrashItems;
|
2025-03-19 23:28:29 +01:00
|
|
|
window.formatFileSize = formatFileSize;
|
2025-03-27 01:13:34 +01:00
|
|
|
window.performSearch = performSearch;
|
2025-03-19 23:28:29 +01:00
|
|
|
|
|
|
|
|
// Set up global selectFolder function for navigation
|
|
|
|
|
window.selectFolder = (id, name) => {
|
|
|
|
|
app.currentPath = id;
|
|
|
|
|
ui.updateBreadcrumb(name);
|
|
|
|
|
loadFiles();
|
|
|
|
|
};
|
|
|
|
|
|
2025-04-01 21:14:09 +02:00
|
|
|
/**
|
|
|
|
|
* 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
|
2026-02-12 09:41:25 +01:00
|
|
|
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.shared') : 'Shared';
|
2025-04-01 21:14:09 +02:00
|
|
|
|
|
|
|
|
// Clear breadcrumb and show root
|
2026-02-08 22:44:42 +01:00
|
|
|
ui.updateBreadcrumb('');
|
2025-04-01 21:14:09 +02:00
|
|
|
|
|
|
|
|
// 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;
|
2025-04-02 03:43:44 +02:00
|
|
|
app.isFavoritesView = false;
|
2025-04-02 05:08:30 +02:00
|
|
|
app.isRecentView = false;
|
2025-04-01 21:14:09 +02:00
|
|
|
app.currentSection = 'files';
|
|
|
|
|
|
|
|
|
|
// Update UI
|
2026-02-12 09:41:25 +01:00
|
|
|
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.files') : 'Files';
|
2025-04-01 21:14:09 +02:00
|
|
|
|
|
|
|
|
// 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 = `
|
|
|
|
|
<div class="action-buttons">
|
2026-02-08 22:44:42 +01:00
|
|
|
<div class="upload-dropdown" id="upload-dropdown">
|
|
|
|
|
<button class="btn btn-primary" id="upload-btn">
|
|
|
|
|
<i class="fas fa-cloud-upload-alt" style="margin-right: 5px;"></i>
|
2026-02-12 09:41:25 +01:00
|
|
|
<span data-i18n="actions.upload">Upload</span>
|
2026-02-08 22:44:42 +01:00
|
|
|
<i class="fas fa-caret-down" style="margin-left: 4px; font-size: 12px;"></i>
|
|
|
|
|
</button>
|
|
|
|
|
<div class="upload-dropdown-menu" id="upload-dropdown-menu">
|
|
|
|
|
<button class="upload-dropdown-item" id="upload-files-btn">
|
|
|
|
|
<i class="fas fa-file"></i>
|
2026-02-12 09:41:25 +01:00
|
|
|
<span data-i18n="actions.upload_files">Upload files</span>
|
2026-02-08 22:44:42 +01:00
|
|
|
</button>
|
|
|
|
|
<button class="upload-dropdown-item" id="upload-folder-btn">
|
|
|
|
|
<i class="fas fa-folder-open"></i>
|
2026-02-12 09:41:25 +01:00
|
|
|
<span data-i18n="actions.upload_folder">Upload folder</span>
|
2026-02-08 22:44:42 +01:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-04-01 21:14:09 +02:00
|
|
|
<button class="btn btn-secondary" id="new-folder-btn">
|
2026-02-12 09:41:25 +01:00
|
|
|
<i class="fas fa-folder-plus" style="margin-right: 5px;"></i> <span data-i18n="actions.new_folder">New folder</span>
|
2025-04-01 21:14:09 +02:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="view-toggle">
|
2026-02-12 09:41:25 +01:00
|
|
|
<button class="toggle-btn active" id="grid-view-btn" title="Grid view">
|
2025-04-01 21:14:09 +02:00
|
|
|
<i class="fas fa-th"></i>
|
|
|
|
|
</button>
|
2026-02-12 09:41:25 +01:00
|
|
|
<button class="toggle-btn" id="list-view-btn" title="List view">
|
2025-04-01 21:14:09 +02:00
|
|
|
<i class="fas fa-list"></i>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
elements.actionsBar.style.display = 'flex';
|
|
|
|
|
|
|
|
|
|
// Restore event listeners
|
2026-02-08 22:44:42 +01:00
|
|
|
setupUploadDropdown();
|
2025-04-01 21:14:09 +02:00
|
|
|
|
2026-02-03 17:59:04 +01:00
|
|
|
document.getElementById('new-folder-btn').addEventListener('click', async () => {
|
|
|
|
|
const folderName = await window.Modal.promptNewFolder();
|
2025-04-01 21:14:09 +02:00
|
|
|
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';
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-04 04:30:49 +02:00
|
|
|
// 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 = '';
|
|
|
|
|
}
|
2025-04-01 21:14:09 +02:00
|
|
|
loadFiles();
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-02 03:43:44 +02:00
|
|
|
/**
|
|
|
|
|
* 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
|
2026-02-12 09:41:25 +01:00
|
|
|
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.favorites') : 'Favorites';
|
2025-04-02 03:43:44 +02:00
|
|
|
|
|
|
|
|
// Clear breadcrumb and show root
|
2026-02-08 22:44:42 +01:00
|
|
|
ui.updateBreadcrumb('');
|
2025-04-02 03:43:44 +02:00
|
|
|
|
|
|
|
|
// Hide shared view if it exists
|
|
|
|
|
if (window.sharedView) {
|
|
|
|
|
window.sharedView.hide();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Configure actions bar for favorites view
|
|
|
|
|
elements.actionsBar.innerHTML = `
|
|
|
|
|
<div class="action-buttons">
|
|
|
|
|
<!-- No actions needed for favorites view -->
|
|
|
|
|
</div>
|
|
|
|
|
<div class="view-toggle">
|
2026-02-12 09:41:25 +01:00
|
|
|
<button class="toggle-btn active" id="grid-view-btn" title="Grid view">
|
2025-04-02 03:43:44 +02:00
|
|
|
<i class="fas fa-th"></i>
|
|
|
|
|
</button>
|
2026-02-12 09:41:25 +01:00
|
|
|
<button class="toggle-btn" id="list-view-btn" title="List view">
|
2025-04-02 03:43:44 +02:00
|
|
|
<i class="fas fa-list"></i>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
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 = `
|
|
|
|
|
<div class="empty-state">
|
|
|
|
|
<i class="fas fa-exclamation-circle" style="font-size: 48px; color: #f44336; margin-bottom: 16px;"></i>
|
2026-02-12 09:41:25 +01:00
|
|
|
<p>Error loading the favorites module</p>
|
2025-04-02 03:43:44 +02:00
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-02 05:08:30 +02:00
|
|
|
/**
|
|
|
|
|
* 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
|
2026-02-12 09:41:25 +01:00
|
|
|
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.recent') : 'Recent';
|
2025-04-02 05:08:30 +02:00
|
|
|
|
|
|
|
|
// Clear breadcrumb and show root
|
2026-02-08 22:44:42 +01:00
|
|
|
ui.updateBreadcrumb('');
|
2025-04-02 05:08:30 +02:00
|
|
|
|
|
|
|
|
// Hide shared view if it exists
|
|
|
|
|
if (window.sharedView) {
|
|
|
|
|
window.sharedView.hide();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Configure actions bar for recent view
|
|
|
|
|
elements.actionsBar.innerHTML = `
|
|
|
|
|
<div class="action-buttons">
|
|
|
|
|
<button class="btn btn-secondary" id="clear-recent-btn">
|
2026-02-12 09:41:25 +01:00
|
|
|
<i class="fas fa-broom" style="margin-right: 5px;"></i> <span data-i18n="actions.clear_recent">Clear recent</span>
|
2025-04-02 05:08:30 +02:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="view-toggle">
|
2026-02-12 09:41:25 +01:00
|
|
|
<button class="toggle-btn active" id="grid-view-btn" title="Grid view">
|
2025-04-02 05:08:30 +02:00
|
|
|
<i class="fas fa-th"></i>
|
|
|
|
|
</button>
|
2026-02-12 09:41:25 +01:00
|
|
|
<button class="toggle-btn" id="list-view-btn" title="List view">
|
2025-04-02 05:08:30 +02:00
|
|
|
<i class="fas fa-list"></i>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
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();
|
2026-02-12 09:41:25 +01:00
|
|
|
window.ui.showNotification('Cleanup completed', 'Recent files history has been cleared');
|
2025-04-02 05:08:30 +02:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 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 = `
|
|
|
|
|
<div class="empty-state">
|
|
|
|
|
<i class="fas fa-exclamation-circle" style="font-size: 48px; color: #f44336; margin-bottom: 16px;"></i>
|
2026-02-12 09:41:25 +01:00
|
|
|
<p>Error loading the recent files module</p>
|
2025-04-02 05:08:30 +02:00
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-01 21:14:09 +02:00
|
|
|
// Expose view switching functions globally
|
|
|
|
|
window.switchToFilesView = switchToFilesView;
|
|
|
|
|
window.switchToSharedView = switchToSharedView;
|
2025-04-02 03:43:44 +02:00
|
|
|
window.switchToFavoritesView = switchToFavoritesView;
|
2025-04-02 05:08:30 +02:00
|
|
|
window.switchToRecentFilesView = switchToRecentFilesView;
|
2025-04-01 21:14:09 +02:00
|
|
|
|
2026-02-03 17:59:04 +01:00
|
|
|
/**
|
|
|
|
|
* 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');
|
|
|
|
|
|
2026-02-15 17:53:25 +01:00
|
|
|
if (!token) {
|
2026-02-03 17:59:04 +01:00
|
|
|
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;
|
|
|
|
|
|
2026-02-11 12:26:29 +01:00
|
|
|
/**
|
|
|
|
|
* Show User Profile modal with account details
|
|
|
|
|
*/
|
|
|
|
|
function showUserProfileModal() {
|
|
|
|
|
const USER_DATA_KEY = 'oxicloud_user';
|
|
|
|
|
const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}');
|
2026-02-12 09:41:25 +01:00
|
|
|
const username = userData.username || 'User';
|
2026-02-11 12:26:29 +01:00
|
|
|
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 = `
|
|
|
|
|
<div class="about-modal" style="max-width:380px">
|
|
|
|
|
<div style="text-align:center;padding:20px 20px 0">
|
|
|
|
|
<div style="width:64px;height:64px;border-radius:50%;background:linear-gradient(135deg,#3b82f6,#6366f1);color:#fff;display:inline-flex;align-items:center;justify-content:center;font-size:24px;font-weight:700;margin-bottom:12px">${initials}</div>
|
|
|
|
|
<h3 style="margin:0;font-size:18px;color:#1a1a2e">${username}</h3>
|
|
|
|
|
<p style="margin:4px 0 0;font-size:13px;color:#64748b">${email}</p>
|
|
|
|
|
<span style="display:inline-block;margin-top:8px;padding:2px 10px;border-radius:10px;font-size:11px;font-weight:600;${
|
|
|
|
|
role === 'admin'
|
|
|
|
|
? 'background:#dbeafe;color:#1d4ed8'
|
|
|
|
|
: 'background:#f1f5f9;color:#64748b'
|
|
|
|
|
}">${role === 'admin' ? '🛡️ Admin' : '👤 ' + t('user_menu.role_user', 'User')}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="padding:16px 20px">
|
|
|
|
|
<div style="font-size:12px;color:#64748b;text-transform:uppercase;letter-spacing:.05em;margin-bottom:6px">
|
|
|
|
|
<i class="fas fa-database" style="margin-right:4px"></i>${t('storage.title', 'Storage')}
|
|
|
|
|
</div>
|
|
|
|
|
<div style="background:#f1f5f9;border-radius:6px;height:8px;overflow:hidden;margin-bottom:4px">
|
|
|
|
|
<div style="height:100%;width:${percentage}%;background:${barColor};border-radius:6px;transition:width .3s"></div>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="font-size:12px;color:#64748b;text-align:right">${percentage}% · ${formatFileSize(usedBytes)} / ${quotaBytes > 0 ? formatFileSize(quotaBytes) : '∞'}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="padding:0 20px 16px;display:flex;justify-content:center">
|
|
|
|
|
<button id="profile-modal-close" style="padding:8px 24px;border:1px solid #e2e8f0;border-radius:8px;background:#fff;color:#334155;font-size:13px;font-weight:600;cursor:pointer;transition:background .15s">${t('actions.close', 'Close')}</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
document.body.appendChild(overlay);
|
|
|
|
|
// Show with animation
|
|
|
|
|
requestAnimationFrame(() => overlay.classList.add('show'));
|
|
|
|
|
|
|
|
|
|
// Close handlers
|
|
|
|
|
overlay.querySelector('#profile-modal-close').addEventListener('click', () => {
|
|
|
|
|
overlay.classList.remove('show');
|
|
|
|
|
setTimeout(() => overlay.remove(), 200);
|
|
|
|
|
});
|
|
|
|
|
overlay.addEventListener('click', (e) => {
|
|
|
|
|
if (e.target === overlay) {
|
|
|
|
|
overlay.classList.remove('show');
|
|
|
|
|
setTimeout(() => overlay.remove(), 200);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-20 09:22:31 +01:00
|
|
|
/**
|
2025-03-27 01:13:34 +01:00
|
|
|
* Check if user is authenticated and load user's home folder
|
2025-03-20 09:22:31 +01:00
|
|
|
*/
|
2026-02-13 21:49:01 +01:00
|
|
|
async function checkAuthentication() {
|
2025-03-31 06:20:15 +02:00
|
|
|
try {
|
|
|
|
|
const TOKEN_KEY = 'oxicloud_token';
|
2026-02-13 21:33:45 +01:00
|
|
|
const REFRESH_TOKEN_KEY = 'oxicloud_refresh_token';
|
|
|
|
|
const TOKEN_EXPIRY_KEY = 'oxicloud_token_expiry';
|
2025-03-31 06:20:15 +02:00
|
|
|
const USER_DATA_KEY = 'oxicloud_user';
|
|
|
|
|
|
2026-02-13 21:33:45 +01:00
|
|
|
// --- OIDC exchange code handling ---
|
|
|
|
|
// After OIDC login, the backend redirects here with ?oidc_code=...
|
|
|
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
|
|
|
const oidcCode = urlParams.get('oidc_code');
|
|
|
|
|
|
|
|
|
|
if (oidcCode) {
|
|
|
|
|
console.log('OIDC exchange code detected, exchanging for tokens...');
|
|
|
|
|
try {
|
|
|
|
|
const exchangeResponse = await fetch('/api/auth/oidc/exchange', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ code: oidcCode })
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!exchangeResponse.ok) {
|
|
|
|
|
const errText = await exchangeResponse.text();
|
|
|
|
|
console.error('OIDC token exchange failed:', exchangeResponse.status, errText);
|
2026-02-13 22:44:20 +01:00
|
|
|
window.location.href = '/login?source=oidc_error';
|
2026-02-13 21:33:45 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const data = await exchangeResponse.json();
|
|
|
|
|
console.log('OIDC token exchange successful');
|
|
|
|
|
|
|
|
|
|
// Store tokens (same logic as password login in auth.js)
|
|
|
|
|
const token = data.access_token || data.token;
|
|
|
|
|
const refreshToken = data.refresh_token || data.refreshToken;
|
|
|
|
|
|
|
|
|
|
if (token) {
|
|
|
|
|
localStorage.setItem(TOKEN_KEY, token);
|
|
|
|
|
if (refreshToken) localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken);
|
|
|
|
|
|
|
|
|
|
// Parse JWT expiry
|
|
|
|
|
let parsedExpiry = false;
|
|
|
|
|
const tokenParts = token.split('.');
|
|
|
|
|
if (tokenParts.length === 3) {
|
|
|
|
|
try {
|
|
|
|
|
const payload = JSON.parse(atob(tokenParts[1]));
|
|
|
|
|
if (payload.exp) {
|
|
|
|
|
const expiryDate = new Date(payload.exp * 1000);
|
|
|
|
|
if (!isNaN(expiryDate.getTime())) {
|
|
|
|
|
localStorage.setItem(TOKEN_EXPIRY_KEY, expiryDate.toISOString());
|
|
|
|
|
parsedExpiry = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Error parsing JWT:', e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!parsedExpiry) {
|
|
|
|
|
const expiry = new Date();
|
|
|
|
|
expiry.setDate(expiry.getDate() + 30);
|
|
|
|
|
localStorage.setItem(TOKEN_EXPIRY_KEY, expiry.toISOString());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Store user data
|
|
|
|
|
if (data.user) {
|
|
|
|
|
localStorage.setItem(USER_DATA_KEY, JSON.stringify(data.user));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clean URL and reload without the oidc_code param
|
|
|
|
|
window.history.replaceState({}, document.title, '/');
|
|
|
|
|
window.location.reload();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('OIDC exchange error:', err);
|
2026-02-13 22:44:20 +01:00
|
|
|
window.location.href = '/login?source=oidc_error';
|
2026-02-13 21:33:45 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-15 17:53:25 +01:00
|
|
|
// Verify token exists
|
2025-03-31 06:20:15 +02:00
|
|
|
const token = localStorage.getItem(TOKEN_KEY);
|
|
|
|
|
|
|
|
|
|
if (!token) {
|
|
|
|
|
console.log('No token found, redirecting to login');
|
2026-02-15 17:53:25 +01:00
|
|
|
window.location.href = '/login?source=app';
|
2025-03-31 06:20:15 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-15 17:53:25 +01:00
|
|
|
// Token exists, proceed with app initialization
|
2025-03-31 06:20:15 +02:00
|
|
|
console.log('Token found, proceeding with app initialization');
|
|
|
|
|
|
|
|
|
|
// Display user information if available
|
|
|
|
|
const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}');
|
|
|
|
|
if (userData.username) {
|
|
|
|
|
// Update user avatar with initials
|
|
|
|
|
const userInitials = userData.username.substring(0, 2).toUpperCase();
|
2026-02-08 22:44:42 +01:00
|
|
|
document.querySelectorAll('.user-avatar, .user-menu-avatar').forEach(el => {
|
|
|
|
|
el.textContent = userInitials;
|
|
|
|
|
});
|
|
|
|
|
// Update user menu info
|
|
|
|
|
const menuName = document.getElementById('user-menu-name');
|
|
|
|
|
const menuEmail = document.getElementById('user-menu-email');
|
|
|
|
|
if (menuName) menuName.textContent = userData.username;
|
|
|
|
|
if (menuEmail) menuEmail.textContent = userData.email || '';
|
2025-03-31 06:20:15 +02:00
|
|
|
|
2026-02-03 17:59:04 +01:00
|
|
|
// Update storage usage information with cached data first (for fast display)
|
2025-04-09 00:21:20 +02:00
|
|
|
updateStorageUsageDisplay(userData);
|
|
|
|
|
|
2026-02-03 17:59:04 +01:00
|
|
|
// Then refresh user data from server in the background to get updated storage
|
|
|
|
|
refreshUserData().then(freshData => {
|
|
|
|
|
if (freshData) {
|
|
|
|
|
console.log('Storage usage updated from server');
|
|
|
|
|
}
|
|
|
|
|
}).catch(err => {
|
|
|
|
|
console.warn('Could not refresh user data:', err);
|
|
|
|
|
});
|
|
|
|
|
|
2025-03-31 06:20:15 +02:00
|
|
|
// Find and load the user's home folder
|
|
|
|
|
findUserHomeFolder(userData.username);
|
|
|
|
|
} else {
|
2026-02-15 17:53:25 +01:00
|
|
|
// No user data but token exists — try to fetch from server
|
|
|
|
|
console.log('No user data, attempting to fetch from server');
|
|
|
|
|
try {
|
|
|
|
|
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);
|
|
|
|
|
updateStorageUsageDisplay(freshData);
|
|
|
|
|
findUserHomeFolder(freshData.username);
|
|
|
|
|
} else {
|
|
|
|
|
// Server didn't return valid user data — token is likely invalid
|
|
|
|
|
console.warn('Could not retrieve user data, redirecting to login');
|
|
|
|
|
localStorage.removeItem(TOKEN_KEY);
|
|
|
|
|
localStorage.removeItem(REFRESH_TOKEN_KEY);
|
|
|
|
|
localStorage.removeItem(TOKEN_EXPIRY_KEY);
|
|
|
|
|
localStorage.removeItem(USER_DATA_KEY);
|
|
|
|
|
window.location.href = '/login?source=invalid_session';
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Failed to fetch user data:', err);
|
|
|
|
|
localStorage.removeItem(TOKEN_KEY);
|
|
|
|
|
localStorage.removeItem(REFRESH_TOKEN_KEY);
|
|
|
|
|
localStorage.removeItem(TOKEN_EXPIRY_KEY);
|
|
|
|
|
localStorage.removeItem(USER_DATA_KEY);
|
|
|
|
|
window.location.href = '/login?source=session_error';
|
|
|
|
|
}
|
2025-03-31 06:20:15 +02:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error during authentication check:', error);
|
2026-02-15 17:53:25 +01:00
|
|
|
// On error, redirect to login cleanly — never create fake tokens
|
|
|
|
|
localStorage.removeItem('oxicloud_token');
|
|
|
|
|
localStorage.removeItem('oxicloud_refresh_token');
|
|
|
|
|
localStorage.removeItem('oxicloud_token_expiry');
|
|
|
|
|
localStorage.removeItem('oxicloud_user');
|
|
|
|
|
window.location.href = '/login?source=auth_error';
|
2025-03-27 01:13:34 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Find the user's home folder and load it
|
|
|
|
|
* @param {string} username - The current user's username
|
|
|
|
|
*/
|
|
|
|
|
async function findUserHomeFolder(username) {
|
|
|
|
|
try {
|
|
|
|
|
console.log("Finding home folder for user:", username);
|
|
|
|
|
|
2025-03-31 06:20:15 +02:00
|
|
|
// CRITICAL FIX: Always create a default folder if needed
|
|
|
|
|
// This prevents loops when the folder can't be found
|
|
|
|
|
const defaultFolder = {
|
|
|
|
|
id: 'default-folder',
|
2026-02-12 15:31:39 +01:00
|
|
|
name: `My Folder - ${username}`,
|
2025-03-31 06:20:15 +02:00
|
|
|
parent_id: null,
|
|
|
|
|
created_at: Date.now() / 1000,
|
|
|
|
|
updated_at: Date.now() / 1000
|
|
|
|
|
};
|
2025-03-27 01:13:34 +01:00
|
|
|
|
2025-03-31 06:20:15 +02:00
|
|
|
// First, load all folders at the root
|
|
|
|
|
console.log("Fetching folders from API");
|
2025-03-27 01:13:34 +01:00
|
|
|
|
2025-03-31 06:20:15 +02:00
|
|
|
// Set max retries and timeout to prevent potential infinite loops
|
|
|
|
|
let retries = 0;
|
|
|
|
|
const maxRetries = 1; // Reduced from 2 to 1
|
2025-03-27 01:13:34 +01:00
|
|
|
|
2025-03-31 06:20:15 +02:00
|
|
|
while (retries < maxRetries) {
|
|
|
|
|
try {
|
|
|
|
|
const controller = new AbortController();
|
|
|
|
|
const timeoutId = setTimeout(() => controller.abort(), 3000); // Reduced timeout to 3 seconds
|
2025-03-27 01:13:34 +01:00
|
|
|
|
2026-02-13 08:54:51 +01:00
|
|
|
const folderToken = localStorage.getItem('oxicloud_token');
|
|
|
|
|
const folderHeaders = folderToken ? { 'Authorization': `Bearer ${folderToken}` } : {};
|
2025-03-31 06:20:15 +02:00
|
|
|
const response = await fetch('/api/folders', {
|
2026-02-13 08:54:51 +01:00
|
|
|
headers: folderHeaders,
|
2025-03-31 06:20:15 +02:00
|
|
|
signal: controller.signal
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
|
|
|
|
|
|
if (response.status === 401 || response.status === 403) {
|
|
|
|
|
console.warn(`Authentication error (${response.status}) when fetching folders`);
|
|
|
|
|
// Use default folder to break the loop
|
|
|
|
|
console.log('Using default folder to prevent redirection loop');
|
|
|
|
|
app.userHomeFolderId = defaultFolder.id;
|
|
|
|
|
app.userHomeFolderName = defaultFolder.name;
|
|
|
|
|
app.currentPath = defaultFolder.id;
|
|
|
|
|
ui.updateBreadcrumb(defaultFolder.name);
|
|
|
|
|
loadFiles();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(`Error loading folders: ${response.status}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const folders = await response.json();
|
|
|
|
|
const folderList = Array.isArray(folders) ? folders : [];
|
|
|
|
|
|
|
|
|
|
console.log(`Found ${folderList.length} folders at root`);
|
|
|
|
|
|
|
|
|
|
// Look for a folder with a name pattern that matches the user's home folder
|
2026-02-13 22:33:27 +01:00
|
|
|
const homeFolderPattern = `My Folder - ${username}`;
|
2025-03-31 06:20:15 +02:00
|
|
|
|
2026-02-13 22:31:05 +01:00
|
|
|
// Filter first to remove system folders and other users' folders
|
2025-04-04 04:30:49 +02:00
|
|
|
const visibleFolders = folderList.filter(folder => {
|
|
|
|
|
// Skip system folders (starting with dot)
|
|
|
|
|
if (folder.name.startsWith('.')) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-13 22:33:27 +01:00
|
|
|
// Skip other users' home folders
|
|
|
|
|
if (folder.name.startsWith('My Folder - ') && !folder.name.includes(username)) {
|
2025-04-04 04:30:49 +02:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-13 22:33:27 +01:00
|
|
|
// Find the user's home folder from filtered list
|
|
|
|
|
let homeFolder = visibleFolders.find(folder => folder.name === homeFolderPattern);
|
2025-03-31 06:20:15 +02:00
|
|
|
|
|
|
|
|
if (homeFolder) {
|
|
|
|
|
console.log(`Found user's home folder: ${homeFolder.name} (${homeFolder.id})`);
|
|
|
|
|
|
|
|
|
|
// Store the home folder ID and name in the app state
|
|
|
|
|
// This is used for breadcrumb navigation and restricting user access
|
|
|
|
|
app.userHomeFolderId = homeFolder.id;
|
|
|
|
|
app.userHomeFolderName = homeFolder.name;
|
|
|
|
|
|
|
|
|
|
// Set this as the current path and load its contents
|
|
|
|
|
app.currentPath = homeFolder.id;
|
|
|
|
|
ui.updateBreadcrumb(homeFolder.name);
|
|
|
|
|
loadFiles();
|
|
|
|
|
return; // Success! Exit function
|
|
|
|
|
} else {
|
2026-02-13 22:31:05 +01:00
|
|
|
console.warn("Could not find user's home folder");
|
2025-03-31 06:20:15 +02:00
|
|
|
|
2026-02-13 22:31:05 +01:00
|
|
|
// SECURITY: Never fall back to another user's folder.
|
|
|
|
|
// If user's own folder doesn't exist, show root (empty state).
|
|
|
|
|
console.log('User home folder not found, showing root');
|
|
|
|
|
app.currentPath = '';
|
|
|
|
|
ui.updateBreadcrumb('');
|
|
|
|
|
loadFiles();
|
|
|
|
|
return;
|
2025-03-31 06:20:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If we get here, we've successfully processed the response
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
} catch (fetchError) {
|
|
|
|
|
retries++;
|
|
|
|
|
console.error(`Fetch attempt ${retries} failed:`, fetchError);
|
|
|
|
|
|
|
|
|
|
if (retries >= maxRetries) {
|
|
|
|
|
throw fetchError; // Re-throw after max retries
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Wait before retrying
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
2025-03-27 01:13:34 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error finding user home folder:', error);
|
|
|
|
|
|
|
|
|
|
// Fall back to loading root in case of error
|
2025-03-31 06:20:15 +02:00
|
|
|
// This is a critical fallback to prevent infinite loops
|
2025-03-27 01:13:34 +01:00
|
|
|
app.currentPath = '';
|
|
|
|
|
ui.updateBreadcrumb('');
|
|
|
|
|
loadFiles();
|
2025-03-20 09:22:31 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Logout - clear all auth data and redirect to login
|
|
|
|
|
*/
|
|
|
|
|
function logout() {
|
2026-02-12 09:41:25 +01:00
|
|
|
// Variable names as per auth.js
|
2025-03-20 09:22:31 +01:00
|
|
|
const TOKEN_KEY = 'oxicloud_token';
|
|
|
|
|
const REFRESH_TOKEN_KEY = 'oxicloud_refresh_token';
|
|
|
|
|
const TOKEN_EXPIRY_KEY = 'oxicloud_token_expiry';
|
|
|
|
|
const USER_DATA_KEY = 'oxicloud_user';
|
|
|
|
|
|
|
|
|
|
// Clear all authentication data
|
|
|
|
|
localStorage.removeItem(TOKEN_KEY);
|
|
|
|
|
localStorage.removeItem(REFRESH_TOKEN_KEY);
|
|
|
|
|
localStorage.removeItem(TOKEN_EXPIRY_KEY);
|
|
|
|
|
localStorage.removeItem(USER_DATA_KEY);
|
|
|
|
|
|
2025-03-31 06:20:15 +02:00
|
|
|
// Also clear session storage counters
|
|
|
|
|
sessionStorage.removeItem('redirect_count');
|
|
|
|
|
|
|
|
|
|
// Redirect to login page with correct path
|
2026-02-13 22:44:20 +01:00
|
|
|
window.location.href = '/login';
|
2025-03-20 09:22:31 +01:00
|
|
|
}
|
|
|
|
|
|
2025-04-09 00:21:20 +02:00
|
|
|
/**
|
|
|
|
|
* Update the storage usage display with the user's actual storage usage
|
|
|
|
|
* @param {Object} userData - The user data object
|
|
|
|
|
*/
|
|
|
|
|
function updateStorageUsageDisplay(userData) {
|
|
|
|
|
// Default values
|
|
|
|
|
let usedBytes = 0;
|
|
|
|
|
let quotaBytes = 10737418240; // Default 10GB
|
|
|
|
|
let usagePercentage = 0;
|
|
|
|
|
|
|
|
|
|
// Get values from user data if available
|
|
|
|
|
if (userData) {
|
|
|
|
|
usedBytes = userData.storage_used_bytes || 0;
|
|
|
|
|
quotaBytes = userData.storage_quota_bytes || 10737418240;
|
|
|
|
|
|
|
|
|
|
// Calculate percentage (avoid division by zero)
|
|
|
|
|
if (quotaBytes > 0) {
|
|
|
|
|
usagePercentage = Math.min(Math.round((usedBytes / quotaBytes) * 100), 100);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Format the numbers for display
|
|
|
|
|
const usedFormatted = formatFileSize(usedBytes);
|
|
|
|
|
const quotaFormatted = formatFileSize(quotaBytes);
|
|
|
|
|
|
|
|
|
|
// Update the storage display elements
|
|
|
|
|
const storageFill = document.querySelector('.storage-fill');
|
|
|
|
|
const storageInfo = document.querySelector('.storage-info');
|
|
|
|
|
|
|
|
|
|
if (storageFill) {
|
|
|
|
|
storageFill.style.width = `${usagePercentage}%`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (storageInfo) {
|
2026-02-03 17:59:04 +01:00
|
|
|
// Remove data-i18n attribute to prevent i18n from overwriting our value
|
|
|
|
|
storageInfo.removeAttribute('data-i18n');
|
|
|
|
|
|
|
|
|
|
// Use i18n if available
|
|
|
|
|
if (window.i18n && window.i18n.t) {
|
|
|
|
|
storageInfo.textContent = window.i18n.t('storage.used', {
|
|
|
|
|
percentage: usagePercentage,
|
|
|
|
|
used: usedFormatted,
|
|
|
|
|
total: quotaFormatted
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
storageInfo.textContent = `${usagePercentage}% used (${usedFormatted} / ${quotaFormatted})`;
|
|
|
|
|
}
|
2025-04-09 00:21:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log(`Updated storage display: ${usagePercentage}% (${usedFormatted} / ${quotaFormatted})`);
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-19 23:28:29 +01:00
|
|
|
// Initialize app when DOM is ready
|
|
|
|
|
document.addEventListener('DOMContentLoaded', initApp);
|