refactor(js): avoid use of window.XXX and move to import/export
- change worker: do not cache html pages (not necessary) - remove use of window.XXX and maximize import/export, this will provide more clarety, show circular dependencies + you will benefit IDE help
This commit is contained in:
@@ -3,6 +3,9 @@
|
||||
* Handles login, registration, and admin setup
|
||||
*/
|
||||
|
||||
import { getCsrfHeaders } from '../../core/csrf.js';
|
||||
import { i18n } from '../../core/i18n.js';
|
||||
|
||||
// API endpoints
|
||||
const API_URL = '/api/auth';
|
||||
const LOGIN_ENDPOINT = `${API_URL}/login`;
|
||||
@@ -112,7 +115,7 @@ const LANGUAGE_TEXTS = {
|
||||
|
||||
// Complete language registry — add new languages here, they'll appear automatically
|
||||
// `popular: true` languages show as cards on the main screen, the rest in the modal
|
||||
const ALL_LANGUAGES = [
|
||||
export const ALL_LANGUAGES = [
|
||||
{
|
||||
code: 'en',
|
||||
name: 'English',
|
||||
@@ -509,8 +512,8 @@ function initLanguageSelector() {
|
||||
localStorage.setItem(FIRST_RUN_KEY, 'true');
|
||||
|
||||
// Update i18n if available
|
||||
if (window.i18n?.setLocale) {
|
||||
await window.i18n.setLocale(selectedLanguage);
|
||||
if (i18n?.setLocale) {
|
||||
await i18n.setLocale(selectedLanguage);
|
||||
}
|
||||
|
||||
// Hide language panel
|
||||
@@ -632,7 +635,7 @@ async function configureOidcLoginUI() {
|
||||
// Update button text with provider name
|
||||
const btnTextEl = oidcBtn.querySelector('span');
|
||||
if (btnTextEl && oidcInfo.provider_name) {
|
||||
const template = window.i18n?.t ? window.i18n.t('auth.sso_login_provider') : 'Sign in with {{provider}}';
|
||||
const template = i18n?.t ? i18n.t('auth.sso_login_provider') : 'Sign in with {{provider}}';
|
||||
btnTextEl.textContent = template.replace('{{provider}}', oidcInfo.provider_name);
|
||||
}
|
||||
|
||||
@@ -669,7 +672,6 @@ function initLoginElements() {
|
||||
return false;
|
||||
}
|
||||
|
||||
languagePanel = document.getElementById('language-panel');
|
||||
loginPanel = document.getElementById('login-panel');
|
||||
registerPanel = document.getElementById('register-panel');
|
||||
adminSetupPanel = document.getElementById('admin-setup-panel');
|
||||
@@ -940,7 +942,7 @@ if (isLoginPage && registerForm) {
|
||||
|
||||
// Validate passwords match
|
||||
if (password !== confirmPassword) {
|
||||
const errorMsg = window.i18n ? window.i18n.t('auth.passwords_mismatch') : 'Passwords do not match';
|
||||
const errorMsg = i18n ? i18n.t('auth.passwords_mismatch') : 'Passwords do not match';
|
||||
registerError.textContent = errorMsg;
|
||||
registerError.style.display = 'block';
|
||||
return;
|
||||
@@ -950,7 +952,7 @@ if (isLoginPage && registerForm) {
|
||||
await register(username, email, password);
|
||||
|
||||
// Show success message
|
||||
const successMsg = window.i18n ? window.i18n.t('auth.account_success') : 'Account created successfully! You can now log in.';
|
||||
const successMsg = i18n ? i18n.t('auth.account_success') : 'Account created successfully! You can now log in.';
|
||||
registerSuccess.textContent = successMsg;
|
||||
registerSuccess.style.display = 'block';
|
||||
|
||||
@@ -963,7 +965,7 @@ if (isLoginPage && registerForm) {
|
||||
hidePanel(registerPanel);
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
const errorMsg = window.i18n ? window.i18n.t('auth.admin_create_error') : 'Error registering account';
|
||||
const errorMsg = i18n ? i18n.t('auth.admin_create_error') : 'Error registering account';
|
||||
registerError.textContent = error.message || errorMsg;
|
||||
registerError.style.display = 'block';
|
||||
}
|
||||
@@ -986,7 +988,7 @@ if (isLoginPage && adminSetupForm) {
|
||||
|
||||
// Validate passwords match
|
||||
if (password !== confirmPassword) {
|
||||
const errorMsg = window.i18n ? window.i18n.t('auth.passwords_mismatch') : 'Passwords do not match';
|
||||
const errorMsg = i18n ? i18n.t('auth.passwords_mismatch') : 'Passwords do not match';
|
||||
adminSetupError.textContent = errorMsg;
|
||||
adminSetupError.style.display = 'block';
|
||||
return;
|
||||
@@ -1011,7 +1013,7 @@ if (isLoginPage && adminSetupForm) {
|
||||
await response.json();
|
||||
|
||||
// Show success message in the GUI instead of alert
|
||||
const successMsg = window.i18n ? window.i18n.t('auth.admin_success') : 'Admin account created successfully! You can now log in.';
|
||||
const successMsg = i18n ? i18n.t('auth.admin_success') : 'Admin account created successfully! You can now log in.';
|
||||
|
||||
if (adminSetupSuccess) {
|
||||
adminSetupSuccess.textContent = successMsg;
|
||||
@@ -1025,7 +1027,7 @@ if (isLoginPage && adminSetupForm) {
|
||||
if (adminSetupSuccess) adminSetupSuccess.style.display = 'none';
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
const errorMsg = window.i18n ? window.i18n.t('auth.admin_create_error') : 'Error creating admin account';
|
||||
const errorMsg = i18n ? i18n.t('auth.admin_create_error') : 'Error creating admin account';
|
||||
adminSetupError.textContent = error.message || errorMsg;
|
||||
adminSetupError.style.display = 'block';
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,14 @@
|
||||
* This file handles file and folder operations (create, move, delete, rename, upload)
|
||||
*/
|
||||
|
||||
import { refreshUserData } from '../../app/authSession.js';
|
||||
import { loadFiles } from '../../app/filesView.js';
|
||||
import { app } from '../../app/state.js';
|
||||
import { showConfirmDialog, ui } from '../../app/ui.js';
|
||||
import { getCsrfHeaders, getCsrfToken } from '../../core/csrf.js';
|
||||
import { i18n } from '../../core/i18n.js';
|
||||
import { notifications } from '../../core/notifications.js';
|
||||
|
||||
/**
|
||||
* Get authorization headers for API requests.
|
||||
* Tokens are now in HttpOnly cookies — no explicit Authorization header needed.
|
||||
@@ -22,13 +30,13 @@ const fileOps = {
|
||||
|
||||
/** Start a new upload batch in the notification bell */
|
||||
_initUploadToast(totalFiles, folderName) {
|
||||
this._currentBatchId = window.notifications ? window.notifications.addUploadBatch(totalFiles, folderName) : null;
|
||||
this._currentBatchId = notifications ? notifications.addUploadBatch(totalFiles, folderName) : null;
|
||||
},
|
||||
|
||||
/** Finalise the batch in the notification bell */
|
||||
_finishUploadToast(successCount, totalFiles) {
|
||||
if (window.notifications && this._currentBatchId) {
|
||||
window.notifications.finishBatch(this._currentBatchId, successCount, totalFiles);
|
||||
if (notifications && this._currentBatchId) {
|
||||
notifications.finishBatch(this._currentBatchId, successCount, totalFiles);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -58,7 +66,7 @@ const fileOps = {
|
||||
_uploadFileXHR(formData, batchId, fileName, timeoutMs = 120000) {
|
||||
return new Promise((resolve) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
const notif = window.notifications;
|
||||
const notif = notifications;
|
||||
// Do NOT set xhr.timeout — it is a TOTAL deadline from send() to
|
||||
// response and would kill large uploads even while data is flowing.
|
||||
// Instead we rely on the stall timer (no progress for N seconds)
|
||||
@@ -299,13 +307,13 @@ const fileOps = {
|
||||
|
||||
const totalFiles = readableFiles.length;
|
||||
|
||||
if (skippedEntries.length > 0 && window.notifications) {
|
||||
const locale = window.i18n?.getCurrentLocale?.() || 'en';
|
||||
if (skippedEntries.length > 0 && notifications) {
|
||||
const locale = i18n?.getCurrentLocale?.() || 'en';
|
||||
const title = locale.startsWith('es') ? 'Entradas omitidas' : 'Entries skipped';
|
||||
const text = locale.startsWith('es')
|
||||
? `Se omitieron ${skippedEntries.length} carpeta(s)/entrada(s) no legibles. Usa "Subir carpeta".`
|
||||
: `${skippedEntries.length} unreadable folder/entry items were skipped. Use "Upload folder".`;
|
||||
window.notifications.addNotification({
|
||||
notifications.addNotification({
|
||||
icon: 'fa-folder-open',
|
||||
iconClass: 'upload',
|
||||
title,
|
||||
@@ -331,7 +339,7 @@ const fileOps = {
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
const targetFolderId = window.app.currentPath || window.app.userHomeFolderId;
|
||||
const targetFolderId = app.currentPath || app.userHomeFolderId;
|
||||
if (targetFolderId) formData.append('folder_id', targetFolderId);
|
||||
formData.append('file', file);
|
||||
|
||||
@@ -353,9 +361,9 @@ const fileOps = {
|
||||
progressBar.style.width = `${(uploadedCount / totalFiles) * 100}%`;
|
||||
}
|
||||
// Notify bell of per-file completion
|
||||
if (window.notifications && batchId) {
|
||||
if (notifications && batchId) {
|
||||
try {
|
||||
window.notifications.fileCompleted(batchId, result.ok);
|
||||
notifications.fileCompleted(batchId, result.ok);
|
||||
} catch (e) {
|
||||
console.warn('Batch progress update failed:', e);
|
||||
}
|
||||
@@ -366,8 +374,8 @@ const fileOps = {
|
||||
console.log(`Successfully uploaded ${file.name}`, result.data);
|
||||
} else {
|
||||
console.error(`Upload error for ${file.name}`);
|
||||
if (result.isTimeout && window.notifications) {
|
||||
window.notifications.addNotification({
|
||||
if (result.isTimeout && notifications) {
|
||||
notifications.addNotification({
|
||||
icon: 'fa-clock',
|
||||
iconClass: 'error',
|
||||
title: file.name,
|
||||
@@ -375,9 +383,9 @@ const fileOps = {
|
||||
});
|
||||
}
|
||||
if (result.isQuotaError) {
|
||||
const msg = result.errorMsg || window.i18n?.t('storage_quota_exceeded') || 'Storage quota exceeded';
|
||||
if (window.notifications) {
|
||||
window.notifications.addNotification({
|
||||
const msg = result.errorMsg || i18n?.t('storage_quota_exceeded') || 'Storage quota exceeded';
|
||||
if (notifications) {
|
||||
notifications.addNotification({
|
||||
icon: 'fa-exclamation-triangle',
|
||||
iconClass: 'error',
|
||||
title: file.name,
|
||||
@@ -393,14 +401,12 @@ const fileOps = {
|
||||
this._finishUploadToast(successCount, totalFiles);
|
||||
|
||||
// Refresh storage usage display
|
||||
if (typeof window.refreshUserData === 'function') {
|
||||
try {
|
||||
await window.refreshUserData();
|
||||
} catch (_) {}
|
||||
}
|
||||
try {
|
||||
await refreshUserData();
|
||||
} catch (_) {}
|
||||
|
||||
try {
|
||||
await window.loadFiles({ forceRefresh: true });
|
||||
await loadFiles({ forceRefresh: true });
|
||||
} catch (reloadError) {
|
||||
console.error('Error reloading files:', reloadError);
|
||||
}
|
||||
@@ -466,7 +472,7 @@ const fileOps = {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentFolderId = window.app.currentPath || window.app.userHomeFolderId;
|
||||
const currentFolderId = app.currentPath || app.userHomeFolderId;
|
||||
|
||||
// Build folder structure from relative paths
|
||||
const folderMap = new Map();
|
||||
@@ -528,7 +534,7 @@ const fileOps = {
|
||||
.filter(Boolean)
|
||||
)
|
||||
];
|
||||
const locale = window.i18n?.getCurrentLocale?.() || 'en';
|
||||
const locale = i18n?.getCurrentLocale?.() || 'en';
|
||||
const rootFolderLabel =
|
||||
rootFolderNames.length <= 1
|
||||
? rootFolderNames[0] || ''
|
||||
@@ -585,9 +591,9 @@ const fileOps = {
|
||||
console.warn(`[SKIP] #${idx} ${rel} — cannot read 0-byte file (FIFO/pipe?), skipping`);
|
||||
uploadedCount++;
|
||||
successCount++;
|
||||
if (window.notifications && batchId) {
|
||||
if (notifications && batchId) {
|
||||
try {
|
||||
window.notifications.fileCompleted(batchId, true);
|
||||
notifications.fileCompleted(batchId, true);
|
||||
} catch (_) {}
|
||||
}
|
||||
return;
|
||||
@@ -617,9 +623,9 @@ const fileOps = {
|
||||
|
||||
uploadedCount++;
|
||||
|
||||
if (window.notifications && batchId) {
|
||||
if (notifications && batchId) {
|
||||
try {
|
||||
window.notifications.fileCompleted(batchId, result.ok);
|
||||
notifications.fileCompleted(batchId, result.ok);
|
||||
} catch (_) {}
|
||||
}
|
||||
if (progressBar && uploadedCount % 10 === 0) {
|
||||
@@ -633,8 +639,8 @@ const fileOps = {
|
||||
successCount++;
|
||||
} else if (result.isQuotaError) {
|
||||
quotaStop = true;
|
||||
if (window.notifications) {
|
||||
window.notifications.addNotification({
|
||||
if (notifications) {
|
||||
notifications.addNotification({
|
||||
icon: 'fa-exclamation-triangle',
|
||||
iconClass: 'error',
|
||||
title: file.name,
|
||||
@@ -661,14 +667,12 @@ const fileOps = {
|
||||
|
||||
this._finishUploadToast(successCount, totalFiles);
|
||||
|
||||
if (typeof window.refreshUserData === 'function') {
|
||||
try {
|
||||
await window.refreshUserData();
|
||||
} catch (_) {}
|
||||
}
|
||||
try {
|
||||
await refreshUserData();
|
||||
} catch (_) {}
|
||||
|
||||
try {
|
||||
await window.loadFiles({ forceRefresh: true });
|
||||
await loadFiles({ forceRefresh: true });
|
||||
} catch (reloadError) {
|
||||
console.error('Error reloading files:', reloadError);
|
||||
}
|
||||
@@ -699,7 +703,7 @@ const fileOps = {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: name,
|
||||
parent_id: window.app.currentPath || window.app.userHomeFolderId || null
|
||||
parent_id: app.currentPath || app.userHomeFolderId || null
|
||||
})
|
||||
});
|
||||
|
||||
@@ -710,17 +714,17 @@ const fileOps = {
|
||||
|
||||
// Optimistic UI: add folder card directly from server response
|
||||
// — no reload needed since the backend already confirmed creation.
|
||||
window.ui.addFolderToView(folder);
|
||||
ui.addFolderToView(folder);
|
||||
|
||||
window.ui.showNotification('Folder created', `"${name}" created successfully`);
|
||||
ui.showNotification('Folder created', `"${name}" created successfully`);
|
||||
} else {
|
||||
const errorData = await response.text();
|
||||
console.error('Create folder error:', errorData);
|
||||
window.ui.showNotification('Error', 'Error creating the folder');
|
||||
ui.showNotification('Error', 'Error creating the folder');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating folder:', error);
|
||||
window.ui.showNotification('Error', 'Error creating the folder');
|
||||
ui.showNotification('Error', 'Error creating the folder');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -745,8 +749,8 @@ const fileOps = {
|
||||
|
||||
if (response.ok) {
|
||||
// Reload files after moving
|
||||
await window.loadFiles();
|
||||
window.ui.showNotification('File moved', 'File moved successfully');
|
||||
await loadFiles();
|
||||
ui.showNotification('File moved', 'File moved successfully');
|
||||
return true;
|
||||
} else {
|
||||
let errorMessage = 'Unknown error';
|
||||
@@ -756,12 +760,12 @@ const fileOps = {
|
||||
} catch (_e) {
|
||||
errorMessage = 'Error processing server response';
|
||||
}
|
||||
window.ui.showNotification('Error', `Error moving the file: ${errorMessage}`);
|
||||
ui.showNotification('Error', `Error moving the file: ${errorMessage}`);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error moving file:', error);
|
||||
window.ui.showNotification('Error', 'Error moving the file');
|
||||
ui.showNotification('Error', 'Error moving the file');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
@@ -787,8 +791,8 @@ const fileOps = {
|
||||
|
||||
if (response.ok) {
|
||||
// Reload files after moving
|
||||
await window.loadFiles();
|
||||
window.ui.showNotification('Folder moved', 'Folder moved successfully');
|
||||
await loadFiles();
|
||||
ui.showNotification('Folder moved', 'Folder moved successfully');
|
||||
return true;
|
||||
} else {
|
||||
let errorMessage = 'Unknown error';
|
||||
@@ -798,12 +802,12 @@ const fileOps = {
|
||||
} catch (_e) {
|
||||
errorMessage = 'Error processing server response';
|
||||
}
|
||||
window.ui.showNotification('Error', `Error moving the folder: ${errorMessage}`);
|
||||
ui.showNotification('Error', `Error moving the folder: ${errorMessage}`);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error moving folder:', error);
|
||||
window.ui.showNotification('Error', 'Error moving the folder');
|
||||
ui.showNotification('Error', 'Error moving the folder');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
@@ -890,8 +894,8 @@ const fileOps = {
|
||||
if (response.ok) {
|
||||
await response.json();
|
||||
// Reload files after copying
|
||||
await window.loadFiles();
|
||||
window.ui.showNotification('File copied', 'File copied successfully');
|
||||
await loadFiles();
|
||||
ui.showNotification('File copied', 'File copied successfully');
|
||||
return true;
|
||||
} else {
|
||||
let errorMessage = 'Unknown error';
|
||||
@@ -901,12 +905,12 @@ const fileOps = {
|
||||
} catch (_e) {
|
||||
errorMessage = 'Error processing server response';
|
||||
}
|
||||
window.ui.showNotification('Error', `Error copying the file: ${errorMessage}`);
|
||||
ui.showNotification('Error', `Error copying the file: ${errorMessage}`);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error copying file:', error);
|
||||
window.ui.showNotification('Error', 'Error copying the file');
|
||||
ui.showNotification('Error', 'Error copying the file');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
@@ -920,7 +924,7 @@ const fileOps = {
|
||||
*/
|
||||
async copyFolder(_folderId, _targetFolderId) {
|
||||
// Folder copy is not yet implemented in the backend
|
||||
window.ui.showNotification('Not implemented', 'Folder copy is not yet supported');
|
||||
ui.showNotification('Not implemented', 'Folder copy is not yet supported');
|
||||
return false;
|
||||
},
|
||||
|
||||
@@ -954,7 +958,7 @@ const fileOps = {
|
||||
|
||||
// Note: Folder copy is not yet implemented in batch API
|
||||
if (folderIds.length > 0) {
|
||||
window.ui.showNotification('Info', 'Folder copy is not yet supported in batch mode');
|
||||
ui.showNotification('Info', 'Folder copy is not yet supported in batch mode');
|
||||
errors += folderIds.lenngth;
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -990,9 +994,9 @@ const fileOps = {
|
||||
console.log('Response status:', response.status);
|
||||
|
||||
if (response.ok) {
|
||||
window.ui.showNotification(
|
||||
window.i18n ? window.i18n.t('notifications.file_renamed') : 'File renamed',
|
||||
window.i18n ? window.i18n.t('notifications.file_renamed_to', { name: newName }) : `File renamed to "${newName}"`
|
||||
ui.showNotification(
|
||||
i18n ? i18n.t('notifications.file_renamed') : 'File renamed',
|
||||
i18n ? i18n.t('notifications.file_renamed_to', { name: newName }) : `File renamed to "${newName}"`
|
||||
);
|
||||
return true;
|
||||
} else {
|
||||
@@ -1005,12 +1009,12 @@ const fileOps = {
|
||||
} catch (_e) {
|
||||
errorMessage = errorText || response.statusText;
|
||||
}
|
||||
window.ui.showNotification('Error', `Error renaming the file: ${errorMessage}`);
|
||||
ui.showNotification('Error', `Error renaming the file: ${errorMessage}`);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error renaming file:', error);
|
||||
window.ui.showNotification('Error', 'Error renaming the file');
|
||||
ui.showNotification('Error', 'Error renaming the file');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
@@ -1037,7 +1041,7 @@ const fileOps = {
|
||||
console.log('Response status:', response.status);
|
||||
|
||||
if (response.ok) {
|
||||
window.ui.showNotification('Folder renamed', `Folder renamed to "${newName}"`);
|
||||
ui.showNotification('Folder renamed', `Folder renamed to "${newName}"`);
|
||||
return true;
|
||||
} else {
|
||||
const errorText = await response.text();
|
||||
@@ -1053,12 +1057,12 @@ const fileOps = {
|
||||
errorMessage = errorText || response.statusText;
|
||||
}
|
||||
|
||||
window.ui.showNotification('Error', `Error renaming the folder: ${errorMessage}`);
|
||||
ui.showNotification('Error', `Error renaming the folder: ${errorMessage}`);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error renaming folder:', error);
|
||||
window.ui.showNotification('Error', 'Error renaming the folder');
|
||||
ui.showNotification('Error', 'Error renaming the folder');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
@@ -1071,11 +1075,9 @@ const fileOps = {
|
||||
*/
|
||||
async deleteFile(fileId, fileName) {
|
||||
const confirmed = await showConfirmDialog({
|
||||
title: window.i18n ? window.i18n.t('dialogs.confirm_delete') : 'Move to trash',
|
||||
message: window.i18n
|
||||
? window.i18n.t('dialogs.confirm_delete_file', { name: fileName })
|
||||
: `Are you sure you want to move the file "${fileName}" to trash?`,
|
||||
confirmText: window.i18n ? window.i18n.t('actions.delete') : 'Delete'
|
||||
title: i18n ? i18n.t('dialogs.confirm_delete') : 'Move to trash',
|
||||
message: i18n ? i18n.t('dialogs.confirm_delete_file', { name: fileName }) : `Are you sure you want to move the file "${fileName}" to trash?`,
|
||||
confirmText: i18n ? i18n.t('actions.delete') : 'Delete'
|
||||
});
|
||||
if (!confirmed) return false;
|
||||
|
||||
@@ -1087,8 +1089,8 @@ const fileOps = {
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
window.loadFiles();
|
||||
window.ui.showNotification('File moved to trash', `"${fileName}" moved to trash`);
|
||||
loadFiles();
|
||||
ui.showNotification('File moved to trash', `"${fileName}" moved to trash`);
|
||||
return true;
|
||||
} else {
|
||||
// Fallback to direct deletion if trash fails
|
||||
@@ -1098,17 +1100,17 @@ const fileOps = {
|
||||
});
|
||||
|
||||
if (fallbackResponse.ok) {
|
||||
window.loadFiles();
|
||||
window.ui.showNotification('File deleted', `"${fileName}" deleted successfully`);
|
||||
loadFiles();
|
||||
ui.showNotification('File deleted', `"${fileName}" deleted successfully`);
|
||||
return true;
|
||||
} else {
|
||||
window.ui.showNotification('Error', 'Error deleting the file');
|
||||
ui.showNotification('Error', 'Error deleting the file');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting file:', error);
|
||||
window.ui.showNotification('Error', 'Error deleting the file');
|
||||
ui.showNotification('Error', 'Error deleting the file');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
@@ -1121,11 +1123,11 @@ const fileOps = {
|
||||
*/
|
||||
async deleteFolder(folderId, folderName) {
|
||||
const confirmed = await showConfirmDialog({
|
||||
title: window.i18n ? window.i18n.t('dialogs.confirm_delete') : 'Move to trash',
|
||||
message: window.i18n
|
||||
? window.i18n.t('dialogs.confirm_delete_folder', { name: folderName })
|
||||
title: i18n ? i18n.t('dialogs.confirm_delete') : 'Move to trash',
|
||||
message: i18n
|
||||
? i18n.t('dialogs.confirm_delete_folder', { name: folderName })
|
||||
: `Are you sure you want to move the folder "${folderName}" and all its contents to trash?`,
|
||||
confirmText: window.i18n ? window.i18n.t('actions.delete') : 'Delete'
|
||||
confirmText: i18n ? i18n.t('actions.delete') : 'Delete'
|
||||
});
|
||||
if (!confirmed) return false;
|
||||
|
||||
@@ -1138,12 +1140,12 @@ const fileOps = {
|
||||
|
||||
if (response.ok) {
|
||||
// If we're inside the folder we just deleted, go back up
|
||||
if (window.app.currentPath === folderId) {
|
||||
window.app.currentPath = '';
|
||||
window.ui.updateBreadcrumb('');
|
||||
if (app.currentPath === folderId) {
|
||||
app.currentPath = '';
|
||||
ui.updateBreadcrumb('');
|
||||
}
|
||||
window.loadFiles();
|
||||
window.ui.showNotification('Folder moved to trash', `"${folderName}" moved to trash`);
|
||||
loadFiles();
|
||||
ui.showNotification('Folder moved to trash', `"${folderName}" moved to trash`);
|
||||
return true;
|
||||
} else {
|
||||
// Fallback to direct deletion if trash fails
|
||||
@@ -1154,21 +1156,21 @@ const fileOps = {
|
||||
|
||||
if (fallbackResponse.ok) {
|
||||
// If we're inside the folder we just deleted, go back up
|
||||
if (window.app.currentPath === folderId) {
|
||||
window.app.currentPath = '';
|
||||
window.ui.updateBreadcrumb('');
|
||||
if (app.currentPath === folderId) {
|
||||
app.currentPath = '';
|
||||
ui.updateBreadcrumb('');
|
||||
}
|
||||
window.loadFiles();
|
||||
window.ui.showNotification('Folder deleted', `"${folderName}" deleted successfully`);
|
||||
loadFiles();
|
||||
ui.showNotification('Folder deleted', `"${folderName}" deleted successfully`);
|
||||
return true;
|
||||
} else {
|
||||
window.ui.showNotification('Error', 'Error deleting the folder');
|
||||
ui.showNotification('Error', 'Error deleting the folder');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting folder:', error);
|
||||
window.ui.showNotification('Error', 'Error deleting the folder');
|
||||
ui.showNotification('Error', 'Error deleting the folder');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
@@ -1212,15 +1214,15 @@ const fileOps = {
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
window.ui.showNotification('Item restored', 'Item restored successfully');
|
||||
ui.showNotification('Item restored', 'Item restored successfully');
|
||||
return true;
|
||||
} else {
|
||||
window.ui.showNotification('Error', 'Error restoring the item');
|
||||
ui.showNotification('Error', 'Error restoring the item');
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error restoring item from trash:', error);
|
||||
window.ui.showNotification('Error', 'Error restoring the item');
|
||||
ui.showNotification('Error', 'Error restoring the item');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
@@ -1232,11 +1234,11 @@ const fileOps = {
|
||||
*/
|
||||
async deletePermanently(trashId) {
|
||||
const confirmed = await showConfirmDialog({
|
||||
title: window.i18n ? window.i18n.t('dialogs.confirm_permanent_delete') : 'Delete permanently',
|
||||
message: window.i18n
|
||||
? window.i18n.t('dialogs.confirm_permanent_delete_msg')
|
||||
title: i18n ? i18n.t('dialogs.confirm_permanent_delete') : 'Delete permanently',
|
||||
message: i18n
|
||||
? i18n.t('dialogs.confirm_permanent_delete_msg')
|
||||
: 'Are you sure you want to permanently delete this item? This action cannot be undone.',
|
||||
confirmText: window.i18n ? window.i18n.t('actions.delete_permanently') : 'Delete permanently'
|
||||
confirmText: i18n ? i18n.t('actions.delete_permanently') : 'Delete permanently'
|
||||
});
|
||||
if (!confirmed) return false;
|
||||
|
||||
@@ -1247,15 +1249,15 @@ const fileOps = {
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
window.ui.showNotification('Item deleted', 'Item permanently deleted');
|
||||
ui.showNotification('Item deleted', 'Item permanently deleted');
|
||||
return true;
|
||||
} else {
|
||||
window.ui.showNotification('Error', 'Error deleting the item');
|
||||
ui.showNotification('Error', 'Error deleting the item');
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting item permanently:', error);
|
||||
window.ui.showNotification('Error', 'Error deleting the item');
|
||||
ui.showNotification('Error', 'Error deleting the item');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
@@ -1266,11 +1268,9 @@ const fileOps = {
|
||||
*/
|
||||
async emptyTrash() {
|
||||
const confirmed = await showConfirmDialog({
|
||||
title: window.i18n ? window.i18n.t('dialogs.confirm_empty_trash') : 'Empty trash',
|
||||
message: window.i18n
|
||||
? window.i18n.t('trash.empty_confirm')
|
||||
: 'Are you sure you want to empty the trash? This action will permanently delete all items.',
|
||||
confirmText: window.i18n ? window.i18n.t('actions.empty_trash') : 'Empty trash'
|
||||
title: i18n ? i18n.t('dialogs.confirm_empty_trash') : 'Empty trash',
|
||||
message: i18n ? i18n.t('trash.empty_confirm') : 'Are you sure you want to empty the trash? This action will permanently delete all items.',
|
||||
confirmText: i18n ? i18n.t('actions.empty_trash') : 'Empty trash'
|
||||
});
|
||||
if (!confirmed) return false;
|
||||
|
||||
@@ -1281,15 +1281,15 @@ const fileOps = {
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
window.ui.showNotification('Trash emptied', 'The trash has been emptied successfully');
|
||||
ui.showNotification('Trash emptied', 'The trash has been emptied successfully');
|
||||
return true;
|
||||
} else {
|
||||
window.ui.showNotification('Error', 'Error emptying the trash');
|
||||
ui.showNotification('Error', 'Error emptying the trash');
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error emptying trash:', error);
|
||||
window.ui.showNotification('Error', 'Error emptying the trash');
|
||||
ui.showNotification('Error', 'Error emptying the trash');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
@@ -1315,11 +1315,11 @@ const fileOps = {
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
} else {
|
||||
window.ui.showNotification('Error', 'Error downloading the file');
|
||||
ui.showNotification('Error', 'Error downloading the file');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error downloading file:', error);
|
||||
window.ui.showNotification('Error', 'Error downloading the file');
|
||||
ui.showNotification('Error', 'Error downloading the file');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1331,7 +1331,7 @@ const fileOps = {
|
||||
async downloadFolder(folderId, folderName) {
|
||||
try {
|
||||
// Show notification to user
|
||||
window.ui.showNotification('Preparing download', 'Preparing the folder for download...');
|
||||
ui.showNotification('Preparing download', 'Preparing the folder for download...');
|
||||
|
||||
const response = await fetch(`/api/folders/${folderId}/download?format=zip`, {
|
||||
headers: getAuthHeaders()
|
||||
@@ -1347,14 +1347,13 @@ const fileOps = {
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
} else {
|
||||
window.ui.showNotification('Error', 'Error downloading the folder');
|
||||
ui.showNotification('Error', 'Error downloading the folder');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error downloading folder:', error);
|
||||
window.ui.showNotification('Error', 'Error downloading the folder');
|
||||
ui.showNotification('Error', 'Error downloading the folder');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Expose file operations module globally
|
||||
window.fileOps = fileOps;
|
||||
export { fileOps, getAuthHeaders };
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
* A simpler approach to viewing files that doesn't rely on complex DOM manipulation
|
||||
*/
|
||||
|
||||
import { updateHistory } from '../../app/main.js';
|
||||
import { app } from '../../app/state.js';
|
||||
import { isTextViewable } from '../../core/formatters.js';
|
||||
import { wopiEditor } from './wopiEditor.js';
|
||||
|
||||
class InlineViewer {
|
||||
constructor() {
|
||||
this.setupViewer();
|
||||
@@ -97,9 +102,9 @@ class InlineViewer {
|
||||
const ext = (file.name || '').split('.').pop().toLowerCase();
|
||||
const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'bmp', 'ico', 'heic', 'heif', 'avif', 'tiff'];
|
||||
const isImage = file.mime_type?.startsWith('image/') || imageExts.includes(ext);
|
||||
if (!isImage && window.wopiEditor && (await window.wopiEditor.canEdit(file.name))) {
|
||||
if (!isImage && wopiEditor && (await wopiEditor.canEdit(file.name))) {
|
||||
try {
|
||||
window.wopiEditor.openInModal(file.id, file.name, 'edit');
|
||||
wopiEditor.openInModal(file.id, file.name, 'edit');
|
||||
return;
|
||||
} catch (e) {
|
||||
console.warn('WOPI editor failed, falling back to inline viewer:', e);
|
||||
@@ -204,9 +209,9 @@ class InlineViewer {
|
||||
modal.classList.add('active');
|
||||
}
|
||||
|
||||
// Check if a MIME type is text-viewable — delegates to window.isTextViewable
|
||||
// Check if a MIME type is text-viewable
|
||||
isTextViewable(mimeType) {
|
||||
return window.isTextViewable ? window.isTextViewable(mimeType) : false;
|
||||
return isTextViewable(mimeType);
|
||||
}
|
||||
|
||||
// Creates a text viewer using authenticated fetch
|
||||
@@ -513,8 +518,8 @@ class InlineViewer {
|
||||
}
|
||||
|
||||
// clear
|
||||
window.app.viewFile = null;
|
||||
window.updateHistory(false);
|
||||
app.viewFile = null;
|
||||
updateHistory(false);
|
||||
|
||||
// Clear references
|
||||
this.currentFile = null;
|
||||
@@ -573,21 +578,4 @@ class InlineViewer {
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize viewer when document is ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Check if it's already initialized
|
||||
if (!window.inlineViewer) {
|
||||
console.log('Initializing inline viewer on DOMContentLoaded');
|
||||
window.inlineViewer = new InlineViewer();
|
||||
}
|
||||
});
|
||||
|
||||
// Fallback initialization for cases where DOMContentLoaded already fired
|
||||
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
||||
if (!window.inlineViewer) {
|
||||
console.log('Fallback initialization for inline viewer');
|
||||
setTimeout(() => {
|
||||
window.inlineViewer = new InlineViewer();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
export const inlineViewer = new InlineViewer();
|
||||
|
||||
@@ -11,6 +11,14 @@
|
||||
|
||||
// @ts-check
|
||||
|
||||
import { loadFiles } from '../../app/filesView.js';
|
||||
import { app } from '../../app/state.js';
|
||||
import { showConfirmDialog, ui } from '../../app/ui.js';
|
||||
import { i18n } from '../../core/i18n.js';
|
||||
import { favorites } from '../library/favorites.js';
|
||||
import { contextMenus } from './contextMenus.js';
|
||||
import { getAuthHeaders } from './fileOperations.js';
|
||||
|
||||
const multiSelect = {
|
||||
/** Currently selected items: Map<id, { id, name, type, parentId }> */
|
||||
_selected: new Map(),
|
||||
@@ -42,8 +50,8 @@ const multiSelect = {
|
||||
// ── Helpers for i18n ────────────────────────────────────
|
||||
|
||||
_t(key, vars) {
|
||||
if (window.i18n && typeof window.i18n.t === 'function') {
|
||||
const val = window.i18n.t(key, vars);
|
||||
if (i18n && typeof i18n.t === 'function') {
|
||||
const val = i18n.t(key, vars);
|
||||
// If i18n returned the key itself, it's missing → fall back
|
||||
if (val && val !== key) return val;
|
||||
}
|
||||
@@ -133,15 +141,15 @@ const multiSelect = {
|
||||
showBatchResult(action, result) {
|
||||
if (action === 'copy') {
|
||||
if (result.errors > 0) {
|
||||
window.ui.showNotification('Batch copy', `${result.success} copied, ${result.errors} failed`);
|
||||
ui.showNotification('Batch copy', `${result.success} copied, ${result.errors} failed`);
|
||||
} else {
|
||||
window.ui.showNotification('Items copied', `${result.success} item${result.success !== 1 ? 's' : ''} copied successfully`);
|
||||
ui.showNotification('Items copied', `${result.success} item${result.success !== 1 ? 's' : ''} copied successfully`);
|
||||
}
|
||||
} else {
|
||||
if (result.errors > 0) {
|
||||
window.ui.showNotification('Batch move', `${result.success} moved, ${result.errors} failed`);
|
||||
ui.showNotification('Batch move', `${result.success} moved, ${result.errors} failed`);
|
||||
} else {
|
||||
window.ui.showNotification('Items moved', `${result.success} item${result.success !== 1 ? 's' : ''} moved successfully`);
|
||||
ui.showNotification('Items moved', `${result.success} item${result.success !== 1 ? 's' : ''} moved successfully`);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -366,18 +374,18 @@ const multiSelect = {
|
||||
const errors = data.stats?.failed || 0;
|
||||
|
||||
this.clear();
|
||||
window.loadFiles();
|
||||
loadFiles();
|
||||
|
||||
if (errors > 0) {
|
||||
window.ui.showNotification('Batch delete', `${success} moved to trash, ${errors} failed`);
|
||||
ui.showNotification('Batch delete', `${success} moved to trash, ${errors} failed`);
|
||||
} else {
|
||||
window.ui.showNotification('Moved to trash', `${success} item${success !== 1 ? 's' : ''} moved to trash`);
|
||||
ui.showNotification('Moved to trash', `${success} item${success !== 1 ? 's' : ''} moved to trash`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Batch trash error:', e);
|
||||
window.ui.showNotification('Error', 'Could not move items to trash');
|
||||
ui.showNotification('Error', 'Could not move items to trash');
|
||||
this.clear();
|
||||
window.loadFiles();
|
||||
loadFiles();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -386,9 +394,9 @@ const multiSelect = {
|
||||
const items = this.items;
|
||||
if (items.length === 0) return;
|
||||
|
||||
window.app.moveDialogMode = 'batch';
|
||||
window.app.batchMoveItems = items;
|
||||
window.app.selectedTargetFolderId = '';
|
||||
app.moveDialogMode = 'batch';
|
||||
app.batchMoveItems = items;
|
||||
app.selectedTargetFolderId = '';
|
||||
|
||||
const dialog = document.getElementById('move-file-dialog');
|
||||
const dialogHeader = dialog.querySelector('.rename-dialog-header');
|
||||
@@ -406,7 +414,7 @@ const multiSelect = {
|
||||
const items = this.items;
|
||||
if (items.length === 0) return;
|
||||
|
||||
window.ui.showNotification('Preparing download', 'Creating ZIP archive...');
|
||||
ui.showNotification('Preparing download', 'Creating ZIP archive...');
|
||||
|
||||
try {
|
||||
const fileIds = items.filter((i) => i.type === 'file').map((i) => i.id);
|
||||
@@ -431,20 +439,20 @@ const multiSelect = {
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (e) {
|
||||
console.error('Batch download error:', e);
|
||||
window.ui.showNotification('Error', 'Could not download selected items');
|
||||
ui.showNotification('Error', 'Could not download selected items');
|
||||
}
|
||||
},
|
||||
|
||||
/** Batch add to favorites — single API call */
|
||||
async batchFavorites() {
|
||||
const items = this.items;
|
||||
if (items.length === 0 || !window.favorites) return;
|
||||
if (items.length === 0 || !favorites) return;
|
||||
|
||||
// Filter out items already in favourites
|
||||
const toAdd = items.filter((i) => !window.favorites.isFavorite(i.id, i.type));
|
||||
const toAdd = items.filter((i) => !favorites.isFavorite(i.id, i.type));
|
||||
if (toAdd.length === 0) {
|
||||
this.clear();
|
||||
window.ui.showNotification(this._t('favorites.add') || 'Favorites', 'All selected items are already favorites');
|
||||
ui.showNotification(this._t('favorites.add') || 'Favorites', 'All selected items are already favorites');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -463,23 +471,23 @@ const multiSelect = {
|
||||
const inserted = data.stats?.inserted || 0;
|
||||
|
||||
// Replace cache directly from response (no extra GET)
|
||||
if (data.favorites && window.favorites._replaceCacheFromResponse) {
|
||||
window.favorites._replaceCacheFromResponse(data.favorites);
|
||||
if (data.favorites && favorites._replaceCacheFromResponse) {
|
||||
favorites._replaceCacheFromResponse(data.favorites);
|
||||
} else {
|
||||
await window.favorites._fetchFromServer();
|
||||
await favorites._fetchFromServer();
|
||||
}
|
||||
|
||||
this.clear();
|
||||
if (typeof window.loadFiles === 'function') window.loadFiles();
|
||||
loadFiles();
|
||||
|
||||
if (inserted > 0) {
|
||||
window.ui.showNotification(this._t('favorites.add') || 'Added to favorites', `${inserted} item${inserted !== 1 ? 's' : ''} added to favorites`);
|
||||
ui.showNotification(this._t('favorites.add') || 'Added to favorites', `${inserted} item${inserted !== 1 ? 's' : ''} added to favorites`);
|
||||
} else {
|
||||
window.ui.showNotification(this._t('favorites.add') || 'Favorites', 'All selected items are already favorites');
|
||||
ui.showNotification(this._t('favorites.add') || 'Favorites', 'All selected items are already favorites');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Batch favorites error:', e);
|
||||
window.ui.showNotification('Error', 'Could not add items to favorites');
|
||||
ui.showNotification('Error', 'Could not add items to favorites');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -510,8 +518,8 @@ const multiSelect = {
|
||||
const batchSelectionBar = document.getElementById('batch-selection-bar');
|
||||
batchSelectionBar.innerHTML = this._buildSelectionBarHTML();
|
||||
|
||||
if (window.i18n?.translateElement) {
|
||||
window.i18n.translateElement(batchSelectionBar);
|
||||
if (i18n?.translateElement) {
|
||||
i18n.translateElement(batchSelectionBar);
|
||||
}
|
||||
this._wireBarButtons();
|
||||
},
|
||||
@@ -524,5 +532,4 @@ const multiSelect = {
|
||||
}
|
||||
};
|
||||
|
||||
// Expose globally
|
||||
window.multiSelect = multiSelect;
|
||||
export { multiSelect };
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
* displays the enriched results returned by the server.
|
||||
*/
|
||||
|
||||
import { loadFiles } from '../../app/filesView.js';
|
||||
import { app } from '../../app/state.js';
|
||||
import { ui } from '../../app/ui.js';
|
||||
import { getAuthHeaders } from './fileOperations.js';
|
||||
|
||||
const search = {
|
||||
/**
|
||||
* Perform a search using query parameters.
|
||||
@@ -55,7 +60,7 @@ const search = {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error performing search:', error);
|
||||
window.ui.showNotification('Error', 'Error performing search');
|
||||
ui.showNotification('Error', 'Error performing search');
|
||||
return {
|
||||
files: [],
|
||||
folders: [],
|
||||
@@ -109,7 +114,7 @@ const search = {
|
||||
* @param {Object} results - Enriched search results from backend
|
||||
*/
|
||||
displaySearchResults(results) {
|
||||
window.ui.resetFilesList(); // ensure also list visible & error hidden
|
||||
ui.resetFilesList(); // ensure also list visible & error hidden
|
||||
//FIXME: move into action rather bage sticky header + hide bread crumb ? + note also that search result does not consider section
|
||||
const pageStickyHeader = document.getElementById('page-sticky-header');
|
||||
|
||||
@@ -163,17 +168,17 @@ const search = {
|
||||
if (clearSearchBtn) {
|
||||
clearSearchBtn.addEventListener('click', () => {
|
||||
document.querySelector('.search-container input').value = '';
|
||||
window.app.currentPath = '';
|
||||
window.app.isSearchMode = false;
|
||||
app.currentPath = '';
|
||||
app.isSearchMode = false;
|
||||
document.querySelector('.search-results-header')?.remove();
|
||||
window.ui.updateBreadcrumb('');
|
||||
window.loadFiles();
|
||||
ui.updateBreadcrumb('');
|
||||
loadFiles();
|
||||
});
|
||||
}
|
||||
|
||||
// Empty state
|
||||
if (results.files.length === 0 && results.folders.length === 0) {
|
||||
window.ui.showError(`
|
||||
ui.showError(`
|
||||
<i class="fas fa-search empty-state-icon"></i>
|
||||
<p class="search-empty-text">No results found for this search</p>
|
||||
`);
|
||||
@@ -182,12 +187,12 @@ const search = {
|
||||
|
||||
// Render folders (server-provided enriched data)
|
||||
results.folders.forEach((folder) => {
|
||||
window.ui.addFolderToView(folder);
|
||||
ui.addFolderToView(folder);
|
||||
});
|
||||
|
||||
// Render files (server-provided enriched data)
|
||||
results.files.forEach((file) => {
|
||||
window.ui.addFileToView(file);
|
||||
ui.addFileToView(file);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -203,19 +208,18 @@ const search = {
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
window.ui.showNotification('Cache cleared', 'Search cache cleared successfully');
|
||||
ui.showNotification('Cache cleared', 'Search cache cleared successfully');
|
||||
return true;
|
||||
} else {
|
||||
window.ui.showNotification('Error', 'Error clearing search cache');
|
||||
ui.showNotification('Error', 'Error clearing search cache');
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error clearing search cache:', error);
|
||||
window.ui.showNotification('Error', 'Error clearing search cache');
|
||||
ui.showNotification('Error', 'Error clearing search cache');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Expose the search module globally
|
||||
window.search = search;
|
||||
export { search };
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
* Opens document files in Collabora Online / OnlyOffice via WOPI protocol.
|
||||
* Supports two modes: in-app modal (default) and new browser tab.
|
||||
*/
|
||||
|
||||
import { loadFiles } from '../../app/filesView.js';
|
||||
|
||||
class WopiEditor {
|
||||
constructor() {
|
||||
this.editorModal = null;
|
||||
@@ -203,10 +206,9 @@ class WopiEditor {
|
||||
this._messageHandler = null;
|
||||
}
|
||||
this.editorModal = null;
|
||||
|
||||
// Refresh file list to pick up any saves
|
||||
if (typeof loadFiles === 'function') {
|
||||
loadFiles();
|
||||
}
|
||||
loadFiles();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -243,7 +245,7 @@ class WopiEditor {
|
||||
}
|
||||
|
||||
// Global instance
|
||||
window.wopiEditor = new WopiEditor();
|
||||
export const wopiEditor = new WopiEditor();
|
||||
|
||||
// Prefetch supported extensions so canEdit() is fast on first use
|
||||
window.wopiEditor._fetchSupportedExtensions();
|
||||
wopiEditor._fetchSupportedExtensions();
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
* rendering path so star icons can be painted without a round-trip.
|
||||
*/
|
||||
|
||||
import { ui } from '../../app/ui.js';
|
||||
import { getCsrfHeaders } from '../../core/csrf.js';
|
||||
import { i18n } from '../../core/i18n.js';
|
||||
|
||||
const favorites = {
|
||||
/** @type {Map<string, object>} key = "file:<id>" | "folder:<id>" */
|
||||
_cache: new Map(),
|
||||
@@ -104,10 +108,10 @@ const favorites = {
|
||||
await this._fetchFromServer();
|
||||
|
||||
// Notify user
|
||||
if (window.ui?.showNotification) {
|
||||
window.ui.showNotification(
|
||||
window.i18n ? window.i18n.t('favorites.added_title') : 'Added to favorites',
|
||||
`"${name}" ${window.i18n ? window.i18n.t('favorites.added_msg') : 'added to favorites'}`
|
||||
if (ui?.showNotification) {
|
||||
ui.showNotification(
|
||||
i18n ? i18n.t('favorites.added_title') : 'Added to favorites',
|
||||
`"${name}" ${i18n ? i18n.t('favorites.added_msg') : 'added to favorites'}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -139,10 +143,10 @@ const favorites = {
|
||||
// Remove from local cache
|
||||
this._cache.delete(this._cacheKey(id, type));
|
||||
|
||||
if (window.ui?.showNotification) {
|
||||
window.ui.showNotification(
|
||||
window.i18n ? window.i18n.t('favorites.removed_title') : 'Removed from favorites',
|
||||
`"${itemName}" ${window.i18n ? window.i18n.t('favorites.removed_msg') : 'removed from favorites'}`
|
||||
if (ui?.showNotification) {
|
||||
ui.showNotification(
|
||||
i18n ? i18n.t('favorites.removed_title') : 'Removed from favorites',
|
||||
`"${itemName}" ${i18n ? i18n.t('favorites.removed_msg') : 'removed from favorites'}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -167,14 +171,14 @@ const favorites = {
|
||||
await this._fetchFromServer();
|
||||
}
|
||||
|
||||
window.ui.resetFilesList(); // ensure also list visible & error hidden
|
||||
window.ui.updateBreadcrumb('');
|
||||
ui.resetFilesList(); // ensure also list visible & error hidden
|
||||
ui.updateBreadcrumb('');
|
||||
|
||||
if (this._cache.size === 0) {
|
||||
window.ui.showError(`
|
||||
ui.showError(`
|
||||
<i class="fas fa-star empty-state-icon"></i>
|
||||
<p>${window.i18n ? window.i18n.t('favorites.empty_state') : 'No favorite items'}</p>
|
||||
<p>${window.i18n ? window.i18n.t('favorites.empty_hint') : 'To mark as favorite, right-click on any file or folder'}</p>
|
||||
<p>${i18n ? i18n.t('favorites.empty_state') : 'No favorite items'}</p>
|
||||
<p>${i18n ? i18n.t('favorites.empty_hint') : 'To mark as favorite, right-click on any file or folder'}</p>
|
||||
`);
|
||||
return;
|
||||
}
|
||||
@@ -204,16 +208,15 @@ const favorites = {
|
||||
});
|
||||
}
|
||||
}
|
||||
if (folders.length) window.ui.renderFolders(folders);
|
||||
if (files.length) window.ui.renderFiles(files);
|
||||
if (folders.length) ui.renderFolders(folders);
|
||||
if (files.length) ui.renderFiles(files);
|
||||
} catch (error) {
|
||||
console.error('Error displaying favorites:', error);
|
||||
if (window.ui?.showNotification) {
|
||||
window.ui.showNotification('Error', 'Error loading favorite items');
|
||||
if (ui?.showNotification) {
|
||||
ui.showNotification('Error', 'Error loading favorite items');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Expose globally
|
||||
window.favorites = favorites;
|
||||
export { favorites };
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
import { app } from '../../app/state.js';
|
||||
import { getCsrfHeaders } from '../../core/csrf.js';
|
||||
import { formatFileSize } from '../../core/formatters.js';
|
||||
import { i18n } from '../../core/i18n.js';
|
||||
import { oxiIcon } from '../../core/icons.js';
|
||||
import { Modal } from '../../core/modal.js';
|
||||
import { notifications } from '../../core/notifications.js';
|
||||
|
||||
/**
|
||||
* OxiCloud - Music Library View
|
||||
* Playlist management with track listings and audio player
|
||||
@@ -13,7 +21,7 @@ const musicView = {
|
||||
selected: new Set(),
|
||||
|
||||
_headers(json = false) {
|
||||
const h = typeof getCsrfHeaders === 'function' ? { ...getCsrfHeaders() } : {};
|
||||
const h = { ...getCsrfHeaders() };
|
||||
if (json) h['Content-Type'] = 'application/json';
|
||||
return h;
|
||||
},
|
||||
@@ -78,8 +86,9 @@ const musicView = {
|
||||
_renderPlaylists() {
|
||||
if (!this._container) return;
|
||||
|
||||
const t = (key, fallback = '') => {
|
||||
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
|
||||
// FIXME should call directly
|
||||
const t = (key, _fallback = '') => {
|
||||
return i18n.t(key);
|
||||
};
|
||||
|
||||
// Empty state: no playlists at all — show full-width centered onboarding
|
||||
@@ -178,7 +187,7 @@ const musicView = {
|
||||
if (!listEl) return;
|
||||
|
||||
const t = (key, fallback = '') => {
|
||||
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
|
||||
return i18n?.t ? i18n.t(key) : fallback || key;
|
||||
};
|
||||
|
||||
if (this.playlists.length === 0) {
|
||||
@@ -283,7 +292,7 @@ const musicView = {
|
||||
if (nameEl) nameEl.textContent = playlist.name;
|
||||
if (metaEl) {
|
||||
const t = (key, fallback = '') => {
|
||||
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
|
||||
return i18n?.t ? i18n.t(key) : fallback || key;
|
||||
};
|
||||
metaEl.textContent = `${playlist.track_count || 0} ${t('music.tracks', 'tracks')}`;
|
||||
}
|
||||
@@ -305,7 +314,7 @@ const musicView = {
|
||||
}
|
||||
const togglePublicBtn = document.getElementById('music-toggle-public-btn');
|
||||
if (togglePublicBtn) {
|
||||
const t2 = (key, fallback = '') => (typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key);
|
||||
const t2 = (key, fallback = '') => (i18n?.t ? i18n.t(key) : fallback || key);
|
||||
togglePublicBtn.title = playlist.is_public ? t2('music.make_private', 'Make private') : t2('music.make_public', 'Make public');
|
||||
togglePublicBtn.classList.toggle('active', playlist.is_public);
|
||||
}
|
||||
@@ -344,7 +353,7 @@ const musicView = {
|
||||
if (!trackListEl) return;
|
||||
|
||||
const t = (key, fallback = '') => {
|
||||
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
|
||||
return i18n?.t ? i18n.t(key) : fallback || key;
|
||||
};
|
||||
|
||||
if (this.currentTracks.length === 0) {
|
||||
@@ -459,7 +468,7 @@ const musicView = {
|
||||
if (!this.currentTracks[idx]) return;
|
||||
|
||||
const t = (key, fallback = '') => {
|
||||
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
|
||||
return i18n?.t ? i18n.t(key) : fallback || key;
|
||||
};
|
||||
musicPlayer.setQueue(this.currentTracks, this.currentPlaylist?.name || t('music.playlists', 'Playlist'));
|
||||
musicPlayer.playTrack(idx);
|
||||
@@ -479,7 +488,7 @@ const musicView = {
|
||||
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
||||
}
|
||||
const t = (key, fallback = '') => {
|
||||
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
|
||||
return i18n?.t ? i18n.t(key) : fallback || key;
|
||||
};
|
||||
musicPlayer.setQueue(shuffled, this.currentPlaylist?.name || t('music.shuffle', 'Shuffle'));
|
||||
musicPlayer.playTrack(0);
|
||||
@@ -488,11 +497,10 @@ const musicView = {
|
||||
|
||||
async _showCreatePlaylistDialog() {
|
||||
const t = (key, fallback = '') => {
|
||||
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
|
||||
return i18n?.t ? i18n.t(key) : fallback || key;
|
||||
};
|
||||
|
||||
if (!window.Modal) return;
|
||||
const name = await window.Modal.prompt({
|
||||
const name = await Modal.prompt({
|
||||
title: t('music.create_playlist', 'Create Playlist'),
|
||||
label: t('music.playlist_name', 'Playlist name'),
|
||||
placeholder: t('music.playlist_name', 'Playlist name'),
|
||||
@@ -506,7 +514,7 @@ const musicView = {
|
||||
|
||||
async _createPlaylist(name) {
|
||||
const t = (key, fallback = '') => {
|
||||
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
|
||||
return i18n?.t ? i18n.t(key) : fallback || key;
|
||||
};
|
||||
const createBtn = document.getElementById('music-create-playlist-btn');
|
||||
if (createBtn) createBtn.disabled = true;
|
||||
@@ -524,8 +532,8 @@ const musicView = {
|
||||
this.playlists.unshift(playlist);
|
||||
this._renderPlaylists();
|
||||
this._selectPlaylist(playlist.id);
|
||||
if (window.notifications) {
|
||||
window.notifications.addNotification({
|
||||
if (notifications) {
|
||||
notifications.addNotification({
|
||||
icon: 'fa-check-circle',
|
||||
iconClass: 'upload',
|
||||
title: t('music.create_playlist', 'Create Playlist'),
|
||||
@@ -534,8 +542,8 @@ const musicView = {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Create playlist error:', err);
|
||||
if (window.notifications) {
|
||||
window.notifications.addNotification({
|
||||
if (notifications) {
|
||||
notifications.addNotification({
|
||||
icon: 'fa-exclamation-circle',
|
||||
iconClass: 'error',
|
||||
title: t('music.error', 'Error'),
|
||||
@@ -549,17 +557,13 @@ const musicView = {
|
||||
|
||||
async _deletePlaylist() {
|
||||
const t = (key, fallback = '') => {
|
||||
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
|
||||
return i18n?.t ? i18n.t(key) : fallback || key;
|
||||
};
|
||||
|
||||
if (!this.currentPlaylist) return;
|
||||
|
||||
const confirmed = await new Promise((resolve) => {
|
||||
if (!window.Modal) {
|
||||
resolve(confirm(t('music.confirm_delete', 'Delete this playlist?')));
|
||||
return;
|
||||
}
|
||||
window.Modal.prompt({
|
||||
Modal.prompt({
|
||||
title: t('music.delete', 'Delete'),
|
||||
label: t('music.confirm_delete', 'Delete this playlist?'),
|
||||
placeholder: '',
|
||||
@@ -586,13 +590,13 @@ const musicView = {
|
||||
this.currentPlaylist = null;
|
||||
this.currentTracks = [];
|
||||
this._renderPlaylists();
|
||||
if (window.notifications) {
|
||||
window.notifications.addNotification({ icon: 'fa-check-circle', iconClass: 'upload', title: t('music.delete', 'Delete'), text: deletedName });
|
||||
if (notifications) {
|
||||
notifications.addNotification({ icon: 'fa-check-circle', iconClass: 'upload', title: t('music.delete', 'Delete'), text: deletedName });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Delete playlist error:', err);
|
||||
if (window.notifications) {
|
||||
window.notifications.addNotification({
|
||||
if (notifications) {
|
||||
notifications.addNotification({
|
||||
icon: 'fa-exclamation-circle',
|
||||
iconClass: 'error',
|
||||
title: t('music.error', 'Error'),
|
||||
@@ -641,11 +645,10 @@ const musicView = {
|
||||
async _showEditPlaylistDialog() {
|
||||
if (!this.currentPlaylist) return;
|
||||
const t = (key, fallback = '') => {
|
||||
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
|
||||
return i18n?.t ? i18n.t(key) : fallback || key;
|
||||
};
|
||||
|
||||
if (!window.Modal) return;
|
||||
const newName = await window.Modal.prompt({
|
||||
const newName = await Modal.prompt({
|
||||
title: t('music.edit', 'Edit'),
|
||||
label: t('music.playlist_name', 'Playlist name'),
|
||||
placeholder: t('music.playlist_name', 'Playlist name'),
|
||||
@@ -674,8 +677,8 @@ const musicView = {
|
||||
this._renderPlaylistList();
|
||||
} catch (err) {
|
||||
console.error('Edit playlist error:', err);
|
||||
if (window.notifications) {
|
||||
window.notifications.addNotification({
|
||||
if (notifications) {
|
||||
notifications.addNotification({
|
||||
icon: 'fa-exclamation-circle',
|
||||
iconClass: 'error',
|
||||
title: t('music.error', 'Error'),
|
||||
@@ -688,11 +691,10 @@ const musicView = {
|
||||
async _showSharePlaylistDialog() {
|
||||
if (!this.currentPlaylist) return;
|
||||
const t = (key, fallback = '') => {
|
||||
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
|
||||
return i18n?.t ? i18n.t(key) : fallback || key;
|
||||
};
|
||||
|
||||
if (!window.Modal) return;
|
||||
const userId = await window.Modal.prompt({
|
||||
const userId = await Modal.prompt({
|
||||
title: t('music.share', 'Share'),
|
||||
label: t('music.share_with_user', 'User ID or email'),
|
||||
placeholder: t('music.share_with_user', 'User ID or email'),
|
||||
@@ -711,8 +713,8 @@ const musicView = {
|
||||
|
||||
if (!resp.ok) throw new Error('Failed to share playlist');
|
||||
|
||||
if (window.notifications) {
|
||||
window.notifications.addNotification({
|
||||
if (notifications) {
|
||||
notifications.addNotification({
|
||||
icon: 'fa-check-circle',
|
||||
iconClass: 'upload',
|
||||
title: t('music.share', 'Share'),
|
||||
@@ -721,8 +723,8 @@ const musicView = {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Share playlist error:', err);
|
||||
if (window.notifications) {
|
||||
window.notifications.addNotification({
|
||||
if (notifications) {
|
||||
notifications.addNotification({
|
||||
icon: 'fa-exclamation-circle',
|
||||
iconClass: 'error',
|
||||
title: t('music.error', 'Error'),
|
||||
@@ -735,7 +737,7 @@ const musicView = {
|
||||
async _showAddTracksDialog() {
|
||||
if (!this.currentPlaylist) return;
|
||||
const t = (key, fallback = '') => {
|
||||
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
|
||||
return i18n?.t ? i18n.t(key) : fallback || key;
|
||||
};
|
||||
|
||||
// ── Build modal overlay ──
|
||||
@@ -813,7 +815,7 @@ const musicView = {
|
||||
for (const file of files) {
|
||||
const row = document.createElement('label');
|
||||
row.className = `music-picker-item${selectedIds.has(file.id) ? ' selected' : ''}`;
|
||||
const sizeStr = file.size != null && window.formatFileSize ? window.formatFileSize(file.size) : '';
|
||||
const sizeStr = file.size != null && formatFileSize ? formatFileSize(file.size) : '';
|
||||
row.innerHTML = `
|
||||
<input type="checkbox" value="${file.id}" ${selectedIds.has(file.id) ? 'checked' : ''}>
|
||||
<i class="fas fa-file-audio"></i>
|
||||
@@ -858,8 +860,8 @@ const musicView = {
|
||||
});
|
||||
if (!resp.ok) throw new Error('Failed to add tracks');
|
||||
|
||||
if (window.notifications) {
|
||||
window.notifications.addNotification({
|
||||
if (notifications) {
|
||||
notifications.addNotification({
|
||||
icon: 'fa-check-circle',
|
||||
iconClass: 'upload',
|
||||
title: t('music.add_tracks', 'Add Tracks'),
|
||||
@@ -878,8 +880,8 @@ const musicView = {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Add tracks error:', err);
|
||||
if (window.notifications) {
|
||||
window.notifications.addNotification({
|
||||
if (notifications) {
|
||||
notifications.addNotification({
|
||||
icon: 'fa-exclamation-circle',
|
||||
iconClass: 'error',
|
||||
title: t('music.error', 'Error'),
|
||||
@@ -898,7 +900,7 @@ const musicView = {
|
||||
|
||||
async _removeTrackFromPlaylist(_trackId, fileId) {
|
||||
if (!this.currentPlaylist) return;
|
||||
const t = (key, fallback = '') => (typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key);
|
||||
const t = (key, fallback = '') => (i18n?.t ? i18n.t(key) : fallback || key);
|
||||
|
||||
try {
|
||||
const resp = await fetch(`/api/playlists/${this.currentPlaylist.id}/tracks/${encodeURIComponent(fileId)}`, {
|
||||
@@ -908,8 +910,8 @@ const musicView = {
|
||||
});
|
||||
if (!resp.ok) throw new Error('Failed to remove track');
|
||||
|
||||
if (window.notifications) {
|
||||
window.notifications.addNotification({
|
||||
if (notifications) {
|
||||
notifications.addNotification({
|
||||
icon: 'fa-check-circle',
|
||||
iconClass: 'upload',
|
||||
title: t('music.remove', 'Remove'),
|
||||
@@ -927,8 +929,8 @@ const musicView = {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Remove track error:', err);
|
||||
if (window.notifications) {
|
||||
window.notifications.addNotification({
|
||||
if (notifications) {
|
||||
notifications.addNotification({
|
||||
icon: 'fa-exclamation-circle',
|
||||
iconClass: 'error',
|
||||
title: t('music.error', 'Error'),
|
||||
@@ -940,7 +942,7 @@ const musicView = {
|
||||
|
||||
async _reorderTrack(fromIdx, toIdx) {
|
||||
if (!this.currentPlaylist) return;
|
||||
const t = (key, fallback = '') => (typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key);
|
||||
const t = (key, fallback = '') => (i18n?.t ? i18n.t(key) : fallback || key);
|
||||
|
||||
const tracks = [...this.currentTracks];
|
||||
const [moved] = tracks.splice(fromIdx, 1);
|
||||
@@ -959,8 +961,8 @@ const musicView = {
|
||||
if (!resp.ok) throw new Error('Failed to reorder tracks');
|
||||
} catch (err) {
|
||||
console.error('Reorder error:', err);
|
||||
if (window.notifications) {
|
||||
window.notifications.addNotification({
|
||||
if (notifications) {
|
||||
notifications.addNotification({
|
||||
icon: 'fa-exclamation-circle',
|
||||
iconClass: 'error',
|
||||
title: t('music.error', 'Error'),
|
||||
@@ -973,7 +975,7 @@ const musicView = {
|
||||
|
||||
async _showManageSharesDialog() {
|
||||
if (!this.currentPlaylist) return;
|
||||
const t = (key, fallback = '') => (typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key);
|
||||
const t = (key, fallback = '') => (i18n?.t ? i18n.t(key) : fallback || key);
|
||||
|
||||
const existing = document.getElementById('music-shares-dialog');
|
||||
if (existing) existing.remove();
|
||||
@@ -1025,8 +1027,8 @@ const musicView = {
|
||||
userInput.value = '';
|
||||
writeInput.checked = false;
|
||||
this._loadSharesList(dialog);
|
||||
if (window.notifications) {
|
||||
window.notifications.addNotification({
|
||||
if (notifications) {
|
||||
notifications.addNotification({
|
||||
icon: 'fa-check-circle',
|
||||
iconClass: 'upload',
|
||||
title: t('music.share', 'Share'),
|
||||
@@ -1034,8 +1036,8 @@ const musicView = {
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
if (window.notifications) {
|
||||
window.notifications.addNotification({
|
||||
if (notifications) {
|
||||
notifications.addNotification({
|
||||
icon: 'fa-exclamation-circle',
|
||||
iconClass: 'error',
|
||||
title: t('music.error', 'Error'),
|
||||
@@ -1050,7 +1052,7 @@ const musicView = {
|
||||
|
||||
async _loadSharesList(dialog) {
|
||||
if (!this.currentPlaylist) return;
|
||||
const t = (key, fallback = '') => (typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key);
|
||||
const t = (key, fallback = '') => (i18n?.t ? i18n.t(key) : fallback || key);
|
||||
const body = dialog.querySelector('.music-shares-body');
|
||||
if (!body) return;
|
||||
|
||||
@@ -1095,7 +1097,7 @@ const musicView = {
|
||||
|
||||
async _removeShare(userId, dialog) {
|
||||
if (!this.currentPlaylist) return;
|
||||
const t = (key, fallback = '') => (typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key);
|
||||
const t = (key, fallback = '') => (i18n?.t ? i18n.t(key) : fallback || key);
|
||||
|
||||
try {
|
||||
const resp = await fetch(`/api/playlists/${this.currentPlaylist.id}/share/${encodeURIComponent(userId)}`, {
|
||||
@@ -1106,8 +1108,8 @@ const musicView = {
|
||||
if (!resp.ok) throw new Error('Failed to remove share');
|
||||
this._loadSharesList(dialog);
|
||||
} catch (err) {
|
||||
if (window.notifications) {
|
||||
window.notifications.addNotification({
|
||||
if (notifications) {
|
||||
notifications.addNotification({
|
||||
icon: 'fa-exclamation-circle',
|
||||
iconClass: 'error',
|
||||
title: t('music.error', 'Error'),
|
||||
@@ -1119,7 +1121,7 @@ const musicView = {
|
||||
|
||||
async _togglePublic() {
|
||||
if (!this.currentPlaylist) return;
|
||||
const t = (key, fallback = '') => (typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key);
|
||||
const t = (key, fallback = '') => (i18n?.t ? i18n.t(key) : fallback || key);
|
||||
const newValue = !this.currentPlaylist.is_public;
|
||||
|
||||
try {
|
||||
@@ -1144,9 +1146,9 @@ const musicView = {
|
||||
btn.classList.toggle('active', newValue);
|
||||
}
|
||||
|
||||
if (window.notifications) {
|
||||
if (notifications) {
|
||||
const status = newValue ? t('music.public', 'Public') : t('music.private', 'Private');
|
||||
window.notifications.addNotification({
|
||||
notifications.addNotification({
|
||||
icon: 'fa-check-circle',
|
||||
iconClass: 'upload',
|
||||
title: t('music.toggle_public', 'Visibility'),
|
||||
@@ -1155,8 +1157,8 @@ const musicView = {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Toggle public error:', err);
|
||||
if (window.notifications) {
|
||||
window.notifications.addNotification({
|
||||
if (notifications) {
|
||||
notifications.addNotification({
|
||||
icon: 'fa-exclamation-circle',
|
||||
iconClass: 'error',
|
||||
title: t('music.error', 'Error'),
|
||||
@@ -1168,7 +1170,7 @@ const musicView = {
|
||||
|
||||
async _showCoverPicker() {
|
||||
if (!this.currentPlaylist) return;
|
||||
const t = (key, fallback = '') => (typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key);
|
||||
const t = (key, fallback = '') => (i18n?.t ? i18n.t(key) : fallback || key);
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
@@ -1184,13 +1186,13 @@ const musicView = {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
const folderId = window.app?.currentPath || window.app?.userHomeFolderId || '';
|
||||
const folderId = app?.currentPath || app?.userHomeFolderId || '';
|
||||
formData.append('folder_id', folderId);
|
||||
|
||||
const uploadResp = await fetch('/api/files/upload', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: typeof getCsrfHeaders === 'function' ? getCsrfHeaders() : {},
|
||||
headers: getCsrfHeaders(),
|
||||
body: formData
|
||||
});
|
||||
if (!uploadResp.ok) throw new Error('Upload failed');
|
||||
@@ -1214,8 +1216,8 @@ const musicView = {
|
||||
coverEl.innerHTML = `<img src="/api/files/${encodeURIComponent(uploaded.id)}" alt="" class="music-cover-img"><div class="music-cover-overlay"><i class="fas fa-camera"></i></div>`;
|
||||
}
|
||||
|
||||
if (window.notifications) {
|
||||
window.notifications.addNotification({
|
||||
if (notifications) {
|
||||
notifications.addNotification({
|
||||
icon: 'fa-check-circle',
|
||||
iconClass: 'upload',
|
||||
title: t('music.set_cover', 'Set cover'),
|
||||
@@ -1224,8 +1226,8 @@ const musicView = {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Cover upload error:', err);
|
||||
if (window.notifications) {
|
||||
window.notifications.addNotification({
|
||||
if (notifications) {
|
||||
notifications.addNotification({
|
||||
icon: 'fa-exclamation-circle',
|
||||
iconClass: 'error',
|
||||
title: t('music.error', 'Error'),
|
||||
@@ -1647,11 +1649,11 @@ const musicPlayer = {
|
||||
this.isPlaying = false;
|
||||
this._updateUI();
|
||||
const t = (key, fallback = '') => {
|
||||
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
|
||||
return i18n?.t ? i18n.t(key) : fallback || key;
|
||||
};
|
||||
if (window.notifications) {
|
||||
if (notifications) {
|
||||
const trackName = this.currentTrack?.title || this.currentTrack?.file_name || t('music.unknown_title', 'Unknown');
|
||||
window.notifications.addNotification({
|
||||
notifications.addNotification({
|
||||
icon: 'fa-exclamation-circle',
|
||||
iconClass: 'error',
|
||||
title: t('music.error', 'Error'),
|
||||
@@ -1670,8 +1672,8 @@ const musicPlayer = {
|
||||
if (icon) {
|
||||
const iconName = this.isPlaying ? 'pause' : 'play';
|
||||
const extraClass = 'player-btn-main';
|
||||
if (window.oxiIcon) {
|
||||
icon.outerHTML = window.oxiIcon(iconName, extraClass);
|
||||
if (oxiIcon) {
|
||||
icon.outerHTML = oxiIcon(iconName, extraClass);
|
||||
} else {
|
||||
icon.className = `fas fa-${iconName} ${extraClass}`;
|
||||
}
|
||||
@@ -1680,7 +1682,7 @@ const musicPlayer = {
|
||||
|
||||
if (trackName) {
|
||||
const t = (key, fallback = '') => {
|
||||
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
|
||||
return i18n?.t ? i18n.t(key) : fallback || key;
|
||||
};
|
||||
trackName.textContent = this.currentTrack
|
||||
? this.currentTrack.title || this.currentTrack.file_name || t('music.unknown_title', 'Unknown')
|
||||
@@ -1704,8 +1706,8 @@ const musicPlayer = {
|
||||
if (playIcon) playIcon.classList.remove('hidden');
|
||||
if (playIcon) {
|
||||
const iconName = this.isPlaying ? 'pause' : 'play';
|
||||
if (window.oxiIcon) {
|
||||
playIcon.outerHTML = window.oxiIcon(iconName, 'track-play-icon');
|
||||
if (oxiIcon) {
|
||||
playIcon.outerHTML = oxiIcon(iconName, 'track-play-icon');
|
||||
} else {
|
||||
playIcon.className = `fas fa-${iconName} track-play-icon`;
|
||||
}
|
||||
@@ -1735,7 +1737,7 @@ const musicPlayer = {
|
||||
if (!queueList) return;
|
||||
|
||||
const t = (key, fallback = '') => {
|
||||
return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key;
|
||||
return i18n?.t ? i18n.t(key) : fallback || key;
|
||||
};
|
||||
|
||||
if (this.queue.length === 0) {
|
||||
@@ -1838,4 +1840,4 @@ const musicPlayer = {
|
||||
}
|
||||
};
|
||||
|
||||
window.musicView = musicView;
|
||||
export { musicView };
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
* Photo grid grouped by day/month/year, with infinite scroll and multi-select.
|
||||
*/
|
||||
|
||||
import { getCsrfHeaders } from '../../core/csrf.js';
|
||||
import { i18n } from '../../core/i18n.js';
|
||||
import { photosLightbox } from './photosLightbox.js';
|
||||
|
||||
const photosView = {
|
||||
/** @type {Array} All loaded photo items */
|
||||
items: [],
|
||||
@@ -37,7 +41,7 @@ const photosView = {
|
||||
|
||||
/** Auth headers (HttpOnly cookies) */
|
||||
_headers(json = false) {
|
||||
const h = typeof getCsrfHeaders === 'function' ? { ...getCsrfHeaders() } : {};
|
||||
const h = getCsrfHeaders();
|
||||
if (json) h['Content-Type'] = 'application/json';
|
||||
return h;
|
||||
},
|
||||
@@ -406,7 +410,7 @@ const photosView = {
|
||||
|
||||
/** Render the group mode toolbar */
|
||||
_renderToolbar() {
|
||||
const t = (k, d) => (window.i18n ? window.i18n.t(k) : d);
|
||||
const t = (k, d) => (i18n ? i18n.t(k) : d);
|
||||
const modes = [
|
||||
['daily', t('photos.view_daily', 'Day')],
|
||||
['monthly', t('photos.view_monthly', 'Month')],
|
||||
@@ -423,7 +427,7 @@ const photosView = {
|
||||
|
||||
/** Render empty state */
|
||||
_renderEmpty() {
|
||||
const t = (k, d) => (window.i18n ? window.i18n.t(k) : d);
|
||||
const t = (k, d) => (i18n ? i18n.t(k) : d);
|
||||
this._container.innerHTML = `
|
||||
<div class="photos-empty">
|
||||
<i class="fas fa-images"></i>
|
||||
@@ -483,8 +487,8 @@ const photosView = {
|
||||
|
||||
// Otherwise open lightbox
|
||||
const idx = this.items.findIndex((f) => f.id === id);
|
||||
if (idx >= 0 && window.photosLightbox) {
|
||||
window.photosLightbox.open(this.items, idx);
|
||||
if (idx >= 0) {
|
||||
photosLightbox.open(this.items, idx);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -516,7 +520,7 @@ const photosView = {
|
||||
document.body.appendChild(bar);
|
||||
}
|
||||
|
||||
const t = (k, d) => (window.i18n ? window.i18n.t(k) : d);
|
||||
const t = (k, d) => (i18n ? i18n.t(k) : d);
|
||||
const count = this.selected.size;
|
||||
bar.innerHTML = `
|
||||
<span class="selection-count">${count} ${t('photos.items_selected', 'selected')}</span>
|
||||
@@ -605,4 +609,6 @@ const photosView = {
|
||||
}
|
||||
};
|
||||
|
||||
window.photosView = photosView;
|
||||
photosLightbox.setPhotosView(photosView);
|
||||
|
||||
export { photosView };
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
* Full-screen image/video viewer with prev/next navigation.
|
||||
*/
|
||||
|
||||
const photosLightbox = {
|
||||
import { getCsrfHeaders } from '../../core/csrf.js';
|
||||
import { favorites } from '../library/favorites.js';
|
||||
|
||||
export const photosLightbox = {
|
||||
/** @type {Array} Items array reference */
|
||||
items: [],
|
||||
/** @type {number} Current index */
|
||||
@@ -14,10 +17,20 @@ const photosLightbox = {
|
||||
_blobUrl: null,
|
||||
/** @type {Function|null} */
|
||||
_keyHandler: null,
|
||||
/** @type {Object|null} Reference to photosView, set after both modules load */
|
||||
_photosView: null,
|
||||
|
||||
/**
|
||||
* Register the photosView reference (called from photos.js to avoid circular imports).
|
||||
* @param {Object} pv
|
||||
*/
|
||||
setPhotosView(pv) {
|
||||
this._photosView = pv;
|
||||
},
|
||||
|
||||
/** Auth headers */
|
||||
_headers() {
|
||||
return typeof getCsrfHeaders === 'function' ? { ...getCsrfHeaders() } : {};
|
||||
return getCsrfHeaders();
|
||||
},
|
||||
|
||||
/** Open lightbox at given index */
|
||||
@@ -202,7 +215,7 @@ const photosLightbox = {
|
||||
/** Toggle favorite on current item */
|
||||
async _toggleFavorite() {
|
||||
const item = this.items[this.index];
|
||||
if (!item || !window.favorites) return;
|
||||
if (!item || !favorites) return;
|
||||
try {
|
||||
await fetch(`/api/favorites/file/${item.id}`, {
|
||||
method: 'POST',
|
||||
@@ -235,17 +248,17 @@ const photosLightbox = {
|
||||
headers: this._headers()
|
||||
});
|
||||
// Remove from photosView items too
|
||||
if (window.photosView) {
|
||||
window.photosView.items = window.photosView.items.filter((f) => f.id !== item.id);
|
||||
if (this._photosView) {
|
||||
this._photosView.items = this._photosView.items.filter((f) => f.id !== item.id);
|
||||
}
|
||||
this.items.splice(this.index, 1);
|
||||
if (this.items.length === 0) {
|
||||
this.close();
|
||||
if (window.photosView) window.photosView._render();
|
||||
if (this._photosView) this._photosView._render();
|
||||
} else {
|
||||
if (this.index >= this.items.length) this.index = this.items.length - 1;
|
||||
this._show();
|
||||
if (window.photosView) window.photosView._render();
|
||||
if (this._photosView) this._photosView._render();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Delete failed:', err);
|
||||
@@ -282,5 +295,3 @@ const photosLightbox = {
|
||||
.replace(/</g, '<');
|
||||
}
|
||||
};
|
||||
|
||||
window.photosLightbox = photosLightbox;
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
* No localStorage usage — the server persists and prunes recent items.
|
||||
*/
|
||||
|
||||
import { ui } from '../../app/ui.js';
|
||||
import { getCsrfHeaders } from '../../core/csrf.js';
|
||||
import { i18n } from '../../core/i18n.js';
|
||||
import { multiSelect } from '../files/multiSelect.js';
|
||||
|
||||
const recent = {
|
||||
/** Maximum items to request from the server */
|
||||
MAX_RECENT_FILES: 20,
|
||||
@@ -86,7 +91,7 @@ const recent = {
|
||||
|
||||
const recentItems = await response.json();
|
||||
|
||||
window.ui.resetFilesList(); // ensure also list visible & error hidden
|
||||
ui.resetFilesList(); // ensure also list visible & error hidden
|
||||
const filesList = document.getElementById('files-list');
|
||||
|
||||
filesList.innerHTML = `
|
||||
@@ -100,17 +105,17 @@ const recent = {
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (window.multiSelect) {
|
||||
window.multiSelect.clear();
|
||||
window.multiSelect.init(); // this will wire buttons & select-all-checkbox
|
||||
if (multiSelect) {
|
||||
multiSelect.clear();
|
||||
multiSelect.init(); // this will wire buttons & select-all-checkbox
|
||||
}
|
||||
window.ui.updateBreadcrumb('');
|
||||
ui.updateBreadcrumb('');
|
||||
|
||||
if (recentItems.length === 0) {
|
||||
window.ui.showError(`
|
||||
ui.showError(`
|
||||
<i class="fas fa-clock empty-state-icon"></i>
|
||||
<p>${window.i18n ? window.i18n.t('recent.empty_state') : 'No recent files'}</p>
|
||||
<p>${window.i18n ? window.i18n.t('recent.empty_hint') : 'Files you open will appear here'}</p>
|
||||
<p>${i18n ? i18n.t('recent.empty_state') : 'No recent files'}</p>
|
||||
<p>${i18n ? i18n.t('recent.empty_hint') : 'Files you open will appear here'}</p>
|
||||
`);
|
||||
}
|
||||
|
||||
@@ -140,16 +145,15 @@ const recent = {
|
||||
});
|
||||
}
|
||||
}
|
||||
if (folders.length) window.ui.renderFolders(folders);
|
||||
if (files.length) window.ui.renderFiles(files);
|
||||
if (folders.length) ui.renderFolders(folders);
|
||||
if (files.length) ui.renderFiles(files);
|
||||
} catch (error) {
|
||||
console.error('Error displaying recent files:', error);
|
||||
if (window.ui?.showNotification) {
|
||||
window.ui.showNotification('Error', 'Error loading recent files');
|
||||
if (ui?.showNotification) {
|
||||
ui.showNotification('Error', 'Error loading recent files');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Expose globally
|
||||
window.recent = recent;
|
||||
export { recent };
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
* No localStorage is used for share data.
|
||||
*/
|
||||
|
||||
import { switchToSharedSection } from '../../app/navigation.js';
|
||||
import { ui } from '../../app/ui.js';
|
||||
import { getCsrfHeaders } from '../../core/csrf.js';
|
||||
import { formatDateTime } from '../../core/formatters.js';
|
||||
|
||||
const fileSharing = {
|
||||
/** Auth header helper — tokens are in HttpOnly cookies now */
|
||||
_headers(json = true) {
|
||||
@@ -149,11 +154,11 @@ const fileSharing = {
|
||||
async copyLinkToClipboard(url) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(url);
|
||||
window.ui.showNotification('Link copied', 'Link copied to clipboard');
|
||||
ui.showNotification('Link copied', 'Link copied to clipboard');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error copying to clipboard:', error);
|
||||
window.ui.showNotification('Error', 'Could not copy link');
|
||||
ui.showNotification('Error', 'Could not copy link');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
@@ -165,7 +170,7 @@ const fileSharing = {
|
||||
*/
|
||||
formatExpirationDate(value) {
|
||||
if (!value) return 'No expiration';
|
||||
return window.formatDateTime(value);
|
||||
return formatDateTime(value);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -178,9 +183,7 @@ const fileSharing = {
|
||||
async sendShareNotification(shareUrl, recipientEmail, _message = '') {
|
||||
// TODO: implement backend endpoint for email notifications
|
||||
console.log(`Share notification for ${shareUrl} sent to ${recipientEmail}`);
|
||||
if (window.ui) {
|
||||
window.ui.showNotification('Notification sent', `Notification sent to ${recipientEmail}`);
|
||||
}
|
||||
ui.showNotification('Notification sent', `Notification sent to ${recipientEmail}`);
|
||||
return true;
|
||||
},
|
||||
|
||||
@@ -193,20 +196,11 @@ const fileSharing = {
|
||||
const span = item.querySelector('span');
|
||||
if (span && span.getAttribute('data-i18n') === 'nav.shared') {
|
||||
item.addEventListener('click', () => {
|
||||
if (window.switchToSharedSection) {
|
||||
window.switchToSharedSection();
|
||||
}
|
||||
switchToSharedSection();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Expose module globally
|
||||
window.fileSharing = fileSharing;
|
||||
|
||||
// Global convenience functions that delegate to the module
|
||||
window.getSharedLinks = () => fileSharing.getSharedLinks();
|
||||
window.updateSharedLink = (id, data) => fileSharing.updateSharedLink(id, data);
|
||||
window.removeSharedLink = (id) => fileSharing.removeSharedLink(id);
|
||||
window.sendShareNotification = (url, email, msg) => fileSharing.sendShareNotification(url, email, msg);
|
||||
export { fileSharing };
|
||||
|
||||
Reference in New Issue
Block a user