fix: folder trash/delete operations & frontend refactoring
- Fix recursive CTE: add missing RECURSIVE keyword in move_to_trash and restore_from_trash SQL queries (relation 'descendants' does not exist) - Fix folder deletion: delete descendant files before folder to avoid 'duplicate key violates unique constraint idx_files_unique_name_at_root' - Simplify trash model: only mark the folder as trashed, not child files (implicit trash via parent) - Update trash_items view: filter to show only top-level trashed items - Update schema.sql: change files.folder_id FK from ON DELETE SET NULL to ON DELETE CASCADE - Fix trash view icons: folders and files now show correct visual icons (folder-icon, pdf-icon, etc.) in trash view - Frontend refactoring: extract inline CSS/JS from admin.html and profile.html into dedicated external files - Frontend cleanup: replace all inline style attributes with CSS classes - Frontend cleanup: replace style.display JS - Fix recursive CTE: add missing RECURSIVE keyword in move_to_trash and restore_from_trash SQL queries (relation 'descendants' d
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
/**
|
||||
* Authentication/session bootstrap and home-folder resolution
|
||||
*/
|
||||
|
||||
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);
|
||||
|
||||
localStorage.setItem(USER_DATA_KEY, JSON.stringify(userData));
|
||||
window.updateStorageUsageDisplay(userData);
|
||||
|
||||
return userData;
|
||||
} catch (error) {
|
||||
console.error('Error refreshing user data:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function checkAuthentication() {
|
||||
try {
|
||||
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';
|
||||
|
||||
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);
|
||||
window.location.href = '/login?source=oidc_error';
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await exchangeResponse.json();
|
||||
console.log('OIDC token exchange successful');
|
||||
|
||||
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);
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
if (data.user) {
|
||||
localStorage.setItem(USER_DATA_KEY, JSON.stringify(data.user));
|
||||
}
|
||||
|
||||
window.history.replaceState({}, document.title, '/');
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('OIDC exchange error:', err);
|
||||
window.location.href = '/login?source=oidc_error';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const token = localStorage.getItem(TOKEN_KEY);
|
||||
|
||||
if (!token) {
|
||||
console.log('No token found, redirecting to login');
|
||||
window.location.href = '/login?source=app';
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Token found, proceeding with app initialization');
|
||||
|
||||
const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}');
|
||||
if (userData.username) {
|
||||
const userInitials = userData.username.substring(0, 2).toUpperCase();
|
||||
document.querySelectorAll('.user-avatar, .user-menu-avatar').forEach(el => {
|
||||
el.textContent = userInitials;
|
||||
});
|
||||
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 || '';
|
||||
|
||||
window.updateStorageUsageDisplay(userData);
|
||||
|
||||
refreshUserData().then(freshData => {
|
||||
if (freshData) {
|
||||
console.log('Storage usage updated from server');
|
||||
}
|
||||
}).catch(err => {
|
||||
console.warn('Could not refresh user data:', err);
|
||||
});
|
||||
|
||||
resolveHomeFolder().then(() => window.loadFiles());
|
||||
} else {
|
||||
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);
|
||||
window.updateStorageUsageDisplay(freshData);
|
||||
resolveHomeFolder().then(() => window.loadFiles());
|
||||
} else {
|
||||
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';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during authentication check:', error);
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveHomeFolder() {
|
||||
const app = window.app;
|
||||
|
||||
if (app.userHomeFolderId) return;
|
||||
try {
|
||||
const token = localStorage.getItem('oxicloud_token');
|
||||
const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
|
||||
const response = await fetch('/api/folders', { headers });
|
||||
if (!response.ok) {
|
||||
console.warn(`Could not fetch home folder: ${response.status}`);
|
||||
return;
|
||||
}
|
||||
const folders = await response.json();
|
||||
const folderList = Array.isArray(folders) ? folders : [];
|
||||
if (folderList.length > 0) {
|
||||
const home = folderList[0];
|
||||
app.userHomeFolderId = home.id;
|
||||
app.userHomeFolderName = home.name;
|
||||
app.currentPath = home.id;
|
||||
window.ui.updateBreadcrumb(home.name);
|
||||
console.log(`Home folder resolved: ${home.name} (${home.id})`);
|
||||
} else {
|
||||
console.warn('No root folders found for user');
|
||||
app.currentPath = '';
|
||||
window.ui.updateBreadcrumb('');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error resolving home folder:', error);
|
||||
app.currentPath = '';
|
||||
window.ui.updateBreadcrumb('');
|
||||
}
|
||||
}
|
||||
|
||||
window.refreshUserData = refreshUserData;
|
||||
window.checkAuthentication = checkAuthentication;
|
||||
window.resolveHomeFolder = resolveHomeFolder;
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* OxiCloud - App bootstrap
|
||||
* Isolated startup trigger for the main application initializer.
|
||||
*/
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (typeof window.initApp === 'function') {
|
||||
window.initApp();
|
||||
}
|
||||
});
|
||||
} else if (typeof window.initApp === 'function') {
|
||||
window.initApp();
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* Files view loading logic
|
||||
*/
|
||||
|
||||
async function loadFiles(options = {}) {
|
||||
const app = window.app;
|
||||
const elements = window.appElements;
|
||||
|
||||
try {
|
||||
console.log("Starting loadFiles() - loading files...", options);
|
||||
|
||||
const forceRefresh = options.forceRefresh || false;
|
||||
|
||||
if (window.isLoadingFiles) {
|
||||
console.log("A file load is already in progress, ignoring request");
|
||||
return;
|
||||
}
|
||||
|
||||
window.isLoadingFiles = true;
|
||||
|
||||
elements.filesGrid.innerHTML = `
|
||||
<div class="files-loading-spinner">
|
||||
<div class="spinner"></div>
|
||||
<span>${window.i18n ? window.i18n.t('files.loading') : 'Loading files…'}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (!app.userHomeFolderId) {
|
||||
await window.resolveHomeFolder();
|
||||
}
|
||||
|
||||
const timestamp = new Date().getTime();
|
||||
let url;
|
||||
|
||||
if (!app.currentPath || app.currentPath === '') {
|
||||
if (app.userHomeFolderId) {
|
||||
url = `/api/folders/${app.userHomeFolderId}/listing?t=${timestamp}`;
|
||||
app.currentPath = app.userHomeFolderId;
|
||||
window.ui.updateBreadcrumb(app.userHomeFolderName || 'Home');
|
||||
console.log(`Loading user folder: ${app.userHomeFolderName} (${app.userHomeFolderId})`);
|
||||
} else {
|
||||
url = `/api/folders?t=${timestamp}`;
|
||||
console.warn("Emergency fallback to root folder - this should not normally happen");
|
||||
}
|
||||
} else {
|
||||
url = `/api/folders/${app.currentPath}/listing?t=${timestamp}`;
|
||||
console.log(`Loading subfolder content: ${app.currentPath}`);
|
||||
}
|
||||
|
||||
const token = localStorage.getItem('oxicloud_token');
|
||||
const headers = {
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
'Pragma': 'no-cache'
|
||||
};
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
const requestOptions = {
|
||||
headers,
|
||||
cache: 'no-store'
|
||||
};
|
||||
|
||||
if (forceRefresh) {
|
||||
url += `&force_refresh=true`;
|
||||
requestOptions.headers['X-Force-Refresh'] = 'true';
|
||||
console.log('Forcing complete refresh ignoring cache');
|
||||
}
|
||||
|
||||
console.log(`Loading listing from ${url}`);
|
||||
const response = await fetch(url, requestOptions);
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
console.warn("Auth error when loading files, showing empty list");
|
||||
elements.filesGrid.innerHTML = '<div class="empty-state"><p>Could not load files</p></div>';
|
||||
elements.filesListView.innerHTML = `
|
||||
<div class="list-header">
|
||||
<div class="list-header-checkbox"><input type="checkbox" id="select-all-checkbox" title="Select all"></div>
|
||||
<div>Name</div>
|
||||
<div>Type</div>
|
||||
<div>Size</div>
|
||||
<div>Modified</div>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server responded with status: ${response.status}`);
|
||||
}
|
||||
|
||||
const listing = await response.json();
|
||||
|
||||
if (window.multiSelect) window.multiSelect.clear();
|
||||
window.ui._items.clear();
|
||||
elements.filesGrid.innerHTML = '';
|
||||
const _t = (window.i18n && window.i18n.t) ? window.i18n.t : k => k.split('.').pop();
|
||||
elements.filesListView.innerHTML = `
|
||||
<div class="list-header">
|
||||
<div class="list-header-checkbox"><input type="checkbox" id="select-all-checkbox" title="Select all"></div>
|
||||
<div data-i18n="files.name">${_t('files.name')}</div>
|
||||
<div data-i18n="files.type">${_t('files.type')}</div>
|
||||
<div data-i18n="files.size">${_t('files.size')}</div>
|
||||
<div data-i18n="files.modified">${_t('files.modified')}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const selectAllCb = document.getElementById('select-all-checkbox');
|
||||
if (selectAllCb && window.multiSelect) {
|
||||
selectAllCb.addEventListener('change', () => window.multiSelect.toggleAll());
|
||||
}
|
||||
|
||||
const folderList = Array.isArray(listing.folders) ? listing.folders : [];
|
||||
const fileList = Array.isArray(listing.files) ? listing.files : [];
|
||||
|
||||
window.ui.renderFolders(folderList);
|
||||
window.ui.renderFiles(fileList);
|
||||
|
||||
console.log(`Loaded ${folderList.length} folders and ${fileList.length} files`);
|
||||
} catch (error) {
|
||||
console.error('Error loading folders:', error);
|
||||
window.ui.showNotification('Error', 'Could not load files and folders');
|
||||
} finally {
|
||||
window.isLoadingFiles = false;
|
||||
}
|
||||
}
|
||||
|
||||
window.loadFiles = loadFiles;
|
||||
@@ -0,0 +1,603 @@
|
||||
/**
|
||||
* OxiCloud - Main Application
|
||||
* This file contains the core functionality, initialization and state management
|
||||
*/
|
||||
|
||||
const app = window.app;
|
||||
const elements = window.appElements;
|
||||
|
||||
// Upload dropdown listener state (prevents accumulated listeners)
|
||||
let uploadDropdownDocumentClickHandler = null;
|
||||
let uploadDropdownBindingsController = null;
|
||||
let actionsBarDelegationBound = false;
|
||||
|
||||
const ACTIONS_BAR_TEMPLATES = {
|
||||
files: `
|
||||
<div class="action-buttons">
|
||||
<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>
|
||||
<span data-i18n="actions.upload">Upload</span>
|
||||
<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>
|
||||
<span data-i18n="actions.upload_files">Upload files</span>
|
||||
</button>
|
||||
<button class="upload-dropdown-item" id="upload-folder-btn">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
<span data-i18n="actions.upload_folder">Upload folder</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-secondary" id="new-folder-btn">
|
||||
<i class="fas fa-folder-plus" style="margin-right: 5px;"></i>
|
||||
<span data-i18n="actions.new_folder">New folder</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="view-toggle">
|
||||
<button class="toggle-btn active" id="grid-view-btn" title="Grid view">
|
||||
<i class="fas fa-th"></i>
|
||||
</button>
|
||||
<button class="toggle-btn" id="list-view-btn" title="List view">
|
||||
<i class="fas fa-list"></i>
|
||||
</button>
|
||||
</div>
|
||||
`,
|
||||
trash: `
|
||||
<div class="action-buttons">
|
||||
<button class="btn btn-danger" id="empty-trash-btn">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
<span data-i18n="trash.empty_trash">Empty trash</span>
|
||||
</button>
|
||||
</div>
|
||||
`,
|
||||
favorites: `
|
||||
<div class="action-buttons"></div>
|
||||
<div class="view-toggle">
|
||||
<button class="toggle-btn active" id="grid-view-btn" title="Grid view">
|
||||
<i class="fas fa-th"></i>
|
||||
</button>
|
||||
<button class="toggle-btn" id="list-view-btn" title="List view">
|
||||
<i class="fas fa-list"></i>
|
||||
</button>
|
||||
</div>
|
||||
`,
|
||||
recent: `
|
||||
<div class="action-buttons">
|
||||
<button class="btn btn-secondary" id="clear-recent-btn">
|
||||
<i class="fas fa-broom" style="margin-right: 5px;"></i>
|
||||
<span data-i18n="actions.clear_recent">Clear recent</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="view-toggle">
|
||||
<button class="toggle-btn active" id="grid-view-btn" title="Grid view">
|
||||
<i class="fas fa-th"></i>
|
||||
</button>
|
||||
<button class="toggle-btn" id="list-view-btn" title="List view">
|
||||
<i class="fas fa-list"></i>
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
|
||||
function setActionsBarMode(mode, force = false) {
|
||||
if (!elements.actionsBar) return;
|
||||
|
||||
if (mode === 'hidden') {
|
||||
elements.actionsBar.style.display = 'none';
|
||||
elements.actionsBar.dataset.mode = 'hidden';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!force && elements.actionsBar.dataset.mode === mode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const html = ACTIONS_BAR_TEMPLATES[mode];
|
||||
if (!html) return;
|
||||
|
||||
elements.actionsBar.innerHTML = html;
|
||||
elements.actionsBar.style.display = 'flex';
|
||||
elements.actionsBar.dataset.mode = mode;
|
||||
|
||||
// Refresh cached action elements after rebuild
|
||||
elements.uploadBtn = document.getElementById('upload-btn');
|
||||
elements.newFolderBtn = document.getElementById('new-folder-btn');
|
||||
elements.gridViewBtn = document.getElementById('grid-view-btn');
|
||||
elements.listViewBtn = document.getElementById('list-view-btn');
|
||||
|
||||
if (window.i18n && window.i18n.translateElement) {
|
||||
window.i18n.translateElement(elements.actionsBar);
|
||||
}
|
||||
|
||||
if (mode === 'files') {
|
||||
setupUploadDropdown();
|
||||
}
|
||||
}
|
||||
|
||||
function setupActionsBarDelegation() {
|
||||
if (actionsBarDelegationBound || !elements.actionsBar) return;
|
||||
actionsBarDelegationBound = true;
|
||||
|
||||
elements.actionsBar.addEventListener('click', async (e) => {
|
||||
const btn = e.target.closest('button');
|
||||
if (!btn) return;
|
||||
|
||||
switch (btn.id) {
|
||||
case 'upload-files-btn': {
|
||||
e.stopPropagation();
|
||||
const menu = document.getElementById('upload-dropdown-menu');
|
||||
if (menu) menu.classList.remove('show');
|
||||
if (elements.fileInput) elements.fileInput.click();
|
||||
break;
|
||||
}
|
||||
case 'upload-folder-btn': {
|
||||
e.stopPropagation();
|
||||
const menu = document.getElementById('upload-dropdown-menu');
|
||||
if (menu) menu.classList.remove('show');
|
||||
const folderInput = document.getElementById('folder-input');
|
||||
if (folderInput) folderInput.click();
|
||||
break;
|
||||
}
|
||||
case 'new-folder-btn': {
|
||||
const folderName = await window.Modal.promptNewFolder();
|
||||
if (folderName) {
|
||||
fileOps.createFolder(folderName);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'grid-view-btn':
|
||||
ui.switchToGridView();
|
||||
break;
|
||||
case 'list-view-btn':
|
||||
ui.switchToListView();
|
||||
break;
|
||||
case 'empty-trash-btn':
|
||||
if (await fileOps.emptyTrash()) {
|
||||
window.loadTrashItems();
|
||||
}
|
||||
break;
|
||||
case 'clear-recent-btn':
|
||||
if (window.recent) {
|
||||
window.recent.clearRecentFiles();
|
||||
window.recent.displayRecentFiles();
|
||||
window.ui.showNotification('Cleanup completed', 'Recent files history has been cleared');
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the application
|
||||
*/
|
||||
function initApp() {
|
||||
// Cache DOM elements
|
||||
cacheElements();
|
||||
|
||||
// Initialize file sharing module first
|
||||
if (window.fileSharing && window.fileSharing.init) {
|
||||
window.fileSharing.init();
|
||||
} else {
|
||||
console.warn('fileSharing module not fully initialized');
|
||||
}
|
||||
|
||||
// Then create menus and dialogs after modules have initialized
|
||||
setTimeout(() => {
|
||||
ui.initializeContextMenus();
|
||||
}, 100);
|
||||
|
||||
// Setup event listeners
|
||||
setupEventListeners();
|
||||
|
||||
// Ensure inline viewer is initialized
|
||||
if (!window.inlineViewer && typeof InlineViewer !== 'undefined') {
|
||||
try {
|
||||
window.inlineViewer = new InlineViewer();
|
||||
} catch (e) {
|
||||
console.error('Error initializing inline viewer:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize favorites module if available
|
||||
if (window.favorites && window.favorites.init) {
|
||||
console.log('Initializing favorites module');
|
||||
window.favorites.init();
|
||||
} else {
|
||||
console.warn('Favorites module not available or not initializable');
|
||||
}
|
||||
|
||||
// Initialize recent files module if available
|
||||
if (window.recent && window.recent.init) {
|
||||
console.log('Initializing recent files module');
|
||||
window.recent.init();
|
||||
} else {
|
||||
console.warn('Recent files module not available or not initializable');
|
||||
}
|
||||
|
||||
// Initialize multi-select / batch actions
|
||||
if (window.multiSelect && window.multiSelect.init) {
|
||||
console.log('Initializing multi-select module');
|
||||
window.multiSelect.init();
|
||||
}
|
||||
|
||||
// Wait for translations to load before checking authentication
|
||||
if (window.i18n && window.i18n.isLoaded && window.i18n.isLoaded()) {
|
||||
// Translations already loaded, proceed with authentication
|
||||
window.checkAuthentication();
|
||||
} else {
|
||||
// Wait for translations to be loaded before proceeding
|
||||
console.log('Waiting for translations to load...');
|
||||
window.addEventListener('translationsLoaded', () => {
|
||||
console.log('Translations loaded, proceeding with authentication');
|
||||
window.checkAuthentication();
|
||||
});
|
||||
|
||||
// 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');
|
||||
window.checkAuthentication();
|
||||
}
|
||||
}, 3000); // 3 second timeout
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache DOM elements for faster access
|
||||
*/
|
||||
function cacheElements() {
|
||||
elements.uploadBtn = document.getElementById('upload-btn');
|
||||
elements.dropzone = document.getElementById('dropzone');
|
||||
elements.fileInput = document.getElementById('file-input');
|
||||
elements.filesGrid = document.getElementById('files-grid');
|
||||
elements.filesListView = document.getElementById('files-list-view');
|
||||
elements.newFolderBtn = document.getElementById('new-folder-btn');
|
||||
elements.gridViewBtn = document.getElementById('grid-view-btn');
|
||||
elements.listViewBtn = document.getElementById('list-view-btn');
|
||||
elements.breadcrumb = document.querySelector('.breadcrumb');
|
||||
elements.pageTitle = document.querySelector('.page-title');
|
||||
elements.actionsBar = document.querySelector('.actions-bar');
|
||||
elements.navItems = document.querySelectorAll('.nav-item');
|
||||
elements.trashBtn = document.querySelector('.nav-item:nth-child(5)'); // The trash nav item
|
||||
elements.searchInput = document.querySelector('.search-container input');
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the upload dropdown button and menu
|
||||
* Handles opening/closing the dropdown and triggering file/folder inputs
|
||||
*/
|
||||
function setupUploadDropdown() {
|
||||
const uploadBtn = document.getElementById('upload-btn');
|
||||
const menu = document.getElementById('upload-dropdown-menu');
|
||||
|
||||
if (!uploadBtn || !menu) return;
|
||||
|
||||
// Abort any previous local bindings (safe across repeated/rebuilt UI)
|
||||
if (uploadDropdownBindingsController) {
|
||||
uploadDropdownBindingsController.abort();
|
||||
}
|
||||
uploadDropdownBindingsController = new AbortController();
|
||||
const signal = uploadDropdownBindingsController.signal;
|
||||
|
||||
// Toggle dropdown on button click
|
||||
uploadBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const isOpen = menu.classList.contains('show');
|
||||
// Close any other open dropdowns
|
||||
document.querySelectorAll('.upload-dropdown-menu.show').forEach(m => m.classList.remove('show'));
|
||||
if (!isOpen) {
|
||||
menu.classList.add('show');
|
||||
}
|
||||
}, { signal });
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
// remove+add stable handler: guarantees exactly one global listener
|
||||
if (uploadDropdownDocumentClickHandler) {
|
||||
document.removeEventListener('click', uploadDropdownDocumentClickHandler);
|
||||
}
|
||||
uploadDropdownDocumentClickHandler = (e) => {
|
||||
if (e.target.closest('#upload-dropdown')) return;
|
||||
document.querySelectorAll('.upload-dropdown-menu.show').forEach(m => m.classList.remove('show'));
|
||||
};
|
||||
document.addEventListener('click', uploadDropdownDocumentClickHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup event listeners for main UI elements
|
||||
*/
|
||||
function setupEventListeners() {
|
||||
// Set up drag and drop
|
||||
ui.setupDragAndDrop();
|
||||
|
||||
// Debounce timer for live search
|
||||
let searchDebounceTimer = null;
|
||||
const SEARCH_DEBOUNCE_MS = 300;
|
||||
const SEARCH_MIN_CHARS = 3;
|
||||
|
||||
// Search input — Enter key
|
||||
elements.searchInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
// Cancel any pending debounce
|
||||
if (searchDebounceTimer) clearTimeout(searchDebounceTimer);
|
||||
const query = elements.searchInput.value.trim();
|
||||
if (query) {
|
||||
window.performSearch(query);
|
||||
} else if (app.isSearchMode) {
|
||||
// If search is empty and we're in search mode, return to normal view
|
||||
app.isSearchMode = false;
|
||||
app.currentPath = '';
|
||||
ui.updateBreadcrumb('');
|
||||
window.loadFiles();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Search input — Live search (debounced, after 3+ chars)
|
||||
elements.searchInput.addEventListener('input', () => {
|
||||
if (searchDebounceTimer) clearTimeout(searchDebounceTimer);
|
||||
const query = elements.searchInput.value.trim();
|
||||
|
||||
if (query.length >= SEARCH_MIN_CHARS) {
|
||||
searchDebounceTimer = setTimeout(() => {
|
||||
window.performSearch(query);
|
||||
}, SEARCH_DEBOUNCE_MS);
|
||||
} else if (query.length === 0 && app.isSearchMode) {
|
||||
// User cleared the search input — return to normal view
|
||||
searchDebounceTimer = setTimeout(() => {
|
||||
app.isSearchMode = false;
|
||||
app.currentPath = '';
|
||||
ui.updateBreadcrumb('');
|
||||
window.loadFiles();
|
||||
}, SEARCH_DEBOUNCE_MS);
|
||||
}
|
||||
});
|
||||
|
||||
// Search button
|
||||
document.getElementById('search-button').addEventListener('click', () => {
|
||||
if (searchDebounceTimer) clearTimeout(searchDebounceTimer);
|
||||
const query = elements.searchInput.value.trim();
|
||||
if (query) {
|
||||
window.performSearch(query);
|
||||
}
|
||||
});
|
||||
|
||||
// Upload dropdown
|
||||
setupUploadDropdown();
|
||||
setupActionsBarDelegation();
|
||||
if (elements.actionsBar) {
|
||||
elements.actionsBar.dataset.mode = 'files';
|
||||
}
|
||||
|
||||
// File input
|
||||
elements.fileInput.addEventListener('change', (e) => {
|
||||
if (e.target.files.length > 0) {
|
||||
fileOps.uploadFiles(e.target.files);
|
||||
e.target.value = ''; // reset so same file can be re-uploaded
|
||||
}
|
||||
});
|
||||
|
||||
// Folder input
|
||||
const folderInput = document.getElementById('folder-input');
|
||||
if (folderInput) {
|
||||
folderInput.addEventListener('change', (e) => {
|
||||
if (e.target.files.length > 0) {
|
||||
fileOps.uploadFolderFiles(e.target.files);
|
||||
e.target.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Sidebar navigation
|
||||
elements.navItems.forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
// Remove active class from all nav items
|
||||
elements.navItems.forEach(navItem => navItem.classList.remove('active'));
|
||||
|
||||
// Add active class to clicked item
|
||||
item.classList.add('active');
|
||||
|
||||
// Check if this is the shared item
|
||||
if (item.querySelector('span').getAttribute('data-i18n') === 'nav.shared') {
|
||||
// Switch to shared view
|
||||
switchToSharedView();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is the favorites item
|
||||
if (item.querySelector('span').getAttribute('data-i18n') === 'nav.favorites') {
|
||||
// Switch to favorites view
|
||||
switchToFavoritesView();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is the recent files item
|
||||
if (item.querySelector('span').getAttribute('data-i18n') === 'nav.recent') {
|
||||
// Switch to recent files view
|
||||
switchToRecentFilesView();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is the trash item
|
||||
if (item === elements.trashBtn) {
|
||||
// Hide shared view if active
|
||||
if (app.isSharedView) {
|
||||
// Hide shared view
|
||||
if (window.sharedView) {
|
||||
window.sharedView.hide();
|
||||
}
|
||||
|
||||
// Reset shared view flag
|
||||
app.isSharedView = false;
|
||||
|
||||
// Clean up shared containers if they exist
|
||||
const sharedContainer = document.getElementById('shared-container');
|
||||
if (sharedContainer) {
|
||||
sharedContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Show trash view
|
||||
app.isTrashView = true;
|
||||
app.currentSection = 'trash';
|
||||
|
||||
// Show files containers (to be filled with trash)
|
||||
const filesGrid = document.getElementById('files-grid');
|
||||
const filesListView = document.getElementById('files-list-view');
|
||||
if (filesGrid) filesGrid.style.display = app.currentView === 'grid' ? 'grid' : 'none';
|
||||
if (filesListView) filesListView.style.display = app.currentView === 'list' ? 'block' : 'none';
|
||||
|
||||
// Update UI
|
||||
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.trash') : 'Trash';
|
||||
elements.pageTitle.setAttribute('data-i18n', 'nav.trash');
|
||||
setActionsBarMode('trash');
|
||||
|
||||
// Load trash items
|
||||
window.loadTrashItems();
|
||||
} else {
|
||||
// Check if we need to reset shared view
|
||||
if (app.isSharedView) {
|
||||
// Hide shared view
|
||||
if (window.sharedView) {
|
||||
window.sharedView.hide();
|
||||
}
|
||||
|
||||
// Reset shared view flag
|
||||
app.isSharedView = false;
|
||||
|
||||
// Clean up shared containers if they exist
|
||||
const sharedContainer = document.getElementById('shared-container');
|
||||
if (sharedContainer) {
|
||||
sharedContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Show regular files view
|
||||
app.isTrashView = false;
|
||||
app.currentSection = 'files';
|
||||
|
||||
// Reset UI
|
||||
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.files') : 'Files';
|
||||
setActionsBarMode('files');
|
||||
|
||||
// Show files containers
|
||||
const filesGrid = document.getElementById('files-grid');
|
||||
const filesListView = document.getElementById('files-list-view');
|
||||
if (filesGrid) filesGrid.style.display = app.currentView === 'grid' ? 'grid' : 'none';
|
||||
if (filesListView) filesListView.style.display = app.currentView === 'list' ? 'block' : 'none';
|
||||
|
||||
// Load regular files
|
||||
app.currentPath = '';
|
||||
ui.updateBreadcrumb('');
|
||||
window.loadFiles();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Load saved view preference
|
||||
const savedView = localStorage.getItem('oxicloud-view');
|
||||
if (savedView === 'list') {
|
||||
ui.switchToListView();
|
||||
}
|
||||
|
||||
// User menu
|
||||
window.setupUserMenu();
|
||||
|
||||
// Global events to close context menus and deselect cards
|
||||
document.addEventListener('click', (e) => {
|
||||
const folderMenu = document.getElementById('folder-context-menu');
|
||||
const fileMenu = document.getElementById('file-context-menu');
|
||||
|
||||
if (folderMenu && folderMenu.style.display === 'block' &&
|
||||
!folderMenu.contains(e.target)) {
|
||||
ui.closeContextMenu();
|
||||
}
|
||||
|
||||
if (fileMenu && fileMenu.style.display === 'block' &&
|
||||
!fileMenu.contains(e.target)) {
|
||||
ui.closeFileContextMenu();
|
||||
}
|
||||
|
||||
// Deselect all cards when clicking empty area (not on a card, menu, or modal)
|
||||
// Note: multiSelect._hookGlobalDeselect() handles clearing the internal
|
||||
// selection state; this handler only covers the legacy CSS class removal.
|
||||
if (!e.target.closest('.file-card') && !e.target.closest('.file-item') && !e.target.closest('.context-menu') && !e.target.closest('.about-modal') && !e.target.closest('.batch-action-bar') && !e.target.closest('.list-header.selection-mode')) {
|
||||
document.querySelectorAll('.file-card.selected').forEach(c => c.classList.remove('selected'));
|
||||
document.querySelectorAll('.file-item.selected').forEach(c => c.classList.remove('selected'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Expose needed functions to global scope
|
||||
window.setActionsBarMode = setActionsBarMode;
|
||||
|
||||
// Set up global selectFolder function for navigation
|
||||
window.selectFolder = (id, name) => {
|
||||
app.currentPath = id;
|
||||
ui.updateBreadcrumb(name);
|
||||
window.loadFiles();
|
||||
};
|
||||
|
||||
// View-switching actions moved to app/navigation.js
|
||||
|
||||
/**
|
||||
* Update the storage usage display with the user's actual storage usage
|
||||
* @param {Object} userData - The user data object
|
||||
*/
|
||||
function updateStorageUsageDisplay(userData) {
|
||||
// Default values
|
||||
const DEFAULT_QUOTA = 10 * 1024 * 1024 * 1024; // 10 GB
|
||||
let usedBytes = 0;
|
||||
let quotaBytes = DEFAULT_QUOTA;
|
||||
let usagePercentage = 0;
|
||||
|
||||
// Get values from user data if available
|
||||
if (userData) {
|
||||
usedBytes = userData.storage_used_bytes || 0;
|
||||
quotaBytes = userData.storage_quota_bytes || DEFAULT_QUOTA;
|
||||
|
||||
// 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) {
|
||||
// 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})`;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Updated storage display: ${usagePercentage}% (${usedFormatted} / ${quotaFormatted})`);
|
||||
}
|
||||
|
||||
window.updateStorageUsageDisplay = updateStorageUsageDisplay;
|
||||
|
||||
// Initialize app when DOM is ready
|
||||
window.initApp = initApp;
|
||||
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* OxiCloud - View navigation actions
|
||||
* Extracted from main.js to keep navigation concerns isolated.
|
||||
*/
|
||||
|
||||
function switchToSharedView() {
|
||||
window.app.isTrashView = false;
|
||||
window.app.isSharedView = true;
|
||||
window.app.currentSection = 'shared';
|
||||
|
||||
window.appElements.navItems.forEach(navItem => navItem.classList.remove('active'));
|
||||
|
||||
const sharedNavItem = document.querySelector('.nav-item:nth-child(2)');
|
||||
if (sharedNavItem) {
|
||||
sharedNavItem.classList.add('active');
|
||||
}
|
||||
|
||||
window.appElements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.shared') : 'Shared';
|
||||
window.appElements.pageTitle.setAttribute('data-i18n', 'nav.shared');
|
||||
|
||||
window.ui.updateBreadcrumb('');
|
||||
|
||||
const breadcrumb = document.querySelector('.breadcrumb');
|
||||
if (breadcrumb) breadcrumb.style.display = 'none';
|
||||
|
||||
if (window.appElements.actionsBar) {
|
||||
window.appElements.actionsBar.style.display = 'none';
|
||||
}
|
||||
|
||||
const filesGrid = document.getElementById('files-grid');
|
||||
const filesListView = document.getElementById('files-list-view');
|
||||
if (filesGrid) filesGrid.style.display = 'none';
|
||||
if (filesListView) filesListView.style.display = 'none';
|
||||
|
||||
if (window.sharedView) {
|
||||
window.sharedView.init();
|
||||
window.sharedView.show();
|
||||
}
|
||||
}
|
||||
|
||||
function switchToFilesView() {
|
||||
window.app.isTrashView = false;
|
||||
window.app.isSharedView = false;
|
||||
window.app.isFavoritesView = false;
|
||||
window.app.isRecentView = false;
|
||||
window.app.currentSection = 'files';
|
||||
|
||||
window.appElements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.files') : 'Files';
|
||||
window.appElements.pageTitle.setAttribute('data-i18n', 'nav.files');
|
||||
|
||||
const breadcrumb = document.querySelector('.breadcrumb');
|
||||
if (breadcrumb) breadcrumb.style.display = '';
|
||||
|
||||
window.appElements.navItems.forEach(navItem => navItem.classList.remove('active'));
|
||||
|
||||
const filesNavItem = document.querySelector('.nav-item:first-child');
|
||||
if (filesNavItem) {
|
||||
filesNavItem.classList.add('active');
|
||||
}
|
||||
|
||||
window.setActionsBarMode('files');
|
||||
|
||||
if (window.sharedView) {
|
||||
window.sharedView.hide();
|
||||
}
|
||||
|
||||
const filesGrid = document.getElementById('files-grid');
|
||||
if (filesGrid) {
|
||||
filesGrid.style.display = window.app.currentView === 'grid' ? 'grid' : 'none';
|
||||
}
|
||||
|
||||
const filesListView = document.getElementById('files-list-view');
|
||||
if (filesListView) {
|
||||
filesListView.style.display = window.app.currentView === 'list' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
if (window.app.userHomeFolderId) {
|
||||
window.app.currentPath = window.app.userHomeFolderId;
|
||||
window.ui.updateBreadcrumb(window.app.userHomeFolderName || 'Home');
|
||||
} else {
|
||||
window.app.currentPath = '';
|
||||
}
|
||||
window.loadFiles();
|
||||
}
|
||||
|
||||
function switchToFavoritesView() {
|
||||
window.app.isTrashView = false;
|
||||
window.app.isSharedView = false;
|
||||
|
||||
window.app.isFavoritesView = true;
|
||||
window.app.currentSection = 'favorites';
|
||||
|
||||
window.appElements.navItems.forEach(navItem => navItem.classList.remove('active'));
|
||||
|
||||
const favoritesNavItem = document.querySelector('.nav-item:nth-child(4)');
|
||||
if (favoritesNavItem) {
|
||||
favoritesNavItem.classList.add('active');
|
||||
}
|
||||
|
||||
window.appElements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.favorites') : 'Favorites';
|
||||
window.appElements.pageTitle.setAttribute('data-i18n', 'nav.favorites');
|
||||
|
||||
window.ui.updateBreadcrumb('');
|
||||
|
||||
if (window.sharedView) {
|
||||
window.sharedView.hide();
|
||||
}
|
||||
|
||||
window.setActionsBarMode('favorites');
|
||||
|
||||
const filesGrid = document.getElementById('files-grid');
|
||||
const filesListView = document.getElementById('files-list-view');
|
||||
|
||||
if (filesGrid) {
|
||||
filesGrid.style.display = window.app.currentView === 'grid' ? 'grid' : 'none';
|
||||
}
|
||||
|
||||
if (filesListView) {
|
||||
filesListView.style.display = window.app.currentView === 'list' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
if (window.favorites) {
|
||||
window.favorites.displayFavorites();
|
||||
} else {
|
||||
console.error('Favorites module not loaded or initialized');
|
||||
|
||||
const filesGridError = document.getElementById('files-grid');
|
||||
if (filesGridError) {
|
||||
filesGridError.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-exclamation-circle" style="font-size: 48px; color: #f44336; margin-bottom: 16px;"></i>
|
||||
<p>Error loading the favorites module</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function switchToRecentFilesView() {
|
||||
window.app.isTrashView = false;
|
||||
window.app.isSharedView = false;
|
||||
window.app.isFavoritesView = false;
|
||||
|
||||
window.app.isRecentView = true;
|
||||
window.app.currentSection = 'recent';
|
||||
|
||||
window.appElements.navItems.forEach(navItem => navItem.classList.remove('active'));
|
||||
|
||||
const recentNavItem = document.querySelector('.nav-item:nth-child(3)');
|
||||
if (recentNavItem) {
|
||||
recentNavItem.classList.add('active');
|
||||
}
|
||||
|
||||
window.appElements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.recent') : 'Recent';
|
||||
window.appElements.pageTitle.setAttribute('data-i18n', 'nav.recent');
|
||||
|
||||
window.ui.updateBreadcrumb('');
|
||||
|
||||
if (window.sharedView) {
|
||||
window.sharedView.hide();
|
||||
}
|
||||
|
||||
window.setActionsBarMode('recent');
|
||||
|
||||
const filesGrid = document.getElementById('files-grid');
|
||||
const filesListView = document.getElementById('files-list-view');
|
||||
|
||||
if (filesGrid) {
|
||||
filesGrid.style.display = window.app.currentView === 'grid' ? 'grid' : 'none';
|
||||
}
|
||||
|
||||
if (filesListView) {
|
||||
filesListView.style.display = window.app.currentView === 'list' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
if (window.recent) {
|
||||
window.recent.displayRecentFiles();
|
||||
} else {
|
||||
console.error('Recent files module not loaded or initialized');
|
||||
|
||||
const filesGridError = document.getElementById('files-grid');
|
||||
if (filesGridError) {
|
||||
filesGridError.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-exclamation-circle" style="font-size: 48px; color: #f44336; margin-bottom: 16px;"></i>
|
||||
<p>Error loading the recent files module</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.switchToFilesView = switchToFilesView;
|
||||
window.switchToSharedView = switchToSharedView;
|
||||
window.switchToFavoritesView = switchToFavoritesView;
|
||||
window.switchToRecentFilesView = switchToRecentFilesView;
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Search view orchestration logic
|
||||
*/
|
||||
|
||||
async function performSearch(query, sortBy) {
|
||||
const app = window.app;
|
||||
|
||||
console.log(`Performing search for: "${query}" (sort: ${sortBy || 'relevance'})`);
|
||||
|
||||
try {
|
||||
app.isSearchMode = true;
|
||||
window.ui.updateBreadcrumb(`Search: "${query}"`);
|
||||
|
||||
const filesGrid = document.getElementById('files-grid');
|
||||
if (filesGrid) {
|
||||
filesGrid.innerHTML = `
|
||||
<div class="search-results-header">
|
||||
<h3><i class="fas fa-spinner fa-spin" style="margin-right:8px;"></i> Searching for "${query}"...</h3>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const options = {
|
||||
recursive: true,
|
||||
limit: 100,
|
||||
sort_by: sortBy || 'relevance'
|
||||
};
|
||||
|
||||
if (!app.isTrashView) {
|
||||
options.folder_id = app.currentPath;
|
||||
|
||||
if (!options.folder_id || options.folder_id === '') {
|
||||
await window.resolveHomeFolder();
|
||||
options.folder_id = app.currentPath;
|
||||
}
|
||||
}
|
||||
|
||||
const searchResults = await window.search.searchFiles(query, options);
|
||||
window.search.displaySearchResults(searchResults);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Search error:', error);
|
||||
window.ui.showNotification('Error', 'Error performing search');
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
window.performSearch = performSearch;
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* OxiCloud - App state container
|
||||
* Centralized mutable state for app and cached DOM references.
|
||||
*/
|
||||
|
||||
window.app = {
|
||||
currentView: 'grid',
|
||||
currentPath: '',
|
||||
currentFolder: null,
|
||||
contextMenuTargetFolder: null,
|
||||
contextMenuTargetFile: null,
|
||||
selectedTargetFolderId: '',
|
||||
moveDialogMode: 'file',
|
||||
isTrashView: false,
|
||||
isSharedView: false,
|
||||
isFavoritesView: false,
|
||||
isRecentView: false,
|
||||
currentSection: 'files',
|
||||
isSearchMode: false,
|
||||
shareDialogItem: null,
|
||||
shareDialogItemType: null,
|
||||
notificationShareUrl: null
|
||||
};
|
||||
|
||||
window.appElements = {
|
||||
};
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Trash view loading and rendering logic
|
||||
*/
|
||||
|
||||
async function loadTrashItems() {
|
||||
const elements = window.appElements;
|
||||
|
||||
try {
|
||||
if (window.multiSelect) window.multiSelect.clear();
|
||||
elements.filesGrid.innerHTML = '';
|
||||
const _tt = (window.i18n && window.i18n.t) ? window.i18n.t : k => k.split('.').pop();
|
||||
elements.filesListView.innerHTML = `
|
||||
<div class="list-header trash-header">
|
||||
<div data-i18n="files.name">${_tt('files.name')}</div>
|
||||
<div data-i18n="files.type">${_tt('files.type')}</div>
|
||||
<div data-i18n="trash.original_location">${_tt('trash.original_location')}</div>
|
||||
<div data-i18n="trash.deleted_date">${_tt('trash.deleted_date')}</div>
|
||||
<div data-i18n="trash.actions">${_tt('trash.actions')}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
window.ui.updateBreadcrumb('');
|
||||
|
||||
const trashItems = await window.fileOps.getTrashItems();
|
||||
|
||||
if (trashItems.length === 0) {
|
||||
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>
|
||||
<p>${window.i18n ? window.i18n.t('trash.empty_state') : 'The trash is empty'}</p>
|
||||
`;
|
||||
elements.filesGrid.appendChild(emptyState);
|
||||
return;
|
||||
}
|
||||
|
||||
trashItems.forEach(item => {
|
||||
addTrashItemToView(item);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading trash items:', error);
|
||||
window.ui.showNotification('Error', 'Error loading trash items');
|
||||
}
|
||||
}
|
||||
|
||||
function addTrashItemToView(item) {
|
||||
const elements = window.appElements;
|
||||
const isFile = item.item_type === 'file';
|
||||
|
||||
const formattedDate = window.formatDateTime(item.trashed_at);
|
||||
|
||||
let iconClass;
|
||||
let typeLabel;
|
||||
let iconSpecialClass = '';
|
||||
if (!isFile) {
|
||||
iconClass = item.icon_class || 'fas fa-folder';
|
||||
typeLabel = window.i18n ? window.i18n.t('files.file_types.folder') : 'Folder';
|
||||
} else {
|
||||
iconClass = item.icon_class || (window.ui && window.ui.getIconClass
|
||||
? window.ui.getIconClass(item.name)
|
||||
: 'fas fa-file');
|
||||
iconSpecialClass = (window.ui && window.ui.getIconSpecialClass)
|
||||
? window.ui.getIconSpecialClass(item.name)
|
||||
: '';
|
||||
const cat = item.category || '';
|
||||
typeLabel = cat
|
||||
? (window.i18n ? window.i18n.t(`files.file_types.${cat.toLowerCase()}`) || cat : cat)
|
||||
: (window.i18n ? window.i18n.t('files.file_types.document') : 'Document');
|
||||
}
|
||||
|
||||
const isFolder = !isFile;
|
||||
const iconWrapClass = isFolder
|
||||
? 'file-icon folder-icon'
|
||||
: `file-icon ${iconSpecialClass}`.trim();
|
||||
|
||||
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="${iconWrapClass}">
|
||||
<i class="${iconClass}"></i>
|
||||
</div>
|
||||
<div class="file-name">${escapeHtml(item.name)}</div>
|
||||
<div class="file-info">${escapeHtml(typeLabel)} - ${escapeHtml(formattedDate)}</div>
|
||||
<div class="trash-actions">
|
||||
<button class="btn-restore" title="${window.i18n ? window.i18n.t('trash.restore') : 'Restore'}">
|
||||
<i class="fas fa-undo"></i>
|
||||
</button>
|
||||
<button class="btn-delete" title="${window.i18n ? window.i18n.t('trash.delete_permanently') : 'Delete permanently'}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
gridElement.querySelector('.btn-restore').addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
if (await window.fileOps.restoreFromTrash(item.id)) {
|
||||
window.loadTrashItems();
|
||||
}
|
||||
});
|
||||
|
||||
gridElement.querySelector('.btn-delete').addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
if (await window.fileOps.deletePermanently(item.id)) {
|
||||
window.loadTrashItems();
|
||||
}
|
||||
});
|
||||
|
||||
elements.filesGrid.appendChild(gridElement);
|
||||
|
||||
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="${iconWrapClass}">
|
||||
<i class="${iconClass}"></i>
|
||||
</div>
|
||||
<span>${escapeHtml(item.name)}</span>
|
||||
</div>
|
||||
<div class="type-cell">${escapeHtml(typeLabel)}</div>
|
||||
<div class="path-cell">${escapeHtml(item.original_path || '--')}</div>
|
||||
<div class="date-cell">${escapeHtml(formattedDate)}</div>
|
||||
<div class="actions-cell">
|
||||
<button class="btn-restore" title="${window.i18n ? window.i18n.t('trash.restore') : 'Restore'}">
|
||||
<i class="fas fa-undo"></i>
|
||||
</button>
|
||||
<button class="btn-delete" title="${window.i18n ? window.i18n.t('trash.delete_permanently') : 'Delete permanently'}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
listElement.querySelector('.btn-restore').addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
if (await window.fileOps.restoreFromTrash(item.id)) {
|
||||
window.loadTrashItems();
|
||||
}
|
||||
});
|
||||
|
||||
listElement.querySelector('.btn-delete').addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
if (await window.fileOps.deletePermanently(item.id)) {
|
||||
window.loadTrashItems();
|
||||
}
|
||||
});
|
||||
|
||||
elements.filesListView.appendChild(listElement);
|
||||
}
|
||||
|
||||
window.loadTrashItems = loadTrashItems;
|
||||
+1444
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* OxiCloud - UI file type helpers
|
||||
* Isolated icon and preview classification helpers used by ui.js.
|
||||
*/
|
||||
|
||||
const uiFileTypes = {
|
||||
isViewableFile(file) {
|
||||
if (!file || !file.mime_type) return false;
|
||||
if (file.mime_type.startsWith('image/')) return true;
|
||||
if (file.mime_type === 'application/pdf') return true;
|
||||
return window.isTextViewable ? window.isTextViewable(file.mime_type) : false;
|
||||
},
|
||||
|
||||
getIconClass(fileName) {
|
||||
if (!fileName) return 'fas fa-file';
|
||||
const ext = (fileName.split('.').pop() || '').toLowerCase();
|
||||
const map = {
|
||||
pdf:'fas fa-file-pdf', doc:'fas fa-file-word', docx:'fas fa-file-word',
|
||||
txt:'fas fa-file-alt', rtf:'fas fa-file-alt', odt:'fas fa-file-alt',
|
||||
xls:'fas fa-file-excel', xlsx:'fas fa-file-excel', csv:'fas fa-file-excel', ods:'fas fa-file-excel',
|
||||
ppt:'fas fa-file-powerpoint', pptx:'fas fa-file-powerpoint', odp:'fas fa-file-powerpoint',
|
||||
jpg:'fas fa-file-image', jpeg:'fas fa-file-image', png:'fas fa-file-image',
|
||||
gif:'fas fa-file-image', svg:'fas fa-file-image', webp:'fas fa-file-image',
|
||||
bmp:'fas fa-file-image', ico:'fas fa-file-image',
|
||||
mp4:'fas fa-file-video', avi:'fas fa-file-video', mov:'fas fa-file-video',
|
||||
mkv:'fas fa-file-video', webm:'fas fa-file-video', flv:'fas fa-file-video',
|
||||
mp3:'fas fa-file-audio', wav:'fas fa-file-audio', ogg:'fas fa-file-audio',
|
||||
flac:'fas fa-file-audio', aac:'fas fa-file-audio', m4a:'fas fa-file-audio',
|
||||
zip:'fas fa-file-archive', rar:'fas fa-file-archive', '7z':'fas fa-file-archive',
|
||||
tar:'fas fa-file-archive', gz:'fas fa-file-archive',
|
||||
js:'fas fa-file-code', ts:'fas fa-file-code', py:'fas fa-file-code',
|
||||
rs:'fas fa-file-code', java:'fas fa-file-code', html:'fas fa-file-code',
|
||||
css:'fas fa-file-code', json:'fas fa-file-code', xml:'fas fa-file-code',
|
||||
sh:'fas fa-terminal', bash:'fas fa-terminal', bat:'fas fa-terminal',
|
||||
md:'fas fa-file-alt',
|
||||
};
|
||||
return map[ext] || 'fas fa-file';
|
||||
},
|
||||
|
||||
getIconSpecialClass(fileName) {
|
||||
if (!fileName) return '';
|
||||
const ext = (fileName.split('.').pop() || '').toLowerCase();
|
||||
const map = {
|
||||
pdf:'pdf-icon',
|
||||
doc:'doc-icon', docx:'doc-icon', odt:'doc-icon', rtf:'doc-icon',
|
||||
xls:'spreadsheet-icon', xlsx:'spreadsheet-icon', ods:'spreadsheet-icon', csv:'spreadsheet-icon',
|
||||
ppt:'presentation-icon', pptx:'presentation-icon', odp:'presentation-icon', key:'presentation-icon',
|
||||
jpg:'image-icon', jpeg:'image-icon', png:'image-icon', gif:'image-icon',
|
||||
svg:'image-icon', webp:'image-icon', bmp:'image-icon', ico:'image-icon',
|
||||
heic:'image-icon', heif:'image-icon', avif:'image-icon', tiff:'image-icon',
|
||||
mp4:'video-icon', avi:'video-icon', mkv:'video-icon', mov:'video-icon',
|
||||
wmv:'video-icon', flv:'video-icon', webm:'video-icon', m4v:'video-icon',
|
||||
mp3:'audio-icon', wav:'audio-icon', ogg:'audio-icon', flac:'audio-icon',
|
||||
aac:'audio-icon', wma:'audio-icon', m4a:'audio-icon', opus:'audio-icon',
|
||||
zip:'archive-icon', rar:'archive-icon', '7z':'archive-icon',
|
||||
tar:'archive-icon', gz:'archive-icon', bz2:'archive-icon', xz:'archive-icon',
|
||||
exe:'installer-icon', msi:'installer-icon', dmg:'installer-icon',
|
||||
deb:'installer-icon', rpm:'installer-icon', appimage:'installer-icon',
|
||||
py:'code-icon py-icon', rs:'code-icon rust-icon', go:'code-icon go-icon',
|
||||
js:'code-icon js-icon', jsx:'code-icon js-icon', mjs:'code-icon js-icon',
|
||||
ts:'code-icon ts-icon', tsx:'code-icon ts-icon',
|
||||
java:'code-icon java-icon', c:'code-icon c-icon', cpp:'code-icon c-icon',
|
||||
cs:'code-icon cs-icon', rb:'code-icon ruby-icon', php:'code-icon php-icon',
|
||||
swift:'code-icon swift-icon',
|
||||
html:'code-icon html-icon', htm:'code-icon html-icon',
|
||||
css:'code-icon css-icon', scss:'code-icon css-icon',
|
||||
json:'code-icon json-icon', xml:'code-icon html-icon',
|
||||
yaml:'code-icon config-icon', yml:'code-icon config-icon',
|
||||
toml:'code-icon config-icon', ini:'code-icon config-icon',
|
||||
sql:'code-icon sql-icon', vue:'code-icon js-icon', svelte:'code-icon js-icon',
|
||||
sh:'script-icon', bash:'script-icon', zsh:'script-icon', bat:'script-icon',
|
||||
md:'code-icon md-icon', txt:'doc-icon',
|
||||
};
|
||||
return map[ext] || '';
|
||||
}
|
||||
};
|
||||
|
||||
window.uiFileTypes = uiFileTypes;
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* OxiCloud - UI notifications adapter
|
||||
* Isolates notification rendering policy from ui.js.
|
||||
*/
|
||||
|
||||
const uiNotifications = {
|
||||
show(title, message) {
|
||||
if (window.notifications && typeof window.notifications.addNotification === 'function') {
|
||||
const normalizedTitle = String(title || '').toLowerCase();
|
||||
let icon = 'fa-info-circle';
|
||||
let iconClass = 'upload';
|
||||
|
||||
if (normalizedTitle.includes('error') || normalizedTitle.includes('failed') || normalizedTitle.includes('fail')) {
|
||||
icon = 'fa-exclamation-circle';
|
||||
iconClass = 'error';
|
||||
} else if (normalizedTitle.includes('favorite') || normalizedTitle.includes('favorit') || normalizedTitle.includes('fav')) {
|
||||
icon = 'fa-star';
|
||||
iconClass = 'success';
|
||||
} else if (normalizedTitle.includes('delete') || normalizedTitle.includes('removed') || normalizedTitle.includes('trash') || normalizedTitle.includes('rename') || normalizedTitle.includes('complete')) {
|
||||
icon = 'fa-check-circle';
|
||||
iconClass = 'success';
|
||||
}
|
||||
|
||||
window.notifications.addNotification({
|
||||
icon,
|
||||
iconClass,
|
||||
title: title || '',
|
||||
text: message || ''
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let notification = document.querySelector('.notification');
|
||||
if (!notification) {
|
||||
notification = document.createElement('div');
|
||||
notification.className = 'notification';
|
||||
notification.innerHTML = `
|
||||
<div class="notification-title">${title}</div>
|
||||
<div class="notification-message">${message}</div>
|
||||
`;
|
||||
document.body.appendChild(notification);
|
||||
} else {
|
||||
notification.querySelector('.notification-title').textContent = title;
|
||||
notification.querySelector('.notification-message').textContent = message;
|
||||
}
|
||||
|
||||
notification.style.display = 'block';
|
||||
|
||||
setTimeout(() => {
|
||||
notification.style.display = 'none';
|
||||
}, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
window.uiNotifications = uiNotifications;
|
||||
@@ -0,0 +1,243 @@
|
||||
/**
|
||||
* User menu, profile modal and logout logic
|
||||
*/
|
||||
|
||||
function setupUserMenu() {
|
||||
const wrapper = document.getElementById('user-menu-wrapper');
|
||||
const avatarBtn = document.getElementById('user-avatar-btn');
|
||||
const menu = document.getElementById('user-menu');
|
||||
const logoutBtn = document.getElementById('user-menu-logout');
|
||||
const themeBtn = document.getElementById('user-menu-theme');
|
||||
const aboutBtn = document.getElementById('user-menu-about');
|
||||
const adminBtn = document.getElementById('user-menu-admin');
|
||||
const adminDivider = document.getElementById('user-menu-admin-divider');
|
||||
const profileBtn = document.getElementById('user-menu-profile');
|
||||
const roleBadge = document.getElementById('user-menu-role-badge');
|
||||
|
||||
if (!wrapper || !avatarBtn || !menu) return;
|
||||
|
||||
avatarBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const isOpen = wrapper.classList.contains('open');
|
||||
wrapper.classList.toggle('open');
|
||||
|
||||
const notifWrapper = document.getElementById('notif-wrapper');
|
||||
const notifBtn = document.getElementById('notif-bell-btn');
|
||||
if (notifWrapper) notifWrapper.classList.remove('open');
|
||||
if (notifBtn) notifBtn.classList.remove('active');
|
||||
|
||||
if (!isOpen) {
|
||||
updateUserMenuData();
|
||||
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';
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (wrapper.classList.contains('open') && !wrapper.contains(e.target)) {
|
||||
wrapper.classList.remove('open');
|
||||
}
|
||||
});
|
||||
|
||||
if (logoutBtn) {
|
||||
logoutBtn.addEventListener('click', () => {
|
||||
wrapper.classList.remove('open');
|
||||
logout();
|
||||
});
|
||||
}
|
||||
|
||||
if (themeBtn) {
|
||||
const pill = document.getElementById('theme-toggle-pill');
|
||||
const isDark = localStorage.getItem('oxicloud_theme') === 'dark';
|
||||
if (isDark) {
|
||||
if (pill) pill.classList.add('active');
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
}
|
||||
|
||||
themeBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
if (pill) {
|
||||
pill.classList.toggle('active');
|
||||
const dark = pill.classList.contains('active');
|
||||
localStorage.setItem('oxicloud_theme', dark ? 'dark' : 'light');
|
||||
document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light');
|
||||
window.ui.showNotification(
|
||||
dark ? '🌙' : '☀️',
|
||||
dark ? 'Dark mode enabled' : 'Light mode enabled'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (adminBtn) {
|
||||
adminBtn.addEventListener('click', () => {
|
||||
wrapper.classList.remove('open');
|
||||
window.location.href = '/admin';
|
||||
});
|
||||
}
|
||||
|
||||
if (profileBtn) {
|
||||
profileBtn.addEventListener('click', () => {
|
||||
wrapper.classList.remove('open');
|
||||
window.location.href = '/profile';
|
||||
});
|
||||
}
|
||||
|
||||
if (aboutBtn) {
|
||||
aboutBtn.addEventListener('click', () => {
|
||||
wrapper.classList.remove('open');
|
||||
const overlay = document.getElementById('about-modal-overlay');
|
||||
if (overlay) overlay.classList.add('show');
|
||||
});
|
||||
}
|
||||
|
||||
const aboutCloseBtn = document.getElementById('about-close-btn');
|
||||
const aboutOverlay = document.getElementById('about-modal-overlay');
|
||||
if (aboutCloseBtn) {
|
||||
aboutCloseBtn.addEventListener('click', () => {
|
||||
aboutOverlay.classList.remove('show');
|
||||
});
|
||||
}
|
||||
if (aboutOverlay) {
|
||||
aboutOverlay.addEventListener('click', (e) => {
|
||||
if (e.target === aboutOverlay) {
|
||||
aboutOverlay.classList.remove('show');
|
||||
}
|
||||
});
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && aboutOverlay.classList.contains('show')) {
|
||||
aboutOverlay.classList.remove('show');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fetchAppVersion();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
const usedBytes = userData.storage_used_bytes || 0;
|
||||
const quotaBytes = userData.storage_quota_bytes || (10 * 1024 * 1024 * 1024);
|
||||
const percentage = quotaBytes > 0 ? Math.min(Math.round((usedBytes / quotaBytes) * 100), 100) : 0;
|
||||
|
||||
if (storageFill) storageFill.style.width = percentage + '%';
|
||||
if (storageText) {
|
||||
const used = window.formatFileSize(usedBytes);
|
||||
const total = window.formatFileSize(quotaBytes);
|
||||
storageText.textContent = `${percentage}% · ${used} / ${total}`;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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 || (10 * 1024 * 1024 * 1024);
|
||||
const percentage = quotaBytes > 0 ? Math.min(Math.round((usedBytes / quotaBytes) * 100), 100) : 0;
|
||||
const barColor = percentage > 90 ? '#ef4444' : percentage > 70 ? '#f59e0b' : '#22c55e';
|
||||
|
||||
const t = (key, fallback) => (window.i18n && window.i18n.t) ? window.i18n.t(key) || fallback : fallback;
|
||||
|
||||
const 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}% · ${window.formatFileSize(usedBytes)} / ${quotaBytes > 0 ? window.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);
|
||||
requestAnimationFrame(() => overlay.classList.add('show'));
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function logout() {
|
||||
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';
|
||||
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
localStorage.removeItem(REFRESH_TOKEN_KEY);
|
||||
localStorage.removeItem(TOKEN_EXPIRY_KEY);
|
||||
localStorage.removeItem(USER_DATA_KEY);
|
||||
|
||||
sessionStorage.removeItem('redirect_count');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
|
||||
window.setupUserMenu = setupUserMenu;
|
||||
window.showUserProfileModal = showUserProfileModal;
|
||||
window.logout = logout;
|
||||
Reference in New Issue
Block a user