2025-03-19 23:28:29 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* OxiCloud - UI Module
|
|
|
|
|
|
* This file handles UI-related functions, view toggling, and interface interactions
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
2026-03-25 18:49:38 +01:00
|
|
|
|
// @ts-check
|
|
|
|
|
|
|
2025-03-19 23:28:29 +01:00
|
|
|
|
// UI Module
|
|
|
|
|
|
const ui = {
|
2026-04-03 17:58:14 +02:00
|
|
|
|
/** @type {HTMLDListElement | null} */
|
|
|
|
|
|
//dragPreview,
|
|
|
|
|
|
|
2025-03-19 23:28:29 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* Initialize context menus and dialogs
|
|
|
|
|
|
*/
|
|
|
|
|
|
initializeContextMenus() {
|
|
|
|
|
|
// Folder context menu
|
|
|
|
|
|
if (!document.getElementById('folder-context-menu')) {
|
|
|
|
|
|
const folderMenu = document.createElement('div');
|
|
|
|
|
|
folderMenu.className = 'context-menu';
|
|
|
|
|
|
folderMenu.id = 'folder-context-menu';
|
|
|
|
|
|
folderMenu.innerHTML = `
|
2025-04-02 01:22:05 +02:00
|
|
|
|
<div class="context-menu-item" id="download-folder-option">
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<i class="fas fa-download"></i> <span data-i18n="actions.download">Download</span>
|
2025-04-02 01:22:05 +02:00
|
|
|
|
</div>
|
2025-04-02 03:43:44 +02:00
|
|
|
|
<div class="context-menu-item" id="favorite-folder-option">
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<i class="fas fa-star"></i> <span data-i18n="actions.favorite">Add to favorites</span>
|
2025-04-02 03:43:44 +02:00
|
|
|
|
</div>
|
2026-02-08 22:44:42 +01:00
|
|
|
|
<div class="context-menu-item" id="share-folder-option">
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<i class="fas fa-share-alt"></i> <span data-i18n="actions.share">Share</span>
|
2026-02-08 22:44:42 +01:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="context-menu-separator"></div>
|
2025-03-19 23:28:29 +01:00
|
|
|
|
<div class="context-menu-item" id="rename-folder-option">
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<i class="fas fa-pen"></i> <span data-i18n="actions.rename">Rename</span>
|
2025-03-19 23:28:29 +01:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="context-menu-item" id="move-folder-option">
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<i class="fas fa-arrows-alt"></i> <span data-i18n="actions.move">Move to...</span>
|
2025-03-19 23:28:29 +01:00
|
|
|
|
</div>
|
2026-02-08 22:44:42 +01:00
|
|
|
|
<div class="context-menu-separator"></div>
|
|
|
|
|
|
<div class="context-menu-item context-menu-item-danger" id="delete-folder-option">
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<i class="fas fa-trash-alt"></i> <span data-i18n="actions.delete">Delete</span>
|
2025-03-19 23:28:29 +01:00
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
document.body.appendChild(folderMenu);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// File context menu
|
|
|
|
|
|
if (!document.getElementById('file-context-menu')) {
|
|
|
|
|
|
const fileMenu = document.createElement('div');
|
|
|
|
|
|
fileMenu.className = 'context-menu';
|
|
|
|
|
|
fileMenu.id = 'file-context-menu';
|
|
|
|
|
|
fileMenu.innerHTML = `
|
2025-04-02 05:08:30 +02:00
|
|
|
|
<div class="context-menu-item" id="view-file-option">
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<i class="fas fa-eye"></i> <span data-i18n="actions.view">View</span>
|
2025-04-02 05:08:30 +02:00
|
|
|
|
</div>
|
2026-03-05 16:18:30 -05:00
|
|
|
|
<div class="context-menu-item hidden" id="wopi-edit-file-option">
|
2026-02-21 13:39:27 +01:00
|
|
|
|
<i class="fas fa-file-word"></i> <span>Edit in Office</span>
|
|
|
|
|
|
</div>
|
2026-03-05 16:18:30 -05:00
|
|
|
|
<div class="context-menu-item hidden" id="wopi-edit-file-tab-option">
|
2026-02-21 13:39:27 +01:00
|
|
|
|
<i class="fas fa-external-link-alt"></i> <span>Edit in Office (new tab)</span>
|
|
|
|
|
|
</div>
|
2025-04-02 01:22:05 +02:00
|
|
|
|
<div class="context-menu-item" id="download-file-option">
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<i class="fas fa-download"></i> <span data-i18n="actions.download">Download</span>
|
2025-04-02 01:22:05 +02:00
|
|
|
|
</div>
|
2026-02-08 22:44:42 +01:00
|
|
|
|
<div class="context-menu-separator"></div>
|
2025-04-02 03:43:44 +02:00
|
|
|
|
<div class="context-menu-item" id="favorite-file-option">
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<i class="fas fa-star"></i> <span data-i18n="actions.favorite">Add to favorites</span>
|
2025-04-02 03:43:44 +02:00
|
|
|
|
</div>
|
2025-03-28 08:09:18 +01:00
|
|
|
|
<div class="context-menu-item" id="share-file-option">
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<i class="fas fa-share-alt"></i> <span data-i18n="actions.share">Share</span>
|
2025-03-28 08:09:18 +01:00
|
|
|
|
</div>
|
2026-02-08 22:44:42 +01:00
|
|
|
|
<div class="context-menu-separator"></div>
|
|
|
|
|
|
<div class="context-menu-item" id="rename-file-option">
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<i class="fas fa-pen"></i> <span data-i18n="actions.rename">Rename</span>
|
2026-02-08 22:44:42 +01:00
|
|
|
|
</div>
|
2025-03-19 23:28:29 +01:00
|
|
|
|
<div class="context-menu-item" id="move-file-option">
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<i class="fas fa-arrows-alt"></i> <span data-i18n="actions.move">Move to...</span>
|
2025-03-19 23:28:29 +01:00
|
|
|
|
</div>
|
2026-02-08 22:44:42 +01:00
|
|
|
|
<div class="context-menu-separator"></div>
|
|
|
|
|
|
<div class="context-menu-item context-menu-item-danger" id="delete-file-option">
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<i class="fas fa-trash-alt"></i> <span data-i18n="actions.delete">Delete</span>
|
2025-03-19 23:28:29 +01:00
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
document.body.appendChild(fileMenu);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-08 22:44:42 +01:00
|
|
|
|
// Rename dialog — modern
|
2025-03-19 23:28:29 +01:00
|
|
|
|
if (!document.getElementById('rename-dialog')) {
|
|
|
|
|
|
const renameDialog = document.createElement('div');
|
|
|
|
|
|
renameDialog.className = 'rename-dialog';
|
|
|
|
|
|
renameDialog.id = 'rename-dialog';
|
|
|
|
|
|
renameDialog.innerHTML = `
|
|
|
|
|
|
<div class="rename-dialog-content">
|
2026-02-08 22:44:42 +01:00
|
|
|
|
<div class="rename-dialog-header">
|
2026-03-05 16:18:30 -05:00
|
|
|
|
<i class="fas fa-pen dialog-header-icon"></i>
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<span data-i18n="dialogs.rename_folder">Rename</span>
|
2026-02-08 22:44:42 +01:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="rename-dialog-body">
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<input type="text" id="rename-input" data-i18n-placeholder="dialogs.new_name" placeholder="New name">
|
2026-02-08 22:44:42 +01:00
|
|
|
|
</div>
|
2025-03-19 23:28:29 +01:00
|
|
|
|
<div class="rename-dialog-buttons">
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<button class="btn btn-secondary" id="rename-cancel-btn" data-i18n="actions.cancel">Cancel</button>
|
|
|
|
|
|
<button class="btn btn-primary" id="rename-confirm-btn" data-i18n="actions.rename">Rename</button>
|
2025-03-19 23:28:29 +01:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
document.body.appendChild(renameDialog);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 17:56:25 -08:00
|
|
|
|
// Move dialog — modern with navigation
|
2025-03-19 23:28:29 +01:00
|
|
|
|
if (!document.getElementById('move-file-dialog')) {
|
|
|
|
|
|
const moveDialog = document.createElement('div');
|
|
|
|
|
|
moveDialog.className = 'rename-dialog';
|
|
|
|
|
|
moveDialog.id = 'move-file-dialog';
|
|
|
|
|
|
moveDialog.innerHTML = `
|
|
|
|
|
|
<div class="rename-dialog-content">
|
2026-02-08 22:44:42 +01:00
|
|
|
|
<div class="rename-dialog-header">
|
2026-03-05 16:18:30 -05:00
|
|
|
|
<i class="fas fa-arrows-alt dialog-header-icon"></i>
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<span data-i18n="dialogs.move_file">Move</span>
|
2026-02-08 22:44:42 +01:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="rename-dialog-body">
|
2026-03-05 16:18:30 -05:00
|
|
|
|
<p class="move-dialog-hint" data-i18n="dialogs.select_destination">Select destination folder:</p>
|
2026-02-24 17:56:25 -08:00
|
|
|
|
<div id="move-dialog-breadcrumb" class="move-dialog-breadcrumb"></div>
|
2026-03-05 16:18:30 -05:00
|
|
|
|
<div id="folder-select-container" class="folder-select-container">
|
2025-03-19 23:28:29 +01:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="rename-dialog-buttons">
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<button class="btn btn-secondary" id="move-cancel-btn" data-i18n="actions.cancel">Cancel</button>
|
2026-02-24 18:08:33 -08:00
|
|
|
|
<button class="btn btn-outline" id="copy-confirm-btn" data-i18n="actions.copy">Copy</button>
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<button class="btn btn-primary" id="move-confirm-btn" data-i18n="actions.move_to">Move</button>
|
2025-03-19 23:28:29 +01:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
document.body.appendChild(moveDialog);
|
|
|
|
|
|
}
|
2026-02-22 20:20:00 -08:00
|
|
|
|
|
2025-03-28 08:09:18 +01:00
|
|
|
|
// Share dialog
|
|
|
|
|
|
if (!document.getElementById('share-dialog')) {
|
|
|
|
|
|
const shareDialog = document.createElement('div');
|
|
|
|
|
|
shareDialog.className = 'share-dialog';
|
|
|
|
|
|
shareDialog.id = 'share-dialog';
|
|
|
|
|
|
shareDialog.innerHTML = `
|
|
|
|
|
|
<div class="share-dialog-content">
|
2026-02-08 22:44:42 +01:00
|
|
|
|
<div class="share-dialog-header">
|
2026-03-05 16:18:30 -05:00
|
|
|
|
<i class="fas fa-share-alt dialog-header-icon"></i>
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<span data-i18n="dialogs.share_file">Share file</span>
|
2026-02-08 22:44:42 +01:00
|
|
|
|
</div>
|
2025-03-28 08:09:18 +01:00
|
|
|
|
<div class="shared-item-info">
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<strong>Item:</strong> <span id="shared-item-name"></span>
|
2025-03-28 08:09:18 +01:00
|
|
|
|
</div>
|
2026-02-22 20:20:00 -08:00
|
|
|
|
|
2026-03-05 16:18:30 -05:00
|
|
|
|
<div id="existing-shares-section" class="share-section hidden">
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<h3 data-i18n="dialogs.existing_shares">Existing shared links</h3>
|
2025-03-28 08:09:18 +01:00
|
|
|
|
<div id="existing-shares-container"></div>
|
|
|
|
|
|
</div>
|
2026-02-22 20:20:00 -08:00
|
|
|
|
|
2025-03-28 08:09:18 +01:00
|
|
|
|
<div class="share-options">
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<h3 data-i18n="dialogs.share_options">Share options</h3>
|
2026-02-22 20:20:00 -08:00
|
|
|
|
|
2025-03-28 08:09:18 +01:00
|
|
|
|
<div class="form-group">
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<label for="share-password" data-i18n="dialogs.password">Password (optional):</label>
|
|
|
|
|
|
<input type="password" id="share-password" placeholder="Protect with password">
|
2025-03-28 08:09:18 +01:00
|
|
|
|
</div>
|
2026-02-22 20:20:00 -08:00
|
|
|
|
|
2025-03-28 08:09:18 +01:00
|
|
|
|
<div class="form-group">
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<label for="share-expiration" data-i18n="dialogs.expiration">Expiration date (optional):</label>
|
2025-03-28 08:09:18 +01:00
|
|
|
|
<input type="date" id="share-expiration">
|
|
|
|
|
|
</div>
|
2026-02-22 20:20:00 -08:00
|
|
|
|
|
2025-03-28 08:09:18 +01:00
|
|
|
|
<div class="form-group">
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<label data-i18n="dialogs.permissions">Permissions:</label>
|
2025-03-28 08:09:18 +01:00
|
|
|
|
<div class="permission-options">
|
|
|
|
|
|
<div class="permission-option">
|
|
|
|
|
|
<input type="checkbox" id="share-permission-read" checked>
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<label for="share-permission-read" data-i18n="permissions.read">Read</label>
|
2025-03-28 08:09:18 +01:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="permission-option">
|
|
|
|
|
|
<input type="checkbox" id="share-permission-write">
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<label for="share-permission-write" data-i18n="permissions.write">Write</label>
|
2025-03-28 08:09:18 +01:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="permission-option">
|
|
|
|
|
|
<input type="checkbox" id="share-permission-reshare">
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<label for="share-permission-reshare" data-i18n="permissions.reshare">Allow sharing</label>
|
2025-03-28 08:09:18 +01:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-02-22 20:20:00 -08:00
|
|
|
|
|
2026-03-05 16:18:30 -05:00
|
|
|
|
<div id="new-share-section" class="share-section hidden">
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<h3 data-i18n="dialogs.generated_link">Generated link</h3>
|
2025-03-28 08:09:18 +01:00
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<input type="text" id="generated-share-url" readonly>
|
|
|
|
|
|
<div class="share-link-actions">
|
|
|
|
|
|
<button class="btn btn-small" id="copy-share-btn">
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<i class="fas fa-copy"></i> <span data-i18n="actions.copy">Copy</span>
|
2025-03-28 08:09:18 +01:00
|
|
|
|
</button>
|
|
|
|
|
|
<button class="btn btn-small" id="notify-share-btn">
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<i class="fas fa-envelope"></i> <span data-i18n="actions.notify">Notify</span>
|
2025-03-28 08:09:18 +01:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-02-22 20:20:00 -08:00
|
|
|
|
|
2025-03-28 08:09:18 +01:00
|
|
|
|
<div class="share-dialog-buttons">
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<button class="btn btn-secondary" id="share-cancel-btn" data-i18n="actions.cancel">Cancel</button>
|
|
|
|
|
|
<button class="btn btn-primary" id="share-confirm-btn" data-i18n="actions.share">Share</button>
|
2025-03-28 08:09:18 +01:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
document.body.appendChild(shareDialog);
|
2026-02-22 20:20:00 -08:00
|
|
|
|
|
2025-03-28 08:09:18 +01:00
|
|
|
|
// Add event listeners for share dialog
|
|
|
|
|
|
document.getElementById('share-cancel-btn').addEventListener('click', () => {
|
|
|
|
|
|
contextMenus.closeShareDialog();
|
|
|
|
|
|
});
|
2026-02-22 20:20:00 -08:00
|
|
|
|
|
2026-02-13 12:29:21 +01:00
|
|
|
|
document.getElementById('share-confirm-btn').addEventListener('click', async () => {
|
|
|
|
|
|
await contextMenus.createSharedLink();
|
2025-03-28 08:09:18 +01:00
|
|
|
|
});
|
2026-02-22 20:20:00 -08:00
|
|
|
|
|
2025-03-28 08:09:18 +01:00
|
|
|
|
document.getElementById('copy-share-btn').addEventListener('click', async () => {
|
|
|
|
|
|
const shareUrl = document.getElementById('generated-share-url').value;
|
|
|
|
|
|
await fileSharing.copyLinkToClipboard(shareUrl);
|
|
|
|
|
|
});
|
2026-02-22 20:20:00 -08:00
|
|
|
|
|
2025-03-28 08:09:18 +01:00
|
|
|
|
document.getElementById('notify-share-btn').addEventListener('click', () => {
|
|
|
|
|
|
const shareUrl = document.getElementById('generated-share-url').value;
|
|
|
|
|
|
contextMenus.showEmailNotificationDialog(shareUrl);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-02-22 20:20:00 -08:00
|
|
|
|
|
2025-03-28 08:09:18 +01:00
|
|
|
|
// Notification dialog
|
|
|
|
|
|
if (!document.getElementById('notification-dialog')) {
|
|
|
|
|
|
const notificationDialog = document.createElement('div');
|
|
|
|
|
|
notificationDialog.className = 'share-dialog';
|
|
|
|
|
|
notificationDialog.id = 'notification-dialog';
|
|
|
|
|
|
notificationDialog.innerHTML = `
|
|
|
|
|
|
<div class="share-dialog-content">
|
2026-02-08 22:44:42 +01:00
|
|
|
|
<div class="share-dialog-header">
|
2026-03-05 16:18:30 -05:00
|
|
|
|
<i class="fas fa-envelope dialog-header-icon"></i>
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<span data-i18n="dialogs.notify">Notify shared link</span>
|
2026-02-08 22:44:42 +01:00
|
|
|
|
</div>
|
2026-02-22 20:20:00 -08:00
|
|
|
|
|
2025-03-28 08:09:18 +01:00
|
|
|
|
<p><strong>URL:</strong> <span id="notification-share-url"></span></p>
|
2026-02-22 20:20:00 -08:00
|
|
|
|
|
2025-03-28 08:09:18 +01:00
|
|
|
|
<div class="form-group">
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<label for="notification-email" data-i18n="dialogs.recipient">Recipient:</label>
|
|
|
|
|
|
<input type="email" id="notification-email" placeholder="Email address">
|
2025-03-28 08:09:18 +01:00
|
|
|
|
</div>
|
2026-02-22 20:20:00 -08:00
|
|
|
|
|
2025-03-28 08:09:18 +01:00
|
|
|
|
<div class="form-group">
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<label for="notification-message" data-i18n="dialogs.message">Message (optional):</label>
|
2025-03-28 08:09:18 +01:00
|
|
|
|
<textarea id="notification-message" rows="3"></textarea>
|
|
|
|
|
|
</div>
|
2026-02-22 20:20:00 -08:00
|
|
|
|
|
2025-03-28 08:09:18 +01:00
|
|
|
|
<div class="share-dialog-buttons">
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<button class="btn btn-secondary" id="notification-cancel-btn" data-i18n="actions.cancel">Cancel</button>
|
|
|
|
|
|
<button class="btn btn-primary" id="notification-send-btn" data-i18n="actions.send">Send</button>
|
2025-03-28 08:09:18 +01:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
document.body.appendChild(notificationDialog);
|
2026-02-22 20:20:00 -08:00
|
|
|
|
|
2025-03-28 08:09:18 +01:00
|
|
|
|
// Add event listeners for notification dialog
|
|
|
|
|
|
document.getElementById('notification-cancel-btn').addEventListener('click', () => {
|
|
|
|
|
|
contextMenus.closeNotificationDialog();
|
|
|
|
|
|
});
|
2026-02-22 20:20:00 -08:00
|
|
|
|
|
2025-03-28 08:09:18 +01:00
|
|
|
|
document.getElementById('notification-send-btn').addEventListener('click', () => {
|
|
|
|
|
|
contextMenus.sendShareNotification();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-03-19 23:28:29 +01:00
|
|
|
|
|
|
|
|
|
|
// Assign events to menu items
|
2025-03-28 08:09:18 +01:00
|
|
|
|
if (window.contextMenus) {
|
|
|
|
|
|
window.contextMenus.assignMenuEvents();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.warn('contextMenus module not loaded');
|
|
|
|
|
|
}
|
2025-03-19 23:28:29 +01:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Set up drag and drop functionality
|
|
|
|
|
|
*/
|
|
|
|
|
|
setupDragAndDrop() {
|
2026-03-25 18:49:38 +01:00
|
|
|
|
// prepare area to build dragged elements
|
2026-04-07 22:48:59 +02:00
|
|
|
|
this.dragPreview = document.createElement('div');
|
|
|
|
|
|
this.dragPreview.className = 'drag-preview';
|
2026-03-25 18:49:38 +01:00
|
|
|
|
document.body.appendChild(this.dragPreview);
|
|
|
|
|
|
this.draggedItems = null;
|
|
|
|
|
|
|
2025-03-19 23:28:29 +01:00
|
|
|
|
const dropzone = document.getElementById('dropzone');
|
|
|
|
|
|
|
2026-02-18 10:53:43 +01:00
|
|
|
|
const collectDroppedEntries = async (dataTransfer) => {
|
|
|
|
|
|
const items = Array.from(dataTransfer?.items || []);
|
2026-04-07 22:48:59 +02:00
|
|
|
|
const rootEntries = items.map((it) => (typeof it.webkitGetAsEntry === 'function' ? it.webkitGetAsEntry() : null)).filter(Boolean);
|
2026-02-18 10:53:43 +01:00
|
|
|
|
|
|
|
|
|
|
if (rootEntries.length === 0) return null;
|
|
|
|
|
|
|
|
|
|
|
|
const out = [];
|
|
|
|
|
|
|
|
|
|
|
|
const walkEntry = async (entry, prefix = '') => {
|
|
|
|
|
|
if (!entry) return;
|
|
|
|
|
|
|
|
|
|
|
|
if (entry.isFile) {
|
|
|
|
|
|
await new Promise((resolve) => {
|
|
|
|
|
|
entry.file(
|
|
|
|
|
|
(file) => {
|
|
|
|
|
|
out.push({ file, relativePath: `${prefix}${file.name}` });
|
|
|
|
|
|
resolve();
|
|
|
|
|
|
},
|
|
|
|
|
|
() => resolve()
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (entry.isDirectory) {
|
|
|
|
|
|
const dirPrefix = `${prefix}${entry.name}/`;
|
|
|
|
|
|
const reader = entry.createReader();
|
|
|
|
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
|
|
const children = await new Promise((resolve) => {
|
|
|
|
|
|
reader.readEntries(resolve, () => resolve([]));
|
|
|
|
|
|
});
|
|
|
|
|
|
if (!children || children.length === 0) break;
|
|
|
|
|
|
for (const child of children) {
|
|
|
|
|
|
// eslint-disable-next-line no-await-in-loop
|
|
|
|
|
|
await walkEntry(child, dirPrefix);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
for (const root of rootEntries) {
|
|
|
|
|
|
// eslint-disable-next-line no-await-in-loop
|
|
|
|
|
|
await walkEntry(root, '');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return out;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-03-19 23:28:29 +01:00
|
|
|
|
// Dropzone events
|
|
|
|
|
|
dropzone.addEventListener('dragover', (e) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
dropzone.classList.add('active');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
dropzone.addEventListener('dragleave', () => {
|
|
|
|
|
|
dropzone.classList.remove('active');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-02-18 10:53:43 +01:00
|
|
|
|
dropzone.addEventListener('drop', async (e) => {
|
2025-03-19 23:28:29 +01:00
|
|
|
|
e.preventDefault();
|
2026-02-16 16:18:39 +01:00
|
|
|
|
e.stopPropagation(); // Prevent bubbling to document's drop handler (avoids double upload)
|
2026-04-07 22:48:59 +02:00
|
|
|
|
e._oxiHandled = true; // Mark as handled for document-level fallback
|
2025-03-19 23:28:29 +01:00
|
|
|
|
dropzone.classList.remove('active');
|
|
|
|
|
|
if (e.dataTransfer.files.length > 0) {
|
2026-02-18 10:53:43 +01:00
|
|
|
|
// First try directory-aware extraction (Finder folder drag & drop)
|
|
|
|
|
|
const droppedEntries = await collectDroppedEntries(e.dataTransfer);
|
|
|
|
|
|
if (droppedEntries && droppedEntries.length > 0) {
|
2026-04-07 22:50:42 +02:00
|
|
|
|
const hasFolderStructure = droppedEntries.some((x) => x.relativePath?.includes('/'));
|
2026-02-18 10:53:43 +01:00
|
|
|
|
if (hasFolderStructure) {
|
|
|
|
|
|
fileOps.uploadFolderEntries(droppedEntries);
|
|
|
|
|
|
} else {
|
2026-04-07 22:48:59 +02:00
|
|
|
|
fileOps.uploadFiles(droppedEntries.map((x) => x.file));
|
2026-02-18 10:53:43 +01:00
|
|
|
|
}
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
dropzone.style.display = 'none';
|
|
|
|
|
|
}, 500);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-16 17:58:50 +01:00
|
|
|
|
// Detect folder drops: files from folder drops have webkitRelativePath set
|
2026-04-07 22:50:42 +02:00
|
|
|
|
const hasRelativePaths = Array.from(e.dataTransfer.files).some((f) => f.webkitRelativePath?.includes('/'));
|
2026-02-16 17:58:50 +01:00
|
|
|
|
if (hasRelativePaths) {
|
|
|
|
|
|
fileOps.uploadFolderFiles(e.dataTransfer.files);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
fileOps.uploadFiles(e.dataTransfer.files);
|
|
|
|
|
|
}
|
2025-03-19 23:28:29 +01:00
|
|
|
|
}
|
2026-02-16 16:18:39 +01:00
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
dropzone.style.display = 'none';
|
|
|
|
|
|
}, 500);
|
2025-03-19 23:28:29 +01:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Document-wide drag and drop
|
|
|
|
|
|
document.addEventListener('dragover', (e) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
if (e.dataTransfer.types.includes('Files')) {
|
|
|
|
|
|
dropzone.style.display = 'block';
|
|
|
|
|
|
dropzone.classList.add('active');
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('dragleave', (e) => {
|
2026-04-07 22:48:59 +02:00
|
|
|
|
if (e.clientX <= 0 || e.clientY <= 0 || e.clientX >= window.innerWidth || e.clientY >= window.innerHeight) {
|
2025-03-19 23:28:29 +01:00
|
|
|
|
dropzone.classList.remove('active');
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
if (!dropzone.classList.contains('active')) {
|
|
|
|
|
|
dropzone.style.display = 'none';
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 100);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-02-18 10:53:43 +01:00
|
|
|
|
document.addEventListener('drop', async (e) => {
|
2025-03-19 23:28:29 +01:00
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
dropzone.classList.remove('active');
|
|
|
|
|
|
|
2026-02-16 16:18:39 +01:00
|
|
|
|
// Skip if already handled by the dropzone handler (defensive against bubble leaks)
|
|
|
|
|
|
if (e._oxiHandled) return;
|
|
|
|
|
|
|
2025-03-19 23:28:29 +01:00
|
|
|
|
if (e.dataTransfer.files.length > 0) {
|
2026-02-18 10:53:43 +01:00
|
|
|
|
// First try directory-aware extraction (Finder folder drag & drop)
|
|
|
|
|
|
const droppedEntries = await collectDroppedEntries(e.dataTransfer);
|
|
|
|
|
|
if (droppedEntries && droppedEntries.length > 0) {
|
2026-04-07 22:50:42 +02:00
|
|
|
|
const hasFolderStructure = droppedEntries.some((x) => x.relativePath?.includes('/'));
|
2026-02-18 10:53:43 +01:00
|
|
|
|
if (hasFolderStructure) {
|
|
|
|
|
|
fileOps.uploadFolderEntries(droppedEntries);
|
|
|
|
|
|
} else {
|
2026-04-07 22:48:59 +02:00
|
|
|
|
fileOps.uploadFiles(droppedEntries.map((x) => x.file));
|
2026-02-18 10:53:43 +01:00
|
|
|
|
}
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
dropzone.style.display = 'none';
|
|
|
|
|
|
}, 500);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-16 17:58:50 +01:00
|
|
|
|
// Detect folder drops: files from folder drops have webkitRelativePath set
|
2026-04-07 22:50:42 +02:00
|
|
|
|
const hasRelativePaths = Array.from(e.dataTransfer.files).some((f) => f.webkitRelativePath?.includes('/'));
|
2026-02-16 17:58:50 +01:00
|
|
|
|
if (hasRelativePaths) {
|
|
|
|
|
|
fileOps.uploadFolderFiles(e.dataTransfer.files);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
fileOps.uploadFiles(e.dataTransfer.files);
|
|
|
|
|
|
}
|
2025-03-19 23:28:29 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
dropzone.style.display = 'none';
|
|
|
|
|
|
}, 500);
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Switch to grid view
|
|
|
|
|
|
*/
|
|
|
|
|
|
switchToGridView() {
|
2026-04-03 17:58:14 +02:00
|
|
|
|
this._hydrateViewIfNeeded();
|
|
|
|
|
|
|
2025-03-19 23:28:29 +01:00
|
|
|
|
window.app.currentView = 'grid';
|
|
|
|
|
|
localStorage.setItem('oxicloud-view', 'grid');
|
2026-04-03 17:58:14 +02:00
|
|
|
|
|
|
|
|
|
|
window.syncViewContainers();
|
2025-03-19 23:28:29 +01:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Switch to list view
|
|
|
|
|
|
*/
|
|
|
|
|
|
switchToListView() {
|
2026-04-03 17:58:14 +02:00
|
|
|
|
this._hydrateViewIfNeeded();
|
|
|
|
|
|
|
2025-03-19 23:28:29 +01:00
|
|
|
|
window.app.currentView = 'list';
|
|
|
|
|
|
localStorage.setItem('oxicloud-view', 'list');
|
2026-04-03 17:58:14 +02:00
|
|
|
|
|
|
|
|
|
|
window.syncViewContainers();
|
2025-03-19 23:28:29 +01:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-21 00:19:04 +01:00
|
|
|
|
* Update breadcrumb navigation from the breadcrumbPath array.
|
|
|
|
|
|
* Renders: Home > folder1 > folder2 > ...
|
|
|
|
|
|
* Each segment is clickable to navigate back to that level.
|
2025-03-19 23:28:29 +01:00
|
|
|
|
*/
|
2026-02-21 00:19:04 +01:00
|
|
|
|
updateBreadcrumb() {
|
2025-03-19 23:28:29 +01:00
|
|
|
|
const breadcrumb = document.querySelector('.breadcrumb');
|
|
|
|
|
|
breadcrumb.innerHTML = '';
|
2026-02-21 00:19:04 +01:00
|
|
|
|
const path = window.app.breadcrumbPath; // [{id, name}, ...]
|
|
|
|
|
|
|
2025-03-27 01:13:34 +01:00
|
|
|
|
// Helper function to safely get translation text
|
|
|
|
|
|
const getTranslatedText = (key, defaultValue) => {
|
2026-04-07 22:50:42 +02:00
|
|
|
|
if (!window.i18n?.t) return defaultValue;
|
2025-03-27 01:13:34 +01:00
|
|
|
|
return window.i18n.t(key);
|
|
|
|
|
|
};
|
2026-02-21 00:19:04 +01:00
|
|
|
|
|
2026-02-22 20:20:00 -08:00
|
|
|
|
// -- Home icon (always present, clickable to go to root) --
|
|
|
|
|
|
const homeIcon = document.createElement('span');
|
|
|
|
|
|
homeIcon.className = 'breadcrumb-item breadcrumb-home';
|
|
|
|
|
|
homeIcon.innerHTML = '<i class="fas fa-home"></i>';
|
|
|
|
|
|
homeIcon.title = getTranslatedText('breadcrumb.home', 'Home');
|
2026-02-21 00:19:04 +01:00
|
|
|
|
|
2026-02-22 20:20:00 -08:00
|
|
|
|
// Home is always clickable if we have a home folder
|
|
|
|
|
|
if (window.app.userHomeFolderId) {
|
|
|
|
|
|
homeIcon.classList.add('breadcrumb-link');
|
|
|
|
|
|
homeIcon.addEventListener('click', () => {
|
2026-02-21 00:19:04 +01:00
|
|
|
|
window.app.breadcrumbPath = [];
|
2025-03-27 01:13:34 +01:00
|
|
|
|
window.app.currentPath = window.app.userHomeFolderId;
|
2026-04-07 22:50:42 +02:00
|
|
|
|
this.updateBreadcrumb();
|
2025-03-27 01:13:34 +01:00
|
|
|
|
window.loadFiles();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-02-22 20:20:00 -08:00
|
|
|
|
breadcrumb.appendChild(homeIcon);
|
|
|
|
|
|
|
2026-03-25 17:03:36 +01:00
|
|
|
|
// -- Root/Home folder name (if available) is always the first element of the breadcrumb --
|
|
|
|
|
|
// TODO clarify the difference between homeIcon & this first element
|
2026-02-22 20:20:00 -08:00
|
|
|
|
if (window.app.userHomeFolderName) {
|
2026-03-25 17:03:36 +01:00
|
|
|
|
if (path.length === 0 || path[0].id !== window.app.userHomeFolderId) {
|
2026-04-07 22:48:59 +02:00
|
|
|
|
path.unshift({
|
|
|
|
|
|
name: window.app.userHomeFolderName,
|
|
|
|
|
|
id: window.app.userHomeFolderId
|
|
|
|
|
|
});
|
2026-02-22 20:20:00 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-03-19 23:28:29 +01:00
|
|
|
|
|
2026-03-25 17:03:36 +01:00
|
|
|
|
// -- Root/Home + Intermediate + current segments --
|
2026-02-21 00:19:04 +01:00
|
|
|
|
path.forEach((segment, index) => {
|
|
|
|
|
|
const isLast = index === path.length - 1;
|
|
|
|
|
|
|
|
|
|
|
|
// Separator
|
2025-03-19 23:28:29 +01:00
|
|
|
|
const separator = document.createElement('span');
|
|
|
|
|
|
separator.className = 'breadcrumb-separator';
|
|
|
|
|
|
separator.textContent = '>';
|
|
|
|
|
|
breadcrumb.appendChild(separator);
|
|
|
|
|
|
|
2026-02-21 00:19:04 +01:00
|
|
|
|
// Segment item
|
|
|
|
|
|
const item = document.createElement('span');
|
|
|
|
|
|
item.className = 'breadcrumb-item';
|
|
|
|
|
|
item.textContent = segment.name;
|
2026-03-25 17:47:23 +01:00
|
|
|
|
item.dataset.folderId = segment.id;
|
2026-02-21 00:19:04 +01:00
|
|
|
|
|
|
|
|
|
|
if (!isLast) {
|
|
|
|
|
|
// Intermediate segment: clickable – truncate path to this level
|
|
|
|
|
|
item.classList.add('breadcrumb-link');
|
|
|
|
|
|
item.addEventListener('click', () => {
|
|
|
|
|
|
window.app.breadcrumbPath = path.slice(0, index + 1);
|
|
|
|
|
|
window.app.currentPath = segment.id;
|
2026-04-07 22:50:42 +02:00
|
|
|
|
this.updateBreadcrumb();
|
2026-02-21 00:19:04 +01:00
|
|
|
|
window.loadFiles();
|
|
|
|
|
|
});
|
2026-03-25 17:47:23 +01:00
|
|
|
|
|
|
|
|
|
|
// can drag files on this folder
|
|
|
|
|
|
// dragover – only folders are valid drop targets
|
|
|
|
|
|
item.addEventListener('dragover', (e) => {
|
2026-04-07 22:48:59 +02:00
|
|
|
|
const card = e.target.closest('span');
|
2026-04-07 22:50:42 +02:00
|
|
|
|
if (!card?.dataset.folderId) return;
|
2026-03-25 17:47:23 +01:00
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
card.classList.add('drop-target');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// dragleave
|
|
|
|
|
|
item.addEventListener('dragleave', (e) => {
|
2026-04-07 22:48:59 +02:00
|
|
|
|
console.log('dragleave ', e);
|
|
|
|
|
|
const card = e.target.closest('span');
|
2026-04-07 22:50:42 +02:00
|
|
|
|
if (!card?.dataset.folderId) return;
|
2026-03-25 17:47:23 +01:00
|
|
|
|
card.classList.remove('drop-target');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// drop – only folders accept drops
|
|
|
|
|
|
item.addEventListener('drop', async (e) => {
|
2026-04-07 22:48:59 +02:00
|
|
|
|
const card = e.target.closest('span');
|
2026-03-25 17:47:23 +01:00
|
|
|
|
if (!card) return;
|
|
|
|
|
|
const targetFolderId = card.dataset.folderId;
|
|
|
|
|
|
if (!targetFolderId) return;
|
|
|
|
|
|
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
card.classList.remove('drop-target');
|
|
|
|
|
|
|
2026-03-25 18:49:38 +01:00
|
|
|
|
const action = e.dataTransfer?.dropEffect;
|
2026-04-07 22:50:42 +02:00
|
|
|
|
await this._dropToFolder(action, targetFolderId, e.dataTransfer);
|
2026-03-25 17:47:23 +01:00
|
|
|
|
});
|
2026-02-21 00:19:04 +01:00
|
|
|
|
} else {
|
|
|
|
|
|
// Last segment: current location, not clickable
|
|
|
|
|
|
item.classList.add('breadcrumb-current');
|
|
|
|
|
|
}
|
|
|
|
|
|
breadcrumb.appendChild(item);
|
|
|
|
|
|
});
|
2025-03-19 23:28:29 +01:00
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-12 11:16:58 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* Check if a file can be previewed in the viewer
|
|
|
|
|
|
* @param {Object} file - File object with mime_type property
|
|
|
|
|
|
* @returns {boolean}
|
|
|
|
|
|
*/
|
|
|
|
|
|
isViewableFile(file) {
|
2026-02-20 12:27:52 +01:00
|
|
|
|
return window.uiFileTypes.isViewableFile(file);
|
2026-02-16 09:17:54 +01:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Get FontAwesome icon class for a filename based on its extension.
|
|
|
|
|
|
* Used as fallback when the backend DTO doesn't include icon_class
|
2026-02-16 21:51:53 +01:00
|
|
|
|
* (e.g. trash items).
|
2026-02-16 09:17:54 +01:00
|
|
|
|
*/
|
|
|
|
|
|
getIconClass(fileName) {
|
2026-02-20 12:27:52 +01:00
|
|
|
|
return window.uiFileTypes.getIconClass(fileName);
|
2026-02-12 11:16:58 +01:00
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-18 10:53:43 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* Get CSS special class for icon styling based on filename extension.
|
|
|
|
|
|
* Used as fallback when the backend DTO doesn't include icon_special_class.
|
|
|
|
|
|
*/
|
|
|
|
|
|
getIconSpecialClass(fileName) {
|
2026-02-20 12:27:52 +01:00
|
|
|
|
return window.uiFileTypes.getIconSpecialClass(fileName);
|
2026-02-18 10:53:43 +01:00
|
|
|
|
},
|
|
|
|
|
|
|
2025-03-19 23:28:29 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* Show notification
|
|
|
|
|
|
* @param {string} title - Notification title
|
|
|
|
|
|
* @param {string} message - Notification message
|
|
|
|
|
|
*/
|
|
|
|
|
|
showNotification(title, message) {
|
2026-02-20 12:27:52 +01:00
|
|
|
|
window.uiNotifications.show(title, message);
|
2025-03-19 23:28:29 +01:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Close folder context menu
|
|
|
|
|
|
*/
|
|
|
|
|
|
closeContextMenu() {
|
|
|
|
|
|
const menu = document.getElementById('folder-context-menu');
|
|
|
|
|
|
if (menu) {
|
|
|
|
|
|
menu.style.display = 'none';
|
|
|
|
|
|
window.app.contextMenuTargetFolder = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Close file context menu
|
|
|
|
|
|
*/
|
|
|
|
|
|
closeFileContextMenu() {
|
|
|
|
|
|
const menu = document.getElementById('file-context-menu');
|
|
|
|
|
|
if (menu) {
|
|
|
|
|
|
menu.style.display = 'none';
|
|
|
|
|
|
window.app.contextMenuTargetFile = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-16 13:10:44 +01:00
|
|
|
|
/* ================================================================
|
|
|
|
|
|
* Data store + event delegation (replaces per-item listeners)
|
|
|
|
|
|
* ================================================================ */
|
|
|
|
|
|
|
|
|
|
|
|
/** @type {Map<string, Object>} item data keyed by id */
|
|
|
|
|
|
_items: new Map(),
|
|
|
|
|
|
|
2026-02-18 10:53:43 +01:00
|
|
|
|
/** @type {Array<Object>} last rendered folder dataset */
|
|
|
|
|
|
_lastFolders: [],
|
|
|
|
|
|
|
|
|
|
|
|
/** @type {Array<Object>} last rendered file dataset */
|
|
|
|
|
|
_lastFiles: [],
|
|
|
|
|
|
|
2026-02-16 13:10:44 +01:00
|
|
|
|
/** @type {boolean} */
|
|
|
|
|
|
_delegationReady: false,
|
|
|
|
|
|
|
2026-02-18 10:53:43 +01:00
|
|
|
|
_getActiveView() {
|
|
|
|
|
|
if (window.app && window.app.currentView === 'list') return 'list';
|
|
|
|
|
|
if (window.app && window.app.currentView === 'grid') return 'grid';
|
|
|
|
|
|
|
|
|
|
|
|
const stored = localStorage.getItem('oxicloud-view');
|
|
|
|
|
|
return stored === 'list' ? 'list' : 'grid';
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-04-03 17:58:14 +02:00
|
|
|
|
/**
|
2026-04-07 22:48:59 +02:00
|
|
|
|
*
|
|
|
|
|
|
* @param {Object[]} folders
|
|
|
|
|
|
* @returns
|
2026-04-03 17:58:14 +02:00
|
|
|
|
*/
|
|
|
|
|
|
_renderFoldersToView(folders) {
|
2026-02-18 10:53:43 +01:00
|
|
|
|
if (!Array.isArray(folders) || folders.length === 0) return;
|
2026-04-07 22:48:59 +02:00
|
|
|
|
const target = document.getElementById('files-list');
|
2026-02-18 10:53:43 +01:00
|
|
|
|
if (!target) return;
|
|
|
|
|
|
|
|
|
|
|
|
const frag = document.createDocumentFragment();
|
|
|
|
|
|
for (const folder of folders) {
|
2026-04-07 22:48:59 +02:00
|
|
|
|
frag.appendChild(this._createFolderItem(folder));
|
2026-02-18 10:53:43 +01:00
|
|
|
|
}
|
|
|
|
|
|
target.appendChild(frag);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-04-07 22:48:59 +02:00
|
|
|
|
/**
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {Object[]} files
|
|
|
|
|
|
* @returns
|
2026-04-03 17:58:14 +02:00
|
|
|
|
*/
|
|
|
|
|
|
_renderFilesToView(files) {
|
2026-02-18 10:53:43 +01:00
|
|
|
|
if (!Array.isArray(files) || files.length === 0) return;
|
2026-04-07 22:48:59 +02:00
|
|
|
|
const target = document.getElementById('files-list');
|
2026-02-18 10:53:43 +01:00
|
|
|
|
if (!target) return;
|
|
|
|
|
|
|
|
|
|
|
|
const frag = document.createDocumentFragment();
|
|
|
|
|
|
for (const file of files) {
|
2026-04-07 22:48:59 +02:00
|
|
|
|
frag.appendChild(this._createFileItem(file));
|
2026-02-18 10:53:43 +01:00
|
|
|
|
}
|
|
|
|
|
|
target.appendChild(frag);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
_upsertById(arr, item) {
|
2026-04-07 22:50:42 +02:00
|
|
|
|
if (!Array.isArray(arr) || !item?.id) return;
|
2026-04-07 22:48:59 +02:00
|
|
|
|
const idx = arr.findIndex((x) => x && x.id === item.id);
|
2026-02-18 10:53:43 +01:00
|
|
|
|
if (idx >= 0) {
|
|
|
|
|
|
arr[idx] = item;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
arr.push(item);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-03-25 18:49:38 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* handle the drop
|
|
|
|
|
|
* @param {string} action copy|move
|
|
|
|
|
|
* @param {string} targetFolderId the target
|
|
|
|
|
|
* @param {any} dataTransfer fallback if nothing is selected
|
|
|
|
|
|
*/
|
|
|
|
|
|
async _dropToFolder(action, targetFolderId, dataTransfer) {
|
2026-04-07 22:50:42 +02:00
|
|
|
|
const selection = window.multiSelect.getSelection(targetFolderId);
|
2026-04-03 17:58:14 +02:00
|
|
|
|
|
2026-03-25 18:49:38 +01:00
|
|
|
|
window.multiSelect.clear();
|
|
|
|
|
|
|
2026-04-07 22:50:42 +02:00
|
|
|
|
if (selection.fileIds.length === 0 && selection.folderIds.length === 0) {
|
2026-03-25 18:49:38 +01:00
|
|
|
|
// try to use dataTransfer (direct move without selection)
|
|
|
|
|
|
const id = dataTransfer.getData('text/plain');
|
|
|
|
|
|
const isFolder = dataTransfer.getData('application/oxicloud-folder') === 'true';
|
|
|
|
|
|
|
|
|
|
|
|
if (isFolder && id === targetFolderId) {
|
2026-04-07 22:48:59 +02:00
|
|
|
|
console.log('nothing to do');
|
2026-03-25 18:49:38 +01:00
|
|
|
|
return; //nothing to do
|
|
|
|
|
|
}
|
|
|
|
|
|
// append current item to selection
|
|
|
|
|
|
if (isFolder) {
|
|
|
|
|
|
console.log(`adding ${id} as folder`);
|
|
|
|
|
|
selection.folderIds.push(id);
|
2026-04-07 22:48:59 +02:00
|
|
|
|
} else {
|
2026-03-25 18:49:38 +01:00
|
|
|
|
console.log(`adding ${id} as file`);
|
|
|
|
|
|
selection.fileIds.push(id);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`request ${action} of: `, selection);
|
|
|
|
|
|
/*
|
|
|
|
|
|
TODO do we prefer use atomic operation on 1 item ? like:
|
|
|
|
|
|
await fileOps.moveFolder(sourceId, targetFolderId);
|
|
|
|
|
|
await fileOps.moveFile(sourceId, targetFolderId);
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
let result;
|
|
|
|
|
|
switch (action) {
|
2026-04-07 22:48:59 +02:00
|
|
|
|
case 'copy':
|
2026-03-25 18:49:38 +01:00
|
|
|
|
result = await window.fileOps.batchCopy(selection.fileIds, selection.folderIds, targetFolderId);
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
2026-04-07 22:48:59 +02:00
|
|
|
|
case 'move':
|
2026-03-25 18:49:38 +01:00
|
|
|
|
result = await window.fileOps.batchMove(selection.fileIds, selection.folderIds, targetFolderId);
|
2026-04-03 17:58:14 +02:00
|
|
|
|
// redraw directory
|
2026-04-07 22:48:59 +02:00
|
|
|
|
if (result.success > 0) window.loadFiles();
|
2026-03-25 18:49:38 +01:00
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
default:
|
2026-04-03 17:58:14 +02:00
|
|
|
|
console.error(`drag and drop: action ${action} unknown`);
|
|
|
|
|
|
return;
|
2026-03-25 18:49:38 +01:00
|
|
|
|
}
|
|
|
|
|
|
window.multiSelect.showBatchResult(action, result);
|
2026-04-07 22:48:59 +02:00
|
|
|
|
console.log(result);
|
2026-03-25 18:49:38 +01:00
|
|
|
|
},
|
2026-04-03 17:58:14 +02:00
|
|
|
|
|
|
|
|
|
|
_hydrateViewIfNeeded() {
|
2026-02-18 10:53:43 +01:00
|
|
|
|
// Only hydrate if there is at least one rendered item in the opposite/current DOM.
|
|
|
|
|
|
// This prevents stale cache hydration in empty-state screens.
|
2026-04-03 17:58:14 +02:00
|
|
|
|
const hasAnyRenderedItem = !!document.querySelector('#files-list .file-item');
|
2026-02-18 10:53:43 +01:00
|
|
|
|
if (!hasAnyRenderedItem) return;
|
|
|
|
|
|
|
2026-04-03 17:58:14 +02:00
|
|
|
|
// FIXME: thre is the header...
|
|
|
|
|
|
const listView = document.getElementById('files-list');
|
|
|
|
|
|
if (!listView) return;
|
|
|
|
|
|
if (listView.children.length > 1) return;
|
2026-02-18 10:53:43 +01:00
|
|
|
|
|
2026-04-03 17:58:14 +02:00
|
|
|
|
this._renderFoldersToView(this._lastFolders);
|
|
|
|
|
|
this._renderFilesToView(this._lastFiles);
|
2026-02-18 10:53:43 +01:00
|
|
|
|
},
|
|
|
|
|
|
|
2025-03-19 23:28:29 +01:00
|
|
|
|
/**
|
2026-02-16 13:10:44 +01:00
|
|
|
|
* Attach a fixed set of delegated event listeners to the two
|
2026-04-03 17:58:14 +02:00
|
|
|
|
* container elements (files-list).
|
2026-02-16 13:10:44 +01:00
|
|
|
|
* Called once – idempotent.
|
2025-03-19 23:28:29 +01:00
|
|
|
|
*/
|
2026-02-16 13:10:44 +01:00
|
|
|
|
initDelegation() {
|
|
|
|
|
|
if (this._delegationReady) return;
|
2026-04-03 17:58:14 +02:00
|
|
|
|
const filesList = document.getElementById('files-list');
|
|
|
|
|
|
if (!filesList) return;
|
2026-02-16 13:10:44 +01:00
|
|
|
|
this._delegationReady = true;
|
|
|
|
|
|
|
|
|
|
|
|
// ── helpers ────────────────────────────────────────────────
|
|
|
|
|
|
const itemInfo = (card) => {
|
|
|
|
|
|
if (!card) return null;
|
|
|
|
|
|
const fileId = card.dataset.fileId;
|
2026-04-07 22:48:59 +02:00
|
|
|
|
if (fileId)
|
|
|
|
|
|
return {
|
|
|
|
|
|
type: 'file',
|
|
|
|
|
|
id: fileId,
|
|
|
|
|
|
name: card.dataset.fileName,
|
2026-04-07 22:50:42 +02:00
|
|
|
|
data: this._items.get(fileId)
|
2026-04-07 22:48:59 +02:00
|
|
|
|
};
|
2026-02-16 13:10:44 +01:00
|
|
|
|
const folderId = card.dataset.folderId;
|
2026-04-07 22:48:59 +02:00
|
|
|
|
if (folderId)
|
|
|
|
|
|
return {
|
|
|
|
|
|
type: 'folder',
|
|
|
|
|
|
id: folderId,
|
|
|
|
|
|
name: card.dataset.folderName,
|
2026-04-07 22:50:42 +02:00
|
|
|
|
data: this._items.get(folderId)
|
2026-04-07 22:48:59 +02:00
|
|
|
|
};
|
2026-02-16 13:10:44 +01:00
|
|
|
|
return null;
|
|
|
|
|
|
};
|
2026-02-13 09:32:16 +01:00
|
|
|
|
|
2026-03-04 21:44:02 -05:00
|
|
|
|
const openFile = async (file) => {
|
2026-02-16 13:10:44 +01:00
|
|
|
|
if (!file) return;
|
|
|
|
|
|
if (window.recent) {
|
|
|
|
|
|
document.dispatchEvent(new CustomEvent('file-accessed', { detail: { file } }));
|
|
|
|
|
|
}
|
2026-02-21 13:39:27 +01:00
|
|
|
|
// WOPI editor intercept: open Office documents in the WOPI editor
|
2026-03-04 14:02:15 +01:00
|
|
|
|
// But NOT image files - those should be previewed in the inline viewer
|
2026-03-04 15:14:07 +01:00
|
|
|
|
const ext = (file.name || '').split('.').pop().toLowerCase();
|
2026-04-07 22:48:59 +02:00
|
|
|
|
const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'bmp', 'ico', 'heic', 'heif', 'avif', 'tiff'];
|
2026-04-07 22:50:42 +02:00
|
|
|
|
const isImage = file.mime_type?.startsWith('image/') || imageExts.includes(ext);
|
2026-04-06 10:59:46 +02:00
|
|
|
|
try {
|
2026-04-07 22:50:42 +02:00
|
|
|
|
if (!isImage && window.wopiEditor && (await window.wopiEditor.canEdit(file.name))) {
|
|
|
|
|
|
await window.wopiEditor.openInModal(file.id, file.name, 'edit');
|
2026-04-06 10:59:46 +02:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-04-07 22:50:42 +02:00
|
|
|
|
} catch (e) {
|
2026-04-06 10:59:46 +02:00
|
|
|
|
console.warn(`WOPI Editor failed, falling bck to classic view `, e);
|
2026-02-21 13:39:27 +01:00
|
|
|
|
}
|
2026-04-06 10:59:46 +02:00
|
|
|
|
|
2026-04-07 22:50:42 +02:00
|
|
|
|
if (this.isViewableFile(file) || isImage) {
|
2026-04-06 10:59:46 +02:00
|
|
|
|
if (window.inlineViewer) {
|
|
|
|
|
|
window.inlineViewer.openFile(file);
|
|
|
|
|
|
// update history
|
|
|
|
|
|
window.app.viewFile = file.id;
|
|
|
|
|
|
window.updateHistory(false);
|
2026-04-07 22:50:42 +02:00
|
|
|
|
} else {
|
2026-04-06 10:59:46 +02:00
|
|
|
|
window.fileOps.downloadFile(file.id, file.name);
|
|
|
|
|
|
}
|
2026-02-16 13:10:44 +01:00
|
|
|
|
} else {
|
|
|
|
|
|
window.fileOps.downloadFile(file.id, file.name);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2026-02-13 09:32:16 +01:00
|
|
|
|
|
2026-02-16 13:10:44 +01:00
|
|
|
|
const navigateFolder = (card) => {
|
2026-02-21 00:19:04 +01:00
|
|
|
|
const folderId = card.dataset.folderId;
|
|
|
|
|
|
const folderName = card.dataset.folderName;
|
|
|
|
|
|
window.app.breadcrumbPath.push({ id: folderId, name: folderName });
|
|
|
|
|
|
window.app.currentPath = folderId;
|
2026-04-07 22:50:42 +02:00
|
|
|
|
this.updateBreadcrumb();
|
2026-02-16 13:10:44 +01:00
|
|
|
|
window.loadFiles();
|
|
|
|
|
|
};
|
2025-03-19 23:28:29 +01:00
|
|
|
|
|
2026-02-16 13:10:44 +01:00
|
|
|
|
const setContextTarget = (card, info) => {
|
|
|
|
|
|
if (info.type === 'folder') {
|
|
|
|
|
|
window.app.contextMenuTargetFolder = {
|
|
|
|
|
|
id: info.id,
|
|
|
|
|
|
name: card.dataset.folderName,
|
2026-04-07 22:48:59 +02:00
|
|
|
|
parent_id: card.dataset.parentId || ''
|
2026-02-16 13:10:44 +01:00
|
|
|
|
};
|
|
|
|
|
|
} else {
|
|
|
|
|
|
window.app.contextMenuTargetFile = {
|
|
|
|
|
|
id: info.id,
|
|
|
|
|
|
name: card.dataset.fileName,
|
2026-04-07 22:48:59 +02:00
|
|
|
|
folder_id: card.dataset.folderId || ''
|
2026-02-16 13:10:44 +01:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-03-19 23:28:29 +01:00
|
|
|
|
|
2026-04-03 17:58:14 +02:00
|
|
|
|
// ── click (open / navigate; select only via checkbox) ──
|
|
|
|
|
|
filesList.addEventListener('click', (e) => {
|
|
|
|
|
|
const card = e.target.closest('.file-item');
|
2026-02-16 13:10:44 +01:00
|
|
|
|
if (!card) return;
|
2025-03-19 23:28:29 +01:00
|
|
|
|
|
2026-04-03 17:58:14 +02:00
|
|
|
|
if (e.target.closest('.file-actions')) {
|
2026-02-16 13:10:44 +01:00
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
const info = itemInfo(card);
|
|
|
|
|
|
if (!info) return;
|
|
|
|
|
|
setContextTarget(card, info);
|
2026-04-07 22:48:59 +02:00
|
|
|
|
const menuId = info.type === 'folder' ? 'folder-context-menu' : 'file-context-menu';
|
|
|
|
|
|
showContextMenuAtElement(e.target.closest('.file-actions'), menuId);
|
2026-02-16 13:10:44 +01:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-03-19 23:28:29 +01:00
|
|
|
|
|
2026-04-03 17:58:14 +02:00
|
|
|
|
if (e.target.closest('.checkbox-cell')) {
|
2026-02-16 13:10:44 +01:00
|
|
|
|
toggleCardSelection(card, e);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-02-08 22:44:42 +01:00
|
|
|
|
|
2026-02-18 10:53:43 +01:00
|
|
|
|
// Favorite star – handled by direct onclick on the button
|
|
|
|
|
|
if (e.target.closest('.favorite-star')) return;
|
2026-02-08 22:44:42 +01:00
|
|
|
|
|
2026-02-18 10:53:43 +01:00
|
|
|
|
// Single-click opens/navigates (selection is only via checkbox)
|
2026-02-16 13:10:44 +01:00
|
|
|
|
const info = itemInfo(card);
|
|
|
|
|
|
if (!info) return;
|
|
|
|
|
|
|
2026-04-04 16:23:03 +02:00
|
|
|
|
// use modifier key to select/deselect item
|
|
|
|
|
|
// note: shift key is used in multiselect
|
|
|
|
|
|
// note: on MacOS, ctrl Key is used to convert click into right click, which invoke the `contextmenu` event
|
|
|
|
|
|
if (e.metaKey || e.altKey || e.ctrlKey) {
|
|
|
|
|
|
toggleCardSelection(card, e);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-05 01:49:52 +02:00
|
|
|
|
// shiftkey is used to complete selection
|
|
|
|
|
|
if (e.shiftKey && window.multiSelect) {
|
|
|
|
|
|
window.multiSelect.handleToggleItem(card, e);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-16 13:10:44 +01:00
|
|
|
|
if (info.type === 'folder') {
|
|
|
|
|
|
navigateFolder(card);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
openFile(info.data);
|
|
|
|
|
|
}
|
2026-02-08 22:44:42 +01:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-02-18 10:53:43 +01:00
|
|
|
|
// ── GRID: dblclick (navigate / open) ──────────────────────
|
2026-04-03 17:58:14 +02:00
|
|
|
|
filesList.addEventListener('dblclick', (e) => {
|
2026-02-18 10:53:43 +01:00
|
|
|
|
// Single-click already handles open/navigate.
|
|
|
|
|
|
// Prevent duplicate actions on double-click.
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-03 17:58:14 +02:00
|
|
|
|
// ── shared events ──────────────────────
|
2026-04-07 22:48:59 +02:00
|
|
|
|
|
2026-04-03 17:58:14 +02:00
|
|
|
|
filesList.addEventListener('contextmenu', (e) => {
|
2026-02-16 13:10:44 +01:00
|
|
|
|
const card = e.target.closest('.file-item');
|
|
|
|
|
|
if (!card) return;
|
2026-04-03 17:58:14 +02:00
|
|
|
|
e.preventDefault();
|
2026-02-16 13:10:44 +01:00
|
|
|
|
const info = itemInfo(card);
|
|
|
|
|
|
if (!info) return;
|
2026-04-03 17:58:14 +02:00
|
|
|
|
setContextTarget(card, info);
|
2026-04-07 22:48:59 +02:00
|
|
|
|
const menuId = info.type === 'folder' ? 'folder-context-menu' : 'file-context-menu';
|
2026-04-03 17:58:14 +02:00
|
|
|
|
const menu = document.getElementById(menuId);
|
|
|
|
|
|
if (window.contextMenus && typeof window.contextMenus.syncFavoriteOptionLabels === 'function') {
|
|
|
|
|
|
window.contextMenus.syncFavoriteOptionLabels();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (window.contextMenus && typeof window.contextMenus.syncWopiOptionVisibility === 'function') {
|
2026-04-07 22:50:42 +02:00
|
|
|
|
window.contextMenus.syncWopiOptionVisibility().catch(() => {});
|
2026-02-16 13:10:44 +01:00
|
|
|
|
}
|
2026-04-03 17:58:14 +02:00
|
|
|
|
menu.style.left = `${e.pageX}px`;
|
2026-04-07 22:48:59 +02:00
|
|
|
|
menu.style.top = `${e.pageY}px`;
|
2026-04-03 17:58:14 +02:00
|
|
|
|
menu.style.display = 'block';
|
2025-03-19 23:28:29 +01:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-03 17:58:14 +02:00
|
|
|
|
// dragstart
|
|
|
|
|
|
filesList.addEventListener('dragstart', (e) => {
|
2026-04-07 22:50:42 +02:00
|
|
|
|
const card = e.target.closest('.file-item');
|
2026-04-07 22:48:59 +02:00
|
|
|
|
if (!card) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 17:58:14 +02:00
|
|
|
|
const info = itemInfo(card);
|
2026-04-07 22:48:59 +02:00
|
|
|
|
if (!info) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-03-19 23:28:29 +01:00
|
|
|
|
|
2026-04-03 17:58:14 +02:00
|
|
|
|
e.dataTransfer.setData('text/plain', info.id);
|
2026-04-07 22:48:59 +02:00
|
|
|
|
if (info.type === 'folder') {
|
|
|
|
|
|
e.dataTransfer.setData('application/oxicloud-folder', 'true');
|
2026-04-03 17:58:14 +02:00
|
|
|
|
}
|
|
|
|
|
|
// allow copy or move (handled by the browser)
|
2026-04-07 22:48:59 +02:00
|
|
|
|
e.dataTransfer.effectAllowed = 'copyMove';
|
|
|
|
|
|
|
2026-04-07 22:50:42 +02:00
|
|
|
|
this.draggedItems = document.createElement('div');
|
|
|
|
|
|
this.draggedItems.className = 'dragged-items';
|
2026-04-07 22:48:59 +02:00
|
|
|
|
|
2026-04-03 17:58:14 +02:00
|
|
|
|
let selectedCardFromList = filesList.querySelectorAll(`div.selected > div.name-cell`);
|
2026-04-07 22:50:42 +02:00
|
|
|
|
if (selectedCardFromList.length === 0) {
|
2026-04-03 17:58:14 +02:00
|
|
|
|
// fallback to current element
|
|
|
|
|
|
selectedCardFromList = card.querySelectorAll('div.name-cell');
|
|
|
|
|
|
}
|
2026-04-07 22:48:59 +02:00
|
|
|
|
|
2026-04-03 17:58:14 +02:00
|
|
|
|
let index = 0;
|
|
|
|
|
|
const maxElements = 4;
|
|
|
|
|
|
let lastItemDiv = null;
|
|
|
|
|
|
|
|
|
|
|
|
while (index < selectedCardFromList.length && index < maxElements) {
|
2026-04-07 22:50:42 +02:00
|
|
|
|
const iconCell = document.createElement('div');
|
|
|
|
|
|
const icon = selectedCardFromList[index].getElementsByClassName('file-icon').item(0)?.cloneNode(true);
|
2026-04-03 17:58:14 +02:00
|
|
|
|
if (icon) {
|
2026-04-07 22:48:59 +02:00
|
|
|
|
iconCell.appendChild(icon);
|
|
|
|
|
|
iconCell.querySelectorAll('img')?.forEach((img) => {
|
|
|
|
|
|
img.loading = 'eager';
|
|
|
|
|
|
});
|
2026-02-21 13:39:27 +01:00
|
|
|
|
}
|
2025-03-19 23:28:29 +01:00
|
|
|
|
|
2026-04-07 22:50:42 +02:00
|
|
|
|
const nameCell = document.createElement('div');
|
|
|
|
|
|
const name = selectedCardFromList[index].getElementsByTagName('span').item(0)?.cloneNode(true);
|
2026-04-03 17:58:14 +02:00
|
|
|
|
if (name) {
|
|
|
|
|
|
nameCell.appendChild(name);
|
2026-03-25 18:49:38 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-07 22:50:42 +02:00
|
|
|
|
const div = document.createElement('div');
|
2026-04-07 22:48:59 +02:00
|
|
|
|
div.className = 'file-item';
|
2026-04-03 17:58:14 +02:00
|
|
|
|
div.appendChild(iconCell);
|
|
|
|
|
|
div.appendChild(nameCell);
|
2026-03-25 18:49:38 +01:00
|
|
|
|
|
2026-04-07 22:50:42 +02:00
|
|
|
|
this.draggedItems.appendChild(div);
|
2026-04-03 17:58:14 +02:00
|
|
|
|
index += 1;
|
|
|
|
|
|
lastItemDiv = div;
|
|
|
|
|
|
}
|
2026-03-25 18:49:38 +01:00
|
|
|
|
|
2026-04-03 17:58:14 +02:00
|
|
|
|
// if more than 1 item, display the badge
|
|
|
|
|
|
if (selectedCardFromList.length > 1) {
|
2026-04-07 22:50:42 +02:00
|
|
|
|
const badge = document.createElement('span');
|
2026-04-07 22:48:59 +02:00
|
|
|
|
badge.className = 'dragged-items-badge';
|
|
|
|
|
|
badge.innerText = `${selectedCardFromList.length}`;
|
2026-04-07 22:50:42 +02:00
|
|
|
|
this.draggedItems.appendChild(badge);
|
2026-04-03 17:58:14 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// if more than maxElements display the fading
|
|
|
|
|
|
if (selectedCardFromList.length > maxElements) {
|
2026-04-07 22:48:59 +02:00
|
|
|
|
lastItemDiv.classList.add('fading');
|
2026-04-03 17:58:14 +02:00
|
|
|
|
}
|
2026-04-07 22:48:59 +02:00
|
|
|
|
|
2026-04-07 22:50:42 +02:00
|
|
|
|
this.dragPreview.appendChild(this.draggedItems);
|
|
|
|
|
|
e.dataTransfer.setDragImage(this.draggedItems, 0, 0);
|
2026-04-03 17:58:14 +02:00
|
|
|
|
});
|
2025-03-19 23:28:29 +01:00
|
|
|
|
|
2026-04-03 17:58:14 +02:00
|
|
|
|
// dragend
|
2026-04-07 22:50:42 +02:00
|
|
|
|
filesList.addEventListener('dragend', (_e) => {
|
|
|
|
|
|
this.dragPreview.removeChild(this.draggedItems);
|
|
|
|
|
|
document.querySelectorAll('.drop-target').forEach((el) => {
|
|
|
|
|
|
el.classList.remove('drop-target');
|
|
|
|
|
|
});
|
2026-04-03 17:58:14 +02:00
|
|
|
|
});
|
2025-03-19 23:28:29 +01:00
|
|
|
|
|
2026-04-03 17:58:14 +02:00
|
|
|
|
// dragover – only folders are valid drop targets
|
|
|
|
|
|
filesList.addEventListener('dragover', (e) => {
|
|
|
|
|
|
const card = e.target.closest('.file-item');
|
|
|
|
|
|
if (!card || card.dataset.fileId) return;
|
|
|
|
|
|
if (!card.dataset.folderId) return;
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
card.classList.add('drop-target');
|
|
|
|
|
|
});
|
2025-03-19 23:28:29 +01:00
|
|
|
|
|
2026-04-03 17:58:14 +02:00
|
|
|
|
// dragleave
|
|
|
|
|
|
filesList.addEventListener('dragleave', (e) => {
|
|
|
|
|
|
const card = e.target.closest('.file-item');
|
|
|
|
|
|
if (!card || card.dataset.fileId) return;
|
|
|
|
|
|
card.classList.remove('drop-target');
|
|
|
|
|
|
});
|
2025-03-19 23:28:29 +01:00
|
|
|
|
|
2026-04-03 17:58:14 +02:00
|
|
|
|
// drop – only folders accept drops
|
|
|
|
|
|
filesList.addEventListener('drop', async (e) => {
|
|
|
|
|
|
const card = e.target.closest('.file-item');
|
|
|
|
|
|
if (!card || card.dataset.fileId) return;
|
|
|
|
|
|
const targetFolderId = card.dataset.folderId;
|
|
|
|
|
|
if (!targetFolderId) return;
|
2026-02-16 13:10:44 +01:00
|
|
|
|
|
2026-04-03 17:58:14 +02:00
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
card.classList.remove('drop-target');
|
2026-02-16 13:10:44 +01:00
|
|
|
|
|
2026-04-03 17:58:14 +02:00
|
|
|
|
const action = e.dataTransfer.dropEffect;
|
2026-04-07 22:50:42 +02:00
|
|
|
|
await this._dropToFolder(action, targetFolderId, e.dataTransfer);
|
2026-04-03 17:58:14 +02:00
|
|
|
|
});
|
2026-02-16 13:10:44 +01:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/* ================================================================
|
2026-02-18 10:53:43 +01:00
|
|
|
|
* Favorite star helper – attaches a direct click handler to a
|
|
|
|
|
|
* star <button> so the event never bubbles to the card.
|
|
|
|
|
|
* ================================================================ */
|
|
|
|
|
|
_bindStarClick(el) {
|
|
|
|
|
|
const star = el.querySelector('.favorite-star');
|
|
|
|
|
|
if (!star) return;
|
|
|
|
|
|
|
|
|
|
|
|
star.addEventListener('click', (e) => {
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
e.stopImmediatePropagation();
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
|
|
if (!window.favorites) return;
|
|
|
|
|
|
|
2026-04-07 22:48:59 +02:00
|
|
|
|
const itemId = star.dataset.itemId;
|
2026-02-18 10:53:43 +01:00
|
|
|
|
const itemType = star.dataset.itemType;
|
|
|
|
|
|
const itemName = star.dataset.itemName;
|
|
|
|
|
|
|
|
|
|
|
|
const isActive = star.classList.contains('active');
|
|
|
|
|
|
|
|
|
|
|
|
if (isActive) {
|
|
|
|
|
|
this.setFavoriteVisualState(itemId, itemType, false);
|
|
|
|
|
|
window.favorites.removeFromFavorites(itemId, itemType);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.setFavoriteVisualState(itemId, itemType, true);
|
|
|
|
|
|
window.favorites.addToFavorites(itemId, itemName, itemType);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Keep context-menu label in sync if available
|
|
|
|
|
|
if (window.contextMenus && typeof window.contextMenus.syncFavoriteOptionLabels === 'function') {
|
|
|
|
|
|
window.contextMenus.syncFavoriteOptionLabels();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Sync favorite visuals for a file/folder across grid and list views.
|
|
|
|
|
|
*/
|
|
|
|
|
|
setFavoriteVisualState(itemId, itemType, isFavorite) {
|
2026-04-07 22:48:59 +02:00
|
|
|
|
const selector = itemType === 'folder' ? `#files-list .file-item[data-folder-id="${itemId}"]` : `#files-list .file-item[data-file-id="${itemId}"]`;
|
2026-02-18 10:53:43 +01:00
|
|
|
|
|
2026-04-03 17:58:14 +02:00
|
|
|
|
const card = document.querySelector(selector);
|
2026-02-18 10:53:43 +01:00
|
|
|
|
const starBtn = card ? card.querySelector('.favorite-star') : null;
|
|
|
|
|
|
|
|
|
|
|
|
if (starBtn) {
|
|
|
|
|
|
starBtn.classList.toggle('active', !!isFavorite);
|
|
|
|
|
|
|
|
|
|
|
|
// SVG icon path (after icons.js replacement)
|
|
|
|
|
|
const svg = starBtn.querySelector('svg');
|
2026-04-07 22:50:42 +02:00
|
|
|
|
const filledPath = window.OxiIcons?.star;
|
|
|
|
|
|
const outlinePath = window.OxiIcons?.['star-outline'];
|
2026-02-18 10:53:43 +01:00
|
|
|
|
const targetPath = isFavorite ? filledPath : outlinePath;
|
|
|
|
|
|
if (svg && targetPath) {
|
|
|
|
|
|
const p = svg.querySelector('path');
|
|
|
|
|
|
if (p) p.setAttribute('d', targetPath[1]);
|
|
|
|
|
|
svg.setAttribute('viewBox', `0 0 ${targetPath[0]} 512`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Fallback <i> icon (before icons.js replacement)
|
|
|
|
|
|
const i = starBtn.querySelector('i');
|
|
|
|
|
|
if (i) {
|
|
|
|
|
|
i.classList.remove('fas', 'far');
|
|
|
|
|
|
i.classList.add(isFavorite ? 'fas' : 'far');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-07 22:48:59 +02:00
|
|
|
|
|
2026-04-03 17:58:14 +02:00
|
|
|
|
const listItem = document.querySelector(selector);
|
2026-02-18 10:53:43 +01:00
|
|
|
|
if (listItem) {
|
|
|
|
|
|
const nameCell = listItem.querySelector('.name-cell');
|
|
|
|
|
|
if (nameCell) {
|
|
|
|
|
|
let inlineStar = nameCell.querySelector('.favorite-star-inline');
|
|
|
|
|
|
if (isFavorite && !inlineStar) {
|
|
|
|
|
|
inlineStar = document.createElement('i');
|
|
|
|
|
|
inlineStar.className = 'fas fa-star favorite-star-inline';
|
|
|
|
|
|
nameCell.appendChild(inlineStar);
|
|
|
|
|
|
if (window.OxiIcons && typeof window.OxiIcons.replaceIconsInElement === 'function') {
|
|
|
|
|
|
window.OxiIcons.replaceIconsInElement(nameCell);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (!isFavorite && inlineStar) {
|
|
|
|
|
|
inlineStar.remove();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/* ================================================================
|
|
|
|
|
|
* Element-creation helpers
|
2026-02-16 13:10:44 +01:00
|
|
|
|
* ================================================================ */
|
|
|
|
|
|
|
|
|
|
|
|
/** Create a list row for a folder */
|
|
|
|
|
|
_createFolderItem(folder) {
|
|
|
|
|
|
const el = document.createElement('div');
|
|
|
|
|
|
el.className = 'file-item';
|
2026-04-07 22:48:59 +02:00
|
|
|
|
el.dataset.folderId = folder.id;
|
2026-02-16 13:10:44 +01:00
|
|
|
|
el.dataset.folderName = folder.name;
|
2026-04-07 22:48:59 +02:00
|
|
|
|
el.dataset.parentId = folder.parent_id || '';
|
2026-02-16 13:10:44 +01:00
|
|
|
|
|
2026-04-07 22:50:42 +02:00
|
|
|
|
const isFav = window.favorites?.isFavorite(folder.id, 'folder');
|
2026-02-16 13:10:44 +01:00
|
|
|
|
const formattedDate = window.formatDateTime(folder.modified_at);
|
|
|
|
|
|
|
|
|
|
|
|
el.innerHTML = `
|
2026-04-03 17:58:14 +02:00
|
|
|
|
<div class="checkbox-cell"><input type="checkbox" class="item-checkbox"></div>
|
2025-03-19 23:28:29 +01:00
|
|
|
|
<div class="name-cell">
|
|
|
|
|
|
<div class="file-icon folder-icon">
|
|
|
|
|
|
<i class="fas fa-folder"></i>
|
|
|
|
|
|
</div>
|
2026-02-15 17:53:25 +01:00
|
|
|
|
<span>${escapeHtml(folder.name)}</span>
|
2026-02-16 13:10:44 +01:00
|
|
|
|
${isFav ? '<i class="fas fa-star favorite-star-inline"></i>' : ''}
|
2025-03-19 23:28:29 +01:00
|
|
|
|
</div>
|
2026-02-12 09:41:25 +01:00
|
|
|
|
<div class="type-cell">${window.i18n ? window.i18n.t('files.file_types.folder') : 'Folder'}</div>
|
2025-03-19 23:28:29 +01:00
|
|
|
|
<div class="size-cell">--</div>
|
|
|
|
|
|
<div class="date-cell">${formattedDate}</div>
|
2026-04-03 17:58:14 +02:00
|
|
|
|
<div class="action-cell">
|
|
|
|
|
|
<button class="favorite-star${isFav ? ' active' : ''}" data-item-id="${folder.id}" data-item-type="folder" data-item-name="${escapeHtml(folder.name)}">
|
|
|
|
|
|
<i class="${isFav ? 'fas' : 'far'} fa-star"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="file-actions"><i class="fas fa-ellipsis-v"></i></button>
|
2025-03-19 23:28:29 +01:00
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
2026-04-03 17:58:14 +02:00
|
|
|
|
|
2026-04-07 22:48:59 +02:00
|
|
|
|
if (window.app.currentPath !== '') {
|
2026-04-03 17:58:14 +02:00
|
|
|
|
el.setAttribute('draggable', 'true');
|
|
|
|
|
|
}
|
2026-02-18 10:53:43 +01:00
|
|
|
|
this._bindStarClick(el);
|
2026-02-16 13:10:44 +01:00
|
|
|
|
return el;
|
|
|
|
|
|
},
|
2025-03-19 23:28:29 +01:00
|
|
|
|
|
2026-04-03 17:58:14 +02:00
|
|
|
|
/** Create a grid card for a file */
|
2026-02-16 13:10:44 +01:00
|
|
|
|
_createFileItem(file) {
|
2026-02-18 10:53:43 +01:00
|
|
|
|
const iconClass = file.icon_class || this.getIconClass(file.name);
|
|
|
|
|
|
const iconSpecialClass = file.icon_special_class || this.getIconSpecialClass(file.name);
|
2026-02-16 13:10:44 +01:00
|
|
|
|
const cat = file.category || '';
|
|
|
|
|
|
const typeLabel = cat
|
2026-04-07 22:48:59 +02:00
|
|
|
|
? window.i18n
|
|
|
|
|
|
? window.i18n.t(`files.file_types.${cat.toLowerCase()}`) || cat
|
|
|
|
|
|
: cat
|
|
|
|
|
|
: window.i18n
|
|
|
|
|
|
? window.i18n.t('files.file_types.document')
|
|
|
|
|
|
: 'Document';
|
2026-02-16 13:10:44 +01:00
|
|
|
|
const fileSize = file.size_formatted || window.formatFileSize(file.size);
|
|
|
|
|
|
const formattedDate = window.formatDateTime(file.modified_at);
|
2026-04-07 22:50:42 +02:00
|
|
|
|
const isFav = window.favorites?.isFavorite(file.id, 'file');
|
2025-03-19 23:28:29 +01:00
|
|
|
|
|
2026-02-16 13:10:44 +01:00
|
|
|
|
const el = document.createElement('div');
|
|
|
|
|
|
el.className = 'file-item';
|
2026-04-07 22:48:59 +02:00
|
|
|
|
el.dataset.fileId = file.id;
|
2026-02-16 13:10:44 +01:00
|
|
|
|
el.dataset.fileName = file.name;
|
2026-04-07 22:48:59 +02:00
|
|
|
|
el.dataset.folderId = file.folder_id || '';
|
2026-02-16 13:10:44 +01:00
|
|
|
|
el.setAttribute('draggable', 'true');
|
2025-03-19 23:28:29 +01:00
|
|
|
|
|
2026-02-16 13:10:44 +01:00
|
|
|
|
el.innerHTML = `
|
2026-04-07 22:50:42 +02:00
|
|
|
|
|
2026-04-03 17:58:14 +02:00
|
|
|
|
<div class="checkbox-cell"><input type="checkbox" class="item-checkbox"></div>
|
2025-03-19 23:28:29 +01:00
|
|
|
|
<div class="name-cell">
|
|
|
|
|
|
<div class="file-icon ${iconSpecialClass}">
|
2026-03-05 16:18:30 -05:00
|
|
|
|
${iconSpecialClass === 'image-icon' ? `<img class="file-thumb" src="/api/files/${file.id}/thumbnail/icon" loading="lazy" alt="">` : ''}
|
2025-03-19 23:28:29 +01:00
|
|
|
|
<i class="${iconClass}"></i>
|
|
|
|
|
|
</div>
|
2026-02-15 17:53:25 +01:00
|
|
|
|
<span>${escapeHtml(file.name)}</span>
|
2026-04-03 17:58:14 +02:00
|
|
|
|
${isFav ? '<i class="fas fa-star favorite-star-inline"></i>' : ''}
|
2025-03-19 23:28:29 +01:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="type-cell">${typeLabel}</div>
|
|
|
|
|
|
<div class="size-cell">${fileSize}</div>
|
|
|
|
|
|
<div class="date-cell">${formattedDate}</div>
|
2026-04-03 17:58:14 +02:00
|
|
|
|
<div class="action-cell">
|
|
|
|
|
|
<button class="favorite-star${isFav ? ' active' : ''}" data-item-id="${file.id}" data-item-type="file" data-item-name="${escapeHtml(file.name)}">
|
|
|
|
|
|
<i class="${isFav ? 'fas' : 'far'} fa-star"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="file-actions"><i class="fas fa-ellipsis-v"></i></button>
|
|
|
|
|
|
</div>
|
2025-03-19 23:28:29 +01:00
|
|
|
|
`;
|
2026-03-05 16:18:30 -05:00
|
|
|
|
var thumb = el.querySelector('.file-thumb');
|
2026-04-07 22:48:59 +02:00
|
|
|
|
if (thumb)
|
|
|
|
|
|
thumb.addEventListener('error', function () {
|
|
|
|
|
|
this.style.display = 'none';
|
|
|
|
|
|
});
|
2026-04-03 17:58:14 +02:00
|
|
|
|
this._bindStarClick(el);
|
2026-02-16 13:10:44 +01:00
|
|
|
|
return el;
|
|
|
|
|
|
},
|
2025-03-19 23:28:29 +01:00
|
|
|
|
|
2026-02-16 13:10:44 +01:00
|
|
|
|
/* ================================================================
|
|
|
|
|
|
* Batch rendering with DocumentFragment
|
|
|
|
|
|
* ================================================================ */
|
2026-02-14 00:12:18 +01:00
|
|
|
|
|
2026-04-03 17:58:14 +02:00
|
|
|
|
resetFilesList() {
|
|
|
|
|
|
const filesList = document.getElementById('files-list');
|
2026-04-07 22:48:59 +02:00
|
|
|
|
const filesContainerError = document.getElementById('files-container-error');
|
2026-04-03 17:58:14 +02:00
|
|
|
|
|
|
|
|
|
|
if (!filesList) return;
|
2026-04-05 01:49:52 +02:00
|
|
|
|
|
2026-04-07 22:48:59 +02:00
|
|
|
|
filesList.innerHTML = `
|
2026-04-03 17:58:14 +02:00
|
|
|
|
<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">Name</div>
|
|
|
|
|
|
<div data-i18n="files.type">Type</div>
|
|
|
|
|
|
<div data-i18n="files.size">Size</div>
|
|
|
|
|
|
<div data-i18n="files.modified">Modified</div>
|
|
|
|
|
|
<div></div><!-- actions -->
|
|
|
|
|
|
</div>`;
|
|
|
|
|
|
|
2026-04-07 22:50:42 +02:00
|
|
|
|
if (window.i18n?.translateElement) window.i18n.translateElement(filesList);
|
2026-04-07 22:48:59 +02:00
|
|
|
|
|
|
|
|
|
|
filesList.classList.remove('hidden');
|
|
|
|
|
|
filesContainerError?.classList.add('hidden');
|
2026-04-03 17:58:14 +02:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
showEmptyList() {
|
|
|
|
|
|
this.showError(`
|
|
|
|
|
|
<i class="fas fa-folder-open empty-state-icon"></i>
|
|
|
|
|
|
<p data-i18n="files.no_files"></p>
|
|
|
|
|
|
<p data-i18n="files.empty_hint"></p>
|
2026-04-07 22:48:59 +02:00
|
|
|
|
`);
|
2026-04-03 17:58:14 +02:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {string} content
|
|
|
|
|
|
*
|
|
|
|
|
|
*/
|
|
|
|
|
|
showError(content) {
|
2026-04-07 22:48:59 +02:00
|
|
|
|
const filesContainerError = document.getElementById('files-container-error');
|
|
|
|
|
|
const filesList = document.getElementById('files-list');
|
|
|
|
|
|
if (filesContainerError) filesContainerError.innerHTML = content;
|
2026-04-03 17:58:14 +02:00
|
|
|
|
|
2026-04-07 22:50:42 +02:00
|
|
|
|
if (window.i18n?.translateElement) window.i18n.translateElement(filesContainerError);
|
2026-04-03 17:58:14 +02:00
|
|
|
|
|
2026-04-07 22:48:59 +02:00
|
|
|
|
filesContainerError?.classList.remove('hidden');
|
|
|
|
|
|
filesList?.classList.add('hidden');
|
2026-04-03 17:58:14 +02:00
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-16 13:10:44 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* Render an array of folders into both grid and list views
|
|
|
|
|
|
* using DocumentFragment for minimal reflows.
|
|
|
|
|
|
*/
|
|
|
|
|
|
renderFolders(folders) {
|
|
|
|
|
|
if (!this._delegationReady) this.initDelegation();
|
2026-02-18 10:53:43 +01:00
|
|
|
|
const safeFolders = Array.isArray(folders) ? folders : [];
|
|
|
|
|
|
this._lastFolders = safeFolders.slice();
|
2026-02-16 13:10:44 +01:00
|
|
|
|
|
2026-02-18 10:53:43 +01:00
|
|
|
|
for (const folder of safeFolders) {
|
2026-02-16 13:10:44 +01:00
|
|
|
|
this._items.set(folder.id, folder);
|
|
|
|
|
|
}
|
2025-03-19 23:28:29 +01:00
|
|
|
|
|
2026-04-03 17:58:14 +02:00
|
|
|
|
this._renderFoldersToView(safeFolders);
|
2026-02-16 13:10:44 +01:00
|
|
|
|
},
|
2025-03-19 23:28:29 +01:00
|
|
|
|
|
2026-02-16 13:10:44 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* Render an array of files into both grid and list views
|
|
|
|
|
|
* using DocumentFragment for minimal reflows.
|
|
|
|
|
|
*/
|
|
|
|
|
|
renderFiles(files) {
|
|
|
|
|
|
if (!this._delegationReady) this.initDelegation();
|
2026-02-18 10:53:43 +01:00
|
|
|
|
const safeFiles = Array.isArray(files) ? files : [];
|
|
|
|
|
|
this._lastFiles = safeFiles.slice();
|
2026-02-16 13:10:44 +01:00
|
|
|
|
|
2026-02-18 10:53:43 +01:00
|
|
|
|
for (const file of safeFiles) {
|
2026-02-16 13:10:44 +01:00
|
|
|
|
this._items.set(file.id, file);
|
|
|
|
|
|
}
|
2025-03-19 23:28:29 +01:00
|
|
|
|
|
2026-04-03 17:58:14 +02:00
|
|
|
|
this._renderFilesToView(safeFiles);
|
2026-02-16 13:10:44 +01:00
|
|
|
|
},
|
2025-03-19 23:28:29 +01:00
|
|
|
|
|
2026-02-16 13:10:44 +01:00
|
|
|
|
/* ================================================================
|
|
|
|
|
|
* Single-item add (backward-compatible API for post-upload, etc.)
|
|
|
|
|
|
* ================================================================ */
|
2025-03-19 23:28:29 +01:00
|
|
|
|
|
2026-02-16 13:10:44 +01:00
|
|
|
|
/**
|
2026-02-18 10:53:43 +01:00
|
|
|
|
* Add a single folder to the active view.
|
2026-02-16 13:10:44 +01:00
|
|
|
|
* @param {Object} folder - Folder object
|
|
|
|
|
|
*/
|
|
|
|
|
|
addFolderToView(folder) {
|
|
|
|
|
|
if (!this._delegationReady) this.initDelegation();
|
2025-03-19 23:28:29 +01:00
|
|
|
|
|
2026-02-16 13:10:44 +01:00
|
|
|
|
// Duplicate guard
|
2026-04-03 17:58:14 +02:00
|
|
|
|
if (document.querySelector(`.file-item[data-folder-id="${folder.id}"]`)) {
|
2026-02-16 13:10:44 +01:00
|
|
|
|
console.log(`Folder ${folder.name} (${folder.id}) already exists in the view, not duplicating`);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this._items.set(folder.id, folder);
|
2026-02-18 10:53:43 +01:00
|
|
|
|
this._upsertById(this._lastFolders, folder);
|
2026-04-03 17:58:14 +02:00
|
|
|
|
this._renderFoldersToView([folder]);
|
2026-02-16 13:10:44 +01:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-18 10:53:43 +01:00
|
|
|
|
* Add a single file to the active view.
|
2026-02-16 13:10:44 +01:00
|
|
|
|
* @param {Object} file - File object
|
|
|
|
|
|
*/
|
|
|
|
|
|
addFileToView(file) {
|
|
|
|
|
|
if (!this._delegationReady) this.initDelegation();
|
|
|
|
|
|
|
|
|
|
|
|
// Duplicate guard
|
2026-04-03 17:58:14 +02:00
|
|
|
|
if (document.querySelector(`.file-item[data-file-id="${file.id}"]`)) {
|
2026-02-16 13:10:44 +01:00
|
|
|
|
console.log(`File ${file.name} (${file.id}) already exists in the view, not duplicating`);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-03-19 23:28:29 +01:00
|
|
|
|
|
2026-02-16 13:10:44 +01:00
|
|
|
|
this._items.set(file.id, file);
|
2026-02-18 10:53:43 +01:00
|
|
|
|
this._upsertById(this._lastFiles, file);
|
2026-04-03 17:58:14 +02:00
|
|
|
|
this._renderFilesToView([file]);
|
2025-03-19 23:28:29 +01:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-08 22:44:42 +01:00
|
|
|
|
// --- Global helper functions for card interactions ---
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Toggle selection state of a file/folder card.
|
2026-02-14 00:12:18 +01:00
|
|
|
|
* Routes through the multiSelect module so batch actions know about selected items.
|
2026-02-08 22:44:42 +01:00
|
|
|
|
*/
|
|
|
|
|
|
function toggleCardSelection(card, event) {
|
2026-02-14 00:12:18 +01:00
|
|
|
|
if (window.multiSelect) {
|
2026-04-05 01:49:52 +02:00
|
|
|
|
window.multiSelect.handleToggleItem(card, event);
|
2026-02-14 00:12:18 +01:00
|
|
|
|
} else {
|
|
|
|
|
|
card.classList.toggle('selected');
|
|
|
|
|
|
}
|
2026-02-08 22:44:42 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Show the context menu anchored next to a trigger element (the 3-dot button).
|
|
|
|
|
|
*/
|
|
|
|
|
|
function showContextMenuAtElement(triggerElement, menuId) {
|
|
|
|
|
|
// Hide any open menus first
|
2026-04-07 22:50:42 +02:00
|
|
|
|
document.querySelectorAll('.context-menu').forEach((m) => {
|
|
|
|
|
|
m.style.display = 'none';
|
|
|
|
|
|
});
|
2026-02-08 22:44:42 +01:00
|
|
|
|
|
|
|
|
|
|
const menu = document.getElementById(menuId);
|
|
|
|
|
|
if (!menu) return;
|
|
|
|
|
|
|
|
|
|
|
|
const rect = triggerElement.getBoundingClientRect();
|
|
|
|
|
|
const menuWidth = 200; // approximate
|
|
|
|
|
|
|
|
|
|
|
|
// Position below the trigger, aligned to the right edge
|
|
|
|
|
|
let left = rect.right - menuWidth + window.scrollX;
|
|
|
|
|
|
let top = rect.bottom + 4 + window.scrollY;
|
|
|
|
|
|
|
|
|
|
|
|
// Keep inside viewport
|
|
|
|
|
|
if (left < 8) left = 8;
|
|
|
|
|
|
if (top + 300 > window.innerHeight + window.scrollY) {
|
|
|
|
|
|
top = rect.top - 4 + window.scrollY; // flip above if no room
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-18 10:53:43 +01:00
|
|
|
|
if (window.contextMenus && typeof window.contextMenus.syncFavoriteOptionLabels === 'function') {
|
|
|
|
|
|
window.contextMenus.syncFavoriteOptionLabels();
|
|
|
|
|
|
}
|
2026-02-21 13:39:27 +01:00
|
|
|
|
if (window.contextMenus && typeof window.contextMenus.syncWopiOptionVisibility === 'function') {
|
2026-04-07 22:50:42 +02:00
|
|
|
|
window.contextMenus.syncWopiOptionVisibility().catch(() => {});
|
2026-02-21 13:39:27 +01:00
|
|
|
|
}
|
2026-02-18 10:53:43 +01:00
|
|
|
|
|
2026-02-08 22:44:42 +01:00
|
|
|
|
menu.style.left = `${left}px`;
|
|
|
|
|
|
menu.style.top = `${top}px`;
|
|
|
|
|
|
menu.style.display = 'block';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Rubber band (lasso) selection — click + drag on empty grid area
|
|
|
|
|
|
* to draw a rectangle and select all cards it touches.
|
|
|
|
|
|
*/
|
|
|
|
|
|
function initRubberBandSelection() {
|
|
|
|
|
|
// Create the visual rectangle element once
|
|
|
|
|
|
let selRect = document.getElementById('selection-rect');
|
|
|
|
|
|
if (!selRect) {
|
|
|
|
|
|
selRect = document.createElement('div');
|
|
|
|
|
|
selRect.id = 'selection-rect';
|
|
|
|
|
|
selRect.className = 'selection-rect';
|
|
|
|
|
|
document.body.appendChild(selRect);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let active = false;
|
2026-04-07 22:48:59 +02:00
|
|
|
|
let startX = 0,
|
|
|
|
|
|
startY = 0;
|
2026-02-08 22:44:42 +01:00
|
|
|
|
|
|
|
|
|
|
// We listen on the whole files-container (covers grid + empty space)
|
2026-04-03 17:58:14 +02:00
|
|
|
|
const container = document.querySelector('.files-container') || document.getElementById('files-list');
|
2026-02-08 22:44:42 +01:00
|
|
|
|
if (!container) return;
|
|
|
|
|
|
|
|
|
|
|
|
container.addEventListener('mousedown', (e) => {
|
|
|
|
|
|
// Only start if clicking empty area (not on a card, button, menu, input…)
|
|
|
|
|
|
if (e.button !== 0) return; // left click only
|
2026-04-07 22:48:59 +02:00
|
|
|
|
if (
|
|
|
|
|
|
e.target.closest('.file-item') ||
|
2026-03-05 21:07:19 -05:00
|
|
|
|
e.target.closest('.context-menu') ||
|
2026-04-07 22:48:59 +02:00
|
|
|
|
e.target.closest('.upload-dropdown') ||
|
|
|
|
|
|
e.target.closest('button') ||
|
|
|
|
|
|
e.target.closest('input') ||
|
|
|
|
|
|
e.target.closest('.breadcrumb') ||
|
|
|
|
|
|
e.target.closest('.list-header')
|
|
|
|
|
|
)
|
|
|
|
|
|
return;
|
2026-02-08 22:44:42 +01:00
|
|
|
|
|
|
|
|
|
|
active = true;
|
|
|
|
|
|
startX = e.clientX;
|
|
|
|
|
|
startY = e.clientY;
|
|
|
|
|
|
|
|
|
|
|
|
selRect.style.left = `${startX}px`;
|
|
|
|
|
|
selRect.style.top = `${startY}px`;
|
|
|
|
|
|
selRect.style.width = '0px';
|
|
|
|
|
|
selRect.style.height = '0px';
|
|
|
|
|
|
selRect.style.display = 'none'; // show only after a small movement
|
|
|
|
|
|
|
|
|
|
|
|
e.preventDefault(); // prevent text selection
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('mousemove', (e) => {
|
|
|
|
|
|
if (!active) return;
|
|
|
|
|
|
|
|
|
|
|
|
const curX = e.clientX;
|
|
|
|
|
|
const curY = e.clientY;
|
|
|
|
|
|
|
|
|
|
|
|
const left = Math.min(startX, curX);
|
|
|
|
|
|
const top = Math.min(startY, curY);
|
|
|
|
|
|
const width = Math.abs(curX - startX);
|
|
|
|
|
|
const height = Math.abs(curY - startY);
|
|
|
|
|
|
|
|
|
|
|
|
// Only show the rect after a small threshold to avoid flicker on click
|
|
|
|
|
|
if (width > 5 || height > 5) {
|
|
|
|
|
|
selRect.style.display = 'block';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
selRect.style.left = `${left}px`;
|
|
|
|
|
|
selRect.style.top = `${top}px`;
|
|
|
|
|
|
selRect.style.width = `${width}px`;
|
|
|
|
|
|
selRect.style.height = `${height}px`;
|
|
|
|
|
|
|
|
|
|
|
|
// Highlight cards that intersect with the rectangle
|
|
|
|
|
|
const rectBounds = { left, top, right: left + width, bottom: top + height };
|
|
|
|
|
|
|
2026-04-07 22:48:59 +02:00
|
|
|
|
document.querySelectorAll('#files-list .file-item').forEach((card) => {
|
2026-02-08 22:44:42 +01:00
|
|
|
|
const cardRect = card.getBoundingClientRect();
|
|
|
|
|
|
const intersects =
|
2026-04-07 22:48:59 +02:00
|
|
|
|
cardRect.left < rectBounds.right && cardRect.right > rectBounds.left && cardRect.top < rectBounds.bottom && cardRect.bottom > rectBounds.top;
|
2026-02-08 22:44:42 +01:00
|
|
|
|
|
|
|
|
|
|
if (intersects) {
|
|
|
|
|
|
card.classList.add('selected');
|
2026-03-25 18:49:38 +01:00
|
|
|
|
|
2026-02-16 00:22:42 +01:00
|
|
|
|
// Sync with multiSelect module
|
|
|
|
|
|
if (window.multiSelect) {
|
|
|
|
|
|
const info = window.multiSelect._extractInfo(card);
|
|
|
|
|
|
if (info) window.multiSelect.select(info.id, info.name, info.type, info.parentId);
|
|
|
|
|
|
}
|
2026-02-08 22:44:42 +01:00
|
|
|
|
} else {
|
|
|
|
|
|
card.classList.remove('selected');
|
2026-02-16 00:22:42 +01:00
|
|
|
|
// Deselect from multiSelect module
|
|
|
|
|
|
if (window.multiSelect) {
|
2026-04-07 22:48:59 +02:00
|
|
|
|
const info = window.multiSelect._extractInfo(card);
|
2026-02-16 00:22:42 +01:00
|
|
|
|
if (info) window.multiSelect.deselect(info.id);
|
|
|
|
|
|
}
|
2026-02-08 22:44:42 +01:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('mouseup', () => {
|
|
|
|
|
|
if (!active) return;
|
|
|
|
|
|
active = false;
|
2026-03-05 21:07:19 -05:00
|
|
|
|
const hadSelection = selRect.style.display === 'block';
|
2026-02-08 22:44:42 +01:00
|
|
|
|
selRect.style.display = 'none';
|
2026-02-16 00:22:42 +01:00
|
|
|
|
// Update the batch bar after rubber band selection completes
|
|
|
|
|
|
if (window.multiSelect) window.multiSelect._syncUI();
|
2026-03-05 21:07:19 -05:00
|
|
|
|
// Suppress the click event that follows mouseup so the global
|
|
|
|
|
|
// deselect handler doesn't immediately clear the selection.
|
|
|
|
|
|
if (hadSelection) {
|
|
|
|
|
|
window.__rubberBandJustFinished = true;
|
2026-04-07 22:48:59 +02:00
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
|
window.__rubberBandJustFinished = false;
|
|
|
|
|
|
});
|
2026-03-05 21:07:19 -05:00
|
|
|
|
}
|
2026-02-08 22:44:42 +01:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Initialize rubber band once DOM is ready
|
|
|
|
|
|
if (document.readyState === 'loading') {
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', initRubberBandSelection);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
initRubberBandSelection();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Expose helpers globally
|
|
|
|
|
|
window.toggleCardSelection = toggleCardSelection;
|
|
|
|
|
|
window.showContextMenuAtElement = showContextMenuAtElement;
|
|
|
|
|
|
window.initRubberBandSelection = initRubberBandSelection;
|
2026-04-03 00:28:53 +02:00
|
|
|
|
|
2026-02-08 22:44:42 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* Show a modern confirm dialog (replaces native confirm())
|
|
|
|
|
|
* @param {Object} options
|
|
|
|
|
|
* @param {string} options.title - Dialog title
|
|
|
|
|
|
* @param {string} options.message - Dialog message/body
|
|
|
|
|
|
* @param {string} [options.confirmText='Confirmar'] - Text for confirm button
|
|
|
|
|
|
* @param {string} [options.cancelText='Cancelar'] - Text for cancel button
|
|
|
|
|
|
* @param {boolean} [options.danger=false] - Use danger styling (red)
|
|
|
|
|
|
* @returns {Promise<boolean>} true if confirmed, false if cancelled
|
|
|
|
|
|
*/
|
|
|
|
|
|
function showConfirmDialog({ title, message, confirmText, cancelText, danger = true } = {}) {
|
2026-02-12 09:41:25 +01:00
|
|
|
|
const ct = confirmText || (window.i18n ? window.i18n.t('actions.delete') : 'Delete');
|
|
|
|
|
|
const cc = cancelText || (window.i18n ? window.i18n.t('actions.cancel') : 'Cancel');
|
|
|
|
|
|
const t = title || (window.i18n ? window.i18n.t('dialogs.confirm_title') : 'Confirm action');
|
2026-02-08 22:44:42 +01:00
|
|
|
|
|
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
|
// Remove any previous confirm dialog
|
|
|
|
|
|
const prev = document.getElementById('confirm-dialog-overlay');
|
|
|
|
|
|
if (prev) prev.remove();
|
|
|
|
|
|
|
|
|
|
|
|
const overlay = document.createElement('div');
|
|
|
|
|
|
overlay.id = 'confirm-dialog-overlay';
|
|
|
|
|
|
overlay.className = 'confirm-dialog';
|
|
|
|
|
|
overlay.innerHTML = `
|
|
|
|
|
|
<div class="confirm-dialog-content">
|
|
|
|
|
|
<div class="confirm-dialog-icon">
|
|
|
|
|
|
<i class="fas ${danger ? 'fa-exclamation-triangle' : 'fa-question-circle'}"></i>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="confirm-dialog-title">${t}</div>
|
|
|
|
|
|
<div class="confirm-dialog-message">${message || ''}</div>
|
|
|
|
|
|
<div class="confirm-dialog-buttons">
|
|
|
|
|
|
<button class="btn btn-secondary confirm-dialog-cancel">${cc}</button>
|
|
|
|
|
|
<button class="btn ${danger ? 'btn-danger' : 'btn-primary'} confirm-dialog-ok">${ct}</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
document.body.appendChild(overlay);
|
|
|
|
|
|
|
|
|
|
|
|
// Force layout then show
|
2026-04-07 22:48:59 +02:00
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
|
overlay.classList.add('active');
|
|
|
|
|
|
});
|
2026-02-08 22:44:42 +01:00
|
|
|
|
|
|
|
|
|
|
const cleanup = (result) => {
|
|
|
|
|
|
overlay.classList.remove('active');
|
|
|
|
|
|
setTimeout(() => overlay.remove(), 200);
|
|
|
|
|
|
resolve(result);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
overlay.querySelector('.confirm-dialog-cancel').addEventListener('click', () => cleanup(false));
|
|
|
|
|
|
overlay.querySelector('.confirm-dialog-ok').addEventListener('click', () => cleanup(true));
|
2026-04-07 22:48:59 +02:00
|
|
|
|
overlay.addEventListener('click', (e) => {
|
|
|
|
|
|
if (e.target === overlay) cleanup(false);
|
|
|
|
|
|
});
|
2026-02-08 22:44:42 +01:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
window.showConfirmDialog = showConfirmDialog;
|
|
|
|
|
|
|
2025-03-19 23:28:29 +01:00
|
|
|
|
// Expose UI module globally
|
|
|
|
|
|
window.ui = ui;
|