From bc0ea9ad150c439af98d6557c34f17ed95d41e20 Mon Sep 17 00:00:00 2001 From: mgutt <10757176+mgutt@users.noreply.github.com> Date: Tue, 30 Dec 2025 23:31:28 +0100 Subject: [PATCH 01/25] Fix #2500: File Manager UI/UX improvements --- emhttp/plugins/dynamix/Browse.page | 498 ++++++++++++++++-- emhttp/plugins/dynamix/include/Browse.php | 93 +++- emhttp/plugins/dynamix/include/Control.php | 124 +++-- emhttp/plugins/dynamix/include/FileTree.php | 84 ++- .../plugins/dynamix/include/OpenTerminal.php | 38 +- .../dynamix/include/PopularDestinations.php | 139 +++++ emhttp/plugins/dynamix/include/Templates.php | 66 ++- emhttp/plugins/dynamix/nchan/file_manager | 89 +++- .../plugins/dynamix/sheets/BrowseButton.css | 6 - .../plugins/dynamix/styles/default-base.css | 25 + .../dynamix/styles/default-dynamix.css | 4 - 11 files changed, 1011 insertions(+), 155 deletions(-) create mode 100644 emhttp/plugins/dynamix/include/PopularDestinations.php diff --git a/emhttp/plugins/dynamix/Browse.page b/emhttp/plugins/dynamix/Browse.page index cb1f94be8d..5cb4666a6b 100644 --- a/emhttp/plugins/dynamix/Browse.page +++ b/emhttp/plugins/dynamix/Browse.page @@ -57,6 +57,14 @@ function autoscale(value) { return ((Math.round(scale*data)/scale)+' '+unit[base]).replace('.','')+'/s'; } +function preventFileTreeClose() { + // Prevent fileTree dropdown from closing when clicking inside the dialog + // by stopping mousedown events from bubbling to the document handler + $('.ui-dfm').off('mousedown.dfmFileTree').on('mousedown.dfmFileTree', function(e) { + e.stopPropagation(); + }); +} + function folderContextMenu(id, button) { var opts = []; context.settings({button:button}); @@ -312,7 +320,7 @@ function doJobs(title) { }, "_(Delete)_": function(){ let row = []; - dfm.window.find('i[id^="queue_"]').each(function(){if ($(this).hasClass('fa-check-square-o')) row.push((($(this).prop('id').split('_')[1]-1)*9)+1);}); + dfm.window.find('i[id^="queue_"]').each(function(){if ($(this).hasClass('fa-check-square-o')) row.push($(this).prop('id').split('_')[1]);}); $.post('/webGui/include/Control.php',{mode:'undo',row:row.join(',')},function(queue){ $.post('/webGui/include/Control.php',{mode:'jobs'},function(jobs){ $('#dfm_jobs').html(jobs); @@ -343,6 +351,298 @@ filemonitor.on('message', function(state) { }); setTimeout(function(){filemonitor.start();},3000); +function setupTargetNavigation() { + var $target = dfm.window.find('#dfm_target'); + if (!$target.length) return; + + var navigationTimeout; + var inputElement = $target[0]; + var isNavigatingFromPopular = false; + var isProgrammaticNavigation = false; // Flag for programmatic tree navigation + var savedInputValue = ''; // Save input value during programmatic navigation + var lastNavigatedPath = ''; // Track last navigated path for backward navigation + + // Event handlers for capture phase to prevent fileTree from closing + var preventClose = function(e) { + console.log('preventClose called, event:', e.type); + + // Re-open tree if it was closed (must do this BEFORE stopImmediatePropagation) + if (e.type === 'click') { + var $tree = $target.closest('dd').find('.fileTree'); + console.log('Tree found:', $tree.length, 'visible:', $tree.is(':visible')); + if ($tree.length && !$tree.is(':visible')) { + // Only show if tree has content + var $content = $tree.find('.jqueryFileTree'); + console.log('Content found:', $content.length, 'children:', $content.children().length); + console.log('Condition check: length?', $content.length > 0, 'children?', $content.children().length > 0); + if ($content.length && $content.children().length > 0) { + console.log('Showing tree NOW'); + try { + $tree.show(); + console.log('Tree.show() completed, visible now:', $tree.is(':visible')); + } catch(err) { + console.error('Error showing tree:', err); + } + } else { + console.log('Tree has no content, not showing'); + } + } else { + console.log('Tree already visible or not found'); + } + } + + e.stopPropagation(); + e.stopImmediatePropagation(); + }; + + // Attach to input element in capture phase (runs before jquery.filetree.js handler) + if (inputElement) { + inputElement.addEventListener('mousedown', preventClose, true); + inputElement.addEventListener('focus', preventClose, true); + inputElement.addEventListener('click', preventClose, true); + } + + // Handle clicks on ANY tree item to remove popular destinations + var treeClickHandler = function(e) { + var $link = $(e.target).closest('.jqueryFileTree a'); + if ($link.length === 0) return; + + // Remove popular section when clicking on any tree item (except popular items themselves) + if (!$link.closest('.popular-destination').length) { + var $tree = $('.fileTree .jqueryFileTree'); + $tree.find('.popular-header, .popular-destination, .popular-separator').remove(); + } + }; + + // Attach tree click handler in capture phase + document.addEventListener('click', treeClickHandler, true); + + // Handle clicks on popular destinations - use capture phase to run before jQueryFileTree handler + var popularClickHandler = function(e) { + var $link = $(e.target).closest('.popular-destination a'); + if ($link.length === 0) return; + + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + + // Get path from data-path attribute + var path = $link.attr('data-path'); + if (!path) return; + + // Remove trailing slash for input field + var cleanPath = path.replace(/\/+$/, ''); + + // Remove popular section immediately (before any navigation) + var $tree = $('.fileTree .jqueryFileTree'); + $tree.find('.popular-header, .popular-destination, .popular-separator').remove(); + + // Set flag to prevent input handler from triggering + isNavigatingFromPopular = true; + + // Set input field value (this triggers 'input' event) + $target.val(cleanPath + '/'); + + // Trigger navigation to expand the path in normal tree + navigateFileTree(cleanPath + '/'); + lastNavigatedPath = cleanPath + '/'; + + // Reset flag after a short delay + setTimeout(function() { + isNavigatingFromPopular = false; + }, 100); + + return false; + }; + + // Attach in capture phase to run before jQueryFileTree handlers + document.addEventListener('click', popularClickHandler, true); + + // Hide/show popular destinations based on input field content + $target.on('input', function() { + // Ignore input events triggered by popular click or programmatic navigation + if (isNavigatingFromPopular || isProgrammaticNavigation) { + console.log('Input event ignored, isNavigatingFromPopular:', isNavigatingFromPopular, 'isProgrammaticNavigation:', isProgrammaticNavigation); + return; + } + + var inputValue = this.value.trim(); + console.log('Input event, value:', inputValue); + var $tree = $('.fileTree .jqueryFileTree'); + + if (inputValue) { + // Input is not empty - remove popular destinations completely + $tree.find('.popular-header, .popular-destination, .popular-separator').remove(); + } else { + // Input is empty - popular destinations will be shown on next tree reload + } + + clearTimeout(navigationTimeout); + + if (!inputValue) { + // Reset tree to initial state when input is cleared + console.log('Input cleared, resetting tree to initial state'); + resetFileTree($target); + lastNavigatedPath = ''; + return; + } + if (inputValue[inputValue.length - 1] !== '/') return; + + // Check if we're navigating backward (path got shorter) + if (lastNavigatedPath && inputValue.length < lastNavigatedPath.length && lastNavigatedPath.startsWith(inputValue)) { + // Close all folders between the new path and the old path + var pathToClose = lastNavigatedPath; + while (pathToClose.length > inputValue.length) { + closeFolderPath(pathToClose); + // Remove the last segment to go up one level + pathToClose = pathToClose.replace(/\/+$/, ''); // Remove trailing slashes + var lastSlash = pathToClose.lastIndexOf('/'); + if (lastSlash === -1) break; + pathToClose = pathToClose.substring(0, lastSlash + 1); + } + lastNavigatedPath = inputValue; + return; + } + + navigationTimeout = setTimeout(function() { + navigateFileTree(inputValue); + lastNavigatedPath = inputValue; + }, 200); + }); + + // Restore input value if changed during programmatic navigation + $target.on('change', function() { + var isProgrammaticNav = $target.data('isProgrammaticNavigation'); + var savedValue = $target.data('savedInputValue'); + console.log('Change event, isProgrammaticNav:', isProgrammaticNav, 'savedValue:', savedValue); + if (isProgrammaticNav && savedValue) { + console.log('Restoring input value from', this.value, 'to', savedValue); + this.value = savedValue; + } + }); + + // Cleanup on dialog close + dfm.window.on('dialogclose', function() { + document.removeEventListener('click', treeClickHandler, true); + document.removeEventListener('click', popularClickHandler, true); + if (inputElement) { + inputElement.removeEventListener('mousedown', preventClose, true); + inputElement.removeEventListener('focus', preventClose, true); + inputElement.removeEventListener('click', preventClose, true); + } + }); +} + +function closeFolderPath(path) { + var $tree = $('.jqueryFileTree').first(); + if ($tree.length === 0) return; + + // Remove trailing slash for searching + var cleanPath = path.replace(/\/+$/, ''); + + // Find the folder link by rel attribute + var $folderLink = $tree.find('a[rel="' + cleanPath + '/"]'); + if ($folderLink.length === 0) return; + + var $folderLi = $folderLink.closest('li'); + + // Close the folder: change class from expanded to collapsed + $folderLi.removeClass('expanded').addClass('collapsed'); + + // Remove all child elements (the nested