/** * OxiCloud - Context Menus and Dialogs Module * This file handles context menus and dialog functionality */ import { resolveHomeFolder } from '../../app/authSession.js'; import { loadFiles } from '../../app/filesView.js'; import { switchToFilesSection } from '../../app/navigation.js'; import { app } from '../../app/state.js'; import { ui } from '../../app/ui.js'; import { Modal } from '../../components/modal.js'; import { shareModal } from '../../components/shareModal.js'; import { getCsrfHeaders } from '../../core/csrf.js'; import { escapeHtml } from '../../core/formatters.js'; import { i18n } from '../../core/i18n.js'; import { favorites } from '../library/favorites.js'; import { musicView } from '../library/music.js'; import { fileSharing } from '../sharing/fileSharing.js'; import { batchToolbar } from './batchToolbar.js'; import { fileOps } from './fileOperations.js'; import { inlineViewer } from './inlineViewer.js'; import { wopiEditor } from './wopiEditor.js'; /** * @import {FolderItem, FileItem, ItemTypeEnum, Playlist} from '../../core/types.js' */ /** @type {EventListener | null} */ let _moveDialogEscapeHandler = null; // Context Menus Module const contextMenus = { /** * @param {string} optionId * @param {boolean} isFavorite */ _setFavoriteOptionLabel(optionId, isFavorite) { const option = document.getElementById(optionId); if (!option) return; const label = option.querySelector('span'); if (!label) return; label.textContent = i18n.t(isFavorite ? 'actions.unfavorite' : 'actions.favorite'); }, /** * Show or hide WOPI editor options based on current target file */ async syncWopiOptionVisibility() { const wopiEdit = document.getElementById('wopi-edit-file-option'); const wopiEditTab = document.getElementById('wopi-edit-file-tab-option'); if (!wopiEdit || !wopiEditTab) return; const targetFile = app?.contextMenuTargetFile; // Don't show WOPI editor for image files - they should use inline preview const isImage = targetFile?.mime_type?.startsWith('image/'); const show = targetFile && !isImage && wopiEditor && (await wopiEditor.canEdit(targetFile.name)); wopiEdit.classList.toggle('hidden', !show); wopiEditTab.classList.toggle('hidden', !show); }, syncFavoriteOptionLabels() { if (!favorites) return; const targetFile = app?.contextMenuTargetFile; const targetFolder = app?.contextMenuTargetFolder; if (targetFile) { const isFav = favorites.isFavorite(targetFile.id, 'file'); this._setFavoriteOptionLabel('favorite-file-option', isFav); } if (targetFolder) { const isFav = favorites.isFavorite(targetFolder.id, 'folder'); this._setFavoriteOptionLabel('favorite-folder-option', isFav); } }, syncOpenParentFolderOption() { const option = document.getElementById('open-parent-folder-option'); if (!option) return; const folderId = app?.contextMenuTargetFile?.folder_id; const isFilesSection = app.currentSection === 'files'; option.classList.toggle('hidden', !folderId || isFilesSection); }, syncAddToPlaylistOption() { const option = document.getElementById('add-to-playlist-option'); if (!option) return; const targetFile = app?.contextMenuTargetFile; if (targetFile) { const isAudio = targetFile.mime_type?.startsWith('audio/'); option.classList.toggle('hidden', !isAudio); } else { option.classList.add('hidden'); } }, sync() { this.syncFavoriteOptionLabels(); this.syncWopiOptionVisibility().catch(() => {}); this.syncAddToPlaylistOption(); this.syncOpenParentFolderOption(); }, /** * Assign events to menu items and dialogs */ assignMenuEvents() { // Folder context menu options document.getElementById('download-folder-option').addEventListener('click', () => { if (app.contextMenuTargetFolder) { fileOps.downloadFolder(app.contextMenuTargetFolder.id, app.contextMenuTargetFolder.name); } ui.closeContextMenu(); }); document.getElementById('favorite-folder-option').addEventListener('click', async () => { if (app.contextMenuTargetFolder) { const folder = app.contextMenuTargetFolder; // Check if folder is already in favorites to toggle if (favorites?.isFavorite(folder.id, 'folder')) { // Remove from favorites const ok = await favorites.removeFromFavorites(folder.id, 'folder', folder.name); if (ok && ui && typeof ui.setFavoriteVisualState === 'function') { ui.setFavoriteVisualState(folder.id, 'folder', false); } } else { // Add to favorites const ok = await favorites.addToFavorites(folder.id, folder.name, 'folder', folder.parent_id); if (ok && ui && typeof ui.setFavoriteVisualState === 'function') { ui.setFavoriteVisualState(folder.id, 'folder', true); } } this.syncFavoriteOptionLabels(); } ui.closeContextMenu(); }); document.getElementById('rename-folder-option').addEventListener('click', async () => { const folder = app.contextMenuTargetFolder; ui.closeContextMenu(); if (!folder) return; const newName = await Modal.promptRename(folder.name, true, async (name) => { await fileOps.renameFolder(folder.id, name); }); if (newName) loadFiles(); }); document.getElementById('move-folder-option').addEventListener('click', () => { if (app.contextMenuTargetFolder) { this.showMoveDialog(app.contextMenuTargetFolder, 'folder'); } ui.closeContextMenu(); }); document.getElementById('share-folder-option').addEventListener('click', () => { const folder = app.contextMenuTargetFolder; if (folder) { shareModal.open(folder, 'folder'); } ui.closeContextMenu(); }); document.getElementById('delete-folder-option').addEventListener('click', async () => { const folder = app.contextMenuTargetFolder; ui.closeContextMenu(); if (folder) { await fileOps.deleteFolder(folder.id, folder.name); } }); // File context menu options document.getElementById('view-file-option').addEventListener('click', () => { if (app.contextMenuTargetFile) { // Capture reference before context menu cleanup nullifies it const file = app.contextMenuTargetFile; fetch(`/api/files/${file.id}?metadata=true`, { credentials: 'same-origin' }) .then((response) => response.json()) .then((fileDetails) => { // Check if viewable file type (images, PDFs, text files) if (ui?.isViewableFile(fileDetails)) { // Open with inline viewer if (inlineViewer) { inlineViewer.openFile(fileDetails); } else { // If no viewer is available, download directly fileOps.downloadFile(file.id, file.name); } } else { // For non-viewable files, download fileOps.downloadFile(file.id, file.name); } }) .catch((error) => { console.error('Error fetching file details:', error); // On error, fallback to download fileOps.downloadFile(file.id, file.name); }); } ui.closeFileContextMenu(); }); document.getElementById('wopi-edit-file-option').addEventListener('click', () => { if (app.contextMenuTargetFile) { const file = app.contextMenuTargetFile; wopiEditor.openInModal(file.id, file.name, 'edit'); } ui.closeFileContextMenu(); }); document.getElementById('wopi-edit-file-tab-option').addEventListener('click', () => { if (app.contextMenuTargetFile) { const file = app.contextMenuTargetFile; wopiEditor.openInTab(file.id, file.name, 'edit'); } ui.closeFileContextMenu(); }); document.getElementById('download-file-option').addEventListener('click', () => { if (app.contextMenuTargetFile) { fileOps.downloadFile(app.contextMenuTargetFile.id, app.contextMenuTargetFile.name); } ui.closeFileContextMenu(); }); document.getElementById('open-parent-folder-option').addEventListener('click', () => { const folderId = app.contextMenuTargetFile?.folder_id; ui.closeFileContextMenu(); if (folderId) { switchToFilesSection(); app.currentPath = folderId; loadFiles(); } }); document.getElementById('favorite-file-option').addEventListener('click', async () => { if (app.contextMenuTargetFile) { const file = app.contextMenuTargetFile; // Check if file is already in favorites to toggle if (favorites?.isFavorite(file.id, 'file')) { // Remove from favorites const ok = await favorites.removeFromFavorites(file.id, 'file', file.name); if (ok && ui && typeof ui.setFavoriteVisualState === 'function') { ui.setFavoriteVisualState(file.id, 'file', false); } } else { // Add to favorites const ok = await favorites.addToFavorites(file.id, file.name, 'file', file.folder_id); if (ok && ui && typeof ui.setFavoriteVisualState === 'function') { ui.setFavoriteVisualState(file.id, 'file', true); } } this.syncFavoriteOptionLabels(); } ui.closeFileContextMenu(); }); document.getElementById('rename-file-option').addEventListener('click', async () => { const file = app.contextMenuTargetFile; ui.closeFileContextMenu(); if (!file) return; const newName = await Modal.promptRename(file.name, false, async (name) => { await fileOps.renameFile(file.id, name); }); if (newName) loadFiles(); }); document.getElementById('move-file-option').addEventListener('click', () => { if (app.contextMenuTargetFile) { this.showMoveDialog(app.contextMenuTargetFile, 'file'); } ui.closeFileContextMenu(); }); document.getElementById('share-file-option').addEventListener('click', () => { const file = app.contextMenuTargetFile; if (file) { shareModal.open(file, 'file'); } ui.closeFileContextMenu(); }); document.getElementById('add-to-playlist-option').addEventListener('click', () => { const file = app.contextMenuTargetFile; if (file) { this.showPlaylistDialog(file); } ui.closeFileContextMenu(); }); document.getElementById('playlist-add-btn').addEventListener('click', () => { this.addSelectedFilesToPlaylist(); }); document.getElementById('delete-file-option').addEventListener('click', async () => { const file = app.contextMenuTargetFile; ui.closeFileContextMenu(); if (file) { await fileOps.deleteFile(file.id, file.name); } }); // Move dialog events const moveCancelBtn = document.getElementById('move-cancel-btn'); const moveConfirmBtn = document.getElementById('move-confirm-btn'); const copyConfirmBtn = document.getElementById('copy-confirm-btn'); const moveFileDialog = document.getElementById('move-file-dialog'); moveCancelBtn.addEventListener('click', this.closeMoveDialog); // Close move dialog on Escape key // Store handler reference to avoid duplicate listeners // Note: We don't use stopPropagation because all Escape handlers are on document level // Each handler checks its own state, so multiple dialogs can be closed with multiple Escape presses if (!_moveDialogEscapeHandler) { _moveDialogEscapeHandler = /** @type {EventListener} */ ( (/** @type {KeyboardEvent} */ e) => { if (e.key === 'Escape' && !moveFileDialog?.classList.contains('hidden')) { this.closeMoveDialog(); } } ); document.addEventListener('keydown', _moveDialogEscapeHandler); } // Copy button handler copyConfirmBtn.addEventListener('click', async () => { // Batch copy mode (from batchToolbar) if (app.moveDialogMode === 'batch' && batchToolbar) { const targetId = app.selectedTargetFolderId; const items = app.batchMoveItems || []; const fileIds = items.filter((i) => i.type === 'file').map((i) => i.id); const folderIds = items.filter((i) => i.type === 'folder').map((i) => i.id); const result = await fileOps.batchCopy(fileIds, folderIds, targetId); this.closeMoveDialog(); batchToolbar.clear(); loadFiles(); batchToolbar.showBatchResult('copy', result); return; } // Single item copy if (app.moveDialogMode === 'file' && app.contextMenuTargetFile) { const success = await fileOps.copyFile(app.contextMenuTargetFile.id, app.selectedTargetFolderId); if (success) { this.closeMoveDialog(); } } else if (app.moveDialogMode === 'folder' && app.contextMenuTargetFolder) { const success = await fileOps.copyFolder(app.contextMenuTargetFolder.id, app.selectedTargetFolderId); if (success) { this.closeMoveDialog(); } } }); moveConfirmBtn.addEventListener('click', async () => { // Batch move mode (from batchToolbar) if (app.moveDialogMode === 'batch' && batchToolbar) { const targetId = app.selectedTargetFolderId; const items = app.batchMoveItems || []; const fileIds = items.filter((i) => i.type === 'file').map((i) => i.id); const folderIds = items.filter((i) => i.type === 'folder' && i.id !== targetId).map((i) => i.id); const result = await fileOps.batchMove(fileIds, folderIds, targetId); this.closeMoveDialog(); batchToolbar.clear(); loadFiles(); batchToolbar.showBatchResult('move', result); return; } if (app.moveDialogMode === 'file' && app.contextMenuTargetFile) { const success = await fileOps.moveFile(app.contextMenuTargetFile.id, app.selectedTargetFolderId); if (success) { this.closeMoveDialog(); } } else if (app.moveDialogMode === 'folder' && app.contextMenuTargetFolder) { const success = await fileOps.moveFolder(app.contextMenuTargetFolder.id, app.selectedTargetFolderId); if (success) { this.closeMoveDialog(); } } }); }, /** * Show move dialog for a file or folder * @param {FolderItem | FileItem} item - File or folder object * @param {ItemTypeEnum} mode */ async showMoveDialog(item, mode) { // Set mode app.moveDialogMode = mode; // Reset selection app.selectedTargetFolderId = ''; // Ensure we have the home folder ID BEFORE calculating startFolderId if (!app.userHomeFolderId) { console.log('[Move Dialog] Home folder ID not set, resolving...'); await resolveHomeFolder(); } // Initialize dialog navigation state // Start at the parent of the item being moved (so user sees siblings and can navigate) let startFolderId = null; let startFolderName = null; if (mode === 'file' && /** @type {FileItem} */ (item).folder_id) { startFolderId = /** @type {FileItem} */ (item).folder_id; // We need the folder name for breadcrumb - try to get it from current view const folderEl = document.querySelector(`[data-folder-id="${startFolderId}"]`); if (folderEl) { startFolderName = folderEl.querySelector('.folder-name, .item-name')?.textContent || null; } } else if (mode === 'folder' && /** @type {FolderItem} */ (item).parent_id) { startFolderId = /** @type {FolderItem} */ (item).parent_id; } else { // If item is at root level, start at user's home folder startFolderId = app.userHomeFolderId || null; } console.log('[Move Dialog] showMoveDialog - item:', item, 'mode:', mode, 'startFolderId:', startFolderId, 'userHomeFolderId:', app.userHomeFolderId); // Store the item being moved and navigation state app.moveDialogItemId = item.id; app.moveDialogItemMode = mode; app.moveDialogCurrentFolderId = startFolderId; // Build initial breadcrumb if starting at a non-home folder // This allows proper navigation back to home const breadcrumb = []; if (startFolderId && startFolderId !== app.userHomeFolderId && startFolderName) { // We have the folder name, add it to breadcrumb breadcrumb.push({ id: startFolderId, name: startFolderName }); } app.moveDialogBreadcrumb = breadcrumb; // Update dialog title (preserve icon) const dialogHeader = document.getElementById('move-file-dialog').querySelector('.rename-dialog-header'); const titleText = mode === 'file' ? i18n.t('dialogs.move_file') : i18n.t('dialogs.move_folder'); dialogHeader.innerHTML = ` ${titleText}`; // Load folders for the starting location await this.loadMoveDialogFolders(startFolderId); // Show dialog document.getElementById('move-file-dialog')?.classList.remove('hidden'); }, /** * Close move dialog */ closeMoveDialog() { document.getElementById('move-file-dialog')?.classList.add('hidden'); app.contextMenuTargetFile = null; app.contextMenuTargetFolder = null; }, /** * Load folders for the move dialog with navigation support * Shows subfolders of the specified parent folder and allows navigation * @param {string} parentFolderId - Parent folder ID to load children from (null for root) */ async loadMoveDialogFolders(parentFolderId) { try { // Ensure we have the home folder ID before proceeding if (!app.userHomeFolderId) { await resolveHomeFolder(); } // Get the effective folder ID const effectiveParentId = parentFolderId || app.userHomeFolderId; // Must have a folder ID to proceed if (!effectiveParentId) { console.error('[Move Dialog] Cannot load folders - no folder ID available'); return; } /** @type {FolderItem[]} */ const folders = []; let cursor = /** @type {string|null} */ (null); do { const qs = cursor ? `resource_types=folder&cursor=${encodeURIComponent(cursor)}` : 'resource_types=folder'; const response = await fetch(`/api/folders/${effectiveParentId}/resources?${qs}`, { credentials: 'same-origin' }); if (!response.ok) { console.error('Failed to load folders:', response.status); return; } const data = await response.json(); folders.push(...(data.items || [])); cursor = data.next_cursor ?? null; } while (cursor); const folderSelectContainer = document.getElementById('folder-select-container'); const breadcrumbContainer = document.getElementById('move-dialog-breadcrumb'); // Clear container folderSelectContainer.innerHTML = ''; // Get current navigation state const itemId = app.moveDialogItemId; const mode = app.moveDialogItemMode; const breadcrumb = app.moveDialogBreadcrumb || []; // Always show breadcrumb to allow navigation back to home this._renderMoveDialogBreadcrumb(breadcrumbContainer, breadcrumb, effectiveParentId); breadcrumbContainer.style.display = 'flex'; // Option to select current folder as destination (only after navigating into subfolders) if (breadcrumb.length > 0 && effectiveParentId && effectiveParentId !== itemId) { const currentFolderOption = document.createElement('div'); currentFolderOption.className = 'folder-select-item folder-select-current'; currentFolderOption.innerHTML = ` ${i18n.t('dialogs.select_this_folder')} `; currentFolderOption.addEventListener('click', () => { document.querySelectorAll('.folder-select-item').forEach((item) => { item.classList.remove('selected'); }); currentFolderOption.classList.add('selected'); app.selectedTargetFolderId = effectiveParentId; }); folderSelectContainer.appendChild(currentFolderOption); } // Add "Go to parent" option if not at home folder const isAtHomeFolder = effectiveParentId === app.userHomeFolderId; if (!isAtHomeFolder || breadcrumb.length > 0) { const parentOption = document.createElement('div'); parentOption.className = 'folder-select-item folder-navigate-up'; parentOption.innerHTML = ` ${i18n.t('dialogs.go_to_parent')} `; parentOption.addEventListener('click', () => { // Navigate to parent folder const currentBreadcrumb = app.moveDialogBreadcrumb || []; if (currentBreadcrumb.length > 0) { // Remove current folder from breadcrumb currentBreadcrumb.pop(); const parentFolder = currentBreadcrumb.length > 0 ? currentBreadcrumb[currentBreadcrumb.length - 1] : null; app.moveDialogBreadcrumb = currentBreadcrumb; app.moveDialogCurrentFolderId = parentFolder ? parentFolder.id : null; this.loadMoveDialogFolders(parentFolder ? parentFolder.id : null); } else { // Go to root (home folder) app.moveDialogBreadcrumb = []; app.moveDialogCurrentFolderId = app.userHomeFolderId || null; this.loadMoveDialogFolders(app.userHomeFolderId || null); } }); folderSelectContainer.appendChild(parentOption); } // Add subfolders (clicking navigates INTO the folder) folders.forEach((folder) => { // Skip the item being moved (to prevent moving a folder into itself) if (mode === 'folder' && folder.id === itemId) { return; } const folderItem = document.createElement('div'); folderItem.className = 'folder-select-item folder-navigate'; folderItem.dataset.folderId = folder.id; folderItem.innerHTML = ` ${escapeHtml(folder.name)} `; // Click navigates INTO this folder folderItem.addEventListener('click', () => { // Add to breadcrumb const breadcrumb = app.moveDialogBreadcrumb || []; breadcrumb.push({ id: folder.id, name: folder.name }); app.moveDialogBreadcrumb = breadcrumb; app.moveDialogCurrentFolderId = folder.id; this.loadMoveDialogFolders(folder.id); }); folderSelectContainer.appendChild(folderItem); }); // Show "no subfolders" message if there are no folders to navigate if (folders.length === 0 && breadcrumb.length === 0) { // At home folder level with no subfolders - show option to move here const homeOption = document.createElement('div'); homeOption.className = 'folder-select-item folder-select-current'; homeOption.innerHTML = ` ${i18n.t('dialogs.move_to_home')} `; homeOption.addEventListener('click', () => { document.querySelectorAll('.folder-select-item').forEach((item) => { item.classList.remove('selected'); }); homeOption.classList.add('selected'); app.selectedTargetFolderId = ''; // Empty means root/home }); folderSelectContainer.appendChild(homeOption); } else if (folders.length === 0) { // Inside a subfolder with no children - show empty message const emptyMsg = document.createElement('div'); emptyMsg.className = 'folder-select-empty'; emptyMsg.innerHTML = ` ${i18n.t('dialogs.no_subfolders')}`; folderSelectContainer.appendChild(emptyMsg); } // Set default selection to current folder app.selectedTargetFolderId = parentFolderId || ''; // Translate new elements i18n.translateElement(folderSelectContainer); } catch (error) { console.error('Error loading folders:', error); } }, /** * Render breadcrumb navigation for move dialog * @param {HTMLElement | null} container * @param {Array<{id: string, name: string}>} breadcrumb * @param {string | null} _currentFolderId */ _renderMoveDialogBreadcrumb(container, breadcrumb, _currentFolderId) { if (!container) return; container.innerHTML = ''; const homeFolderId = app.userHomeFolderId; const homeFolderName = app.userHomeFolderName || 'Home'; // Home icon (click to go to home folder) const homeItem = document.createElement('span'); homeItem.className = 'move-breadcrumb-item'; homeItem.innerHTML = ''; homeItem.addEventListener('click', () => { app.moveDialogBreadcrumb = []; app.moveDialogCurrentFolderId = homeFolderId || null; this.loadMoveDialogFolders(homeFolderId || null); }); container.appendChild(homeItem); // Home folder name if (homeFolderName) { const separator = document.createElement('span'); separator.className = 'move-breadcrumb-separator'; separator.textContent = '>'; container.appendChild(separator); const homeNameItem = document.createElement('span'); homeNameItem.className = 'move-breadcrumb-item'; if (breadcrumb.length === 0) { homeNameItem.classList.add('current'); } homeNameItem.textContent = homeFolderName; if (breadcrumb.length > 0) { homeNameItem.addEventListener('click', () => { app.moveDialogBreadcrumb = []; app.moveDialogCurrentFolderId = homeFolderId || null; this.loadMoveDialogFolders(homeFolderId || null); }); } container.appendChild(homeNameItem); } // Breadcrumb path breadcrumb.forEach((/** @type {{id: string, name: string}} */ segment, /** @type {number} */ index) => { const separator = document.createElement('span'); separator.className = 'move-breadcrumb-separator'; separator.textContent = '>'; container.appendChild(separator); const item = document.createElement('span'); item.className = 'move-breadcrumb-item'; if (index === breadcrumb.length - 1) { item.classList.add('current'); } item.textContent = segment.name; // Click to navigate back to this level if (index < breadcrumb.length - 1) { item.addEventListener('click', () => { app.moveDialogBreadcrumb = breadcrumb.slice(0, index + 1); app.moveDialogCurrentFolderId = segment.id; this.loadMoveDialogFolders(segment.id); }); } container.appendChild(item); }); }, /** * Load all folders for the move dialog (batch operations) * Uses the same navigation pattern as loadMoveDialogFolders * @param {string} _itemId - ID of the item being moved (unused, kept for compatibility) * @param {string} _mode - 'batch' for batch operations */ async loadAllFolders(_itemId, _mode) { // For batch mode, use the same navigation as regular move dialog // Initialize navigation state starting at home folder app.moveDialogBreadcrumb = []; app.moveDialogCurrentFolderId = app.userHomeFolderId || null; await this.loadMoveDialogFolders(app.userHomeFolderId || null); }, /** * Show email notification dialog * @param {string} shareUrl - URL to share */ showEmailNotificationDialog(shareUrl) { // Update dialog content document.getElementById('notification-share-url').textContent = shareUrl; /** @type HTMLInputElement */ (document.getElementById('notification-email')).value = ''; /** @type HTMLInputElement */ (document.getElementById('notification-message')).value = ''; // Store the URL for later use app.notificationShareUrl = shareUrl; // Show dialog document.getElementById('notification-dialog')?.classList.remove('hidden'); }, /** * Send share notification email */ sendShareNotification() { const email = /** @type HTMLInputElement */ (document.getElementById('notification-email')).value.trim(); const message = /** @type HTMLInputElement */ (document.getElementById('notification-message')).value.trim(); const shareUrl = app.notificationShareUrl; if (!email || !shareUrl) { ui.showNotification(i18n.t('notif.errorTitle'), i18n.t('notif.invalidEmail')); return; } // Validate email format const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { ui.showNotification(i18n.t('notif.errorTitle'), i18n.t('notif.invalidEmail')); return; } try { fileSharing.sendShareNotification(shareUrl, email, message); document.getElementById('notification-dialog')?.classList.add('hidden'); } catch (error) { console.error('Error sending notification:', error); ui.showNotification(i18n.t('notif.errorTitle'), i18n.t('notif.notificationSendError')); } }, /** * Close notification dialog */ closeNotificationDialog() { document.getElementById('notification-dialog')?.classList.add('hidden'); app.notificationShareUrl = null; }, /** @type {String | null} */ _selectedPlaylistId: null, /** * * @param {FileItem} file * @returns */ async showPlaylistDialog(file) { const dialog = document.getElementById('playlist-dialog'); const container = document.getElementById('playlist-select-container'); const filesInfo = document.getElementById('playlist-dialog-files-info'); if (!dialog || !container) { console.error('Playlist dialog elements not found'); return; } // Store the file(s) to add app.playlistDialogFiles = [file]; // Update files info if (filesInfo) { filesInfo.innerHTML = `${i18n.t('music.selected_files')} ${file.name}`; } // Reset selection this._selectedPlaylistId = null; container.innerHTML = '