diff --git a/src/gui/src/UI/Dashboard/TabFiles.js b/src/gui/src/UI/Dashboard/TabFiles.js index dd6237ba2..163d419b1 100644 --- a/src/gui/src/UI/Dashboard/TabFiles.js +++ b/src/gui/src/UI/Dashboard/TabFiles.js @@ -48,6 +48,7 @@ const icons = { restore: ``, list: ``, grid: ``, + gridSmall: ``, sort: ``, select: ``, done: ``, @@ -195,7 +196,7 @@ const TabFiles = { $row.find('.item-modified').text(window.timeago.format(file.modified * 1000)); } if ( - _this.currentView === 'grid' && + _this.isGridView() && typeof file.thumbnail === 'string' && file.thumbnail.length > 0 ) { @@ -536,17 +537,7 @@ const TabFiles = { this.initNativeFileDrop(); // Apply initial view mode from persisted preferences - - const $filesContainer = this.$el_window.find('.files-tab .files'); - const $tabContent = this.$el_window.find('.files-tab'); - if ( this.currentView === 'grid' ) { - $filesContainer.addClass('files-grid-view'); - $tabContent.addClass('files-grid-mode'); - this.$el_window.find('.view-toggle-btn').html(icons.list); - } else { - $filesContainer.addClass('files-list-view'); - this.$el_window.find('.view-toggle-btn').html(icons.grid); - } + this.applyViewMode(); // Check for initial file path from URL routing if ( window.dashboard_initial_file_path ) { @@ -1270,9 +1261,9 @@ const TabFiles = { fileInput.click(); }; - // View toggle button - document.querySelector('.view-toggle-btn').onclick = () => { - this.toggleView(); + // View button (shows dropdown menu: list / compact grid / grid) + document.querySelector('.view-toggle-btn').onclick = (e) => { + this.showViewMenu(e); }; // Sort button (shows dropdown menu) @@ -1583,7 +1574,7 @@ const TabFiles = { $name.text(fullName); } }); - } else if ( this.currentView === 'grid' ) { + } else if ( this.isGridView() ) { // Apply middle-truncation in grid view $filesTab.find('.files.files-grid-view .row .item-name').each(function () { const $name = $(this); @@ -2516,13 +2507,13 @@ const TabFiles = { const $clone = $(el_item).clone(); // Wrap in container structure so CSS selectors match - const viewClass = _this.currentView === 'grid' ? 'files-grid-view' : 'files-list-view'; + const viewClass = _this.viewClass(); const $wrapper = $(`
`); $wrapper.find('.files').append($clone); // In grid view, set fixed width since the grid auto-fill // doesn't work without a proper parent width context - if ( _this.currentView === 'grid' ) { + if ( _this.isGridView() ) { $clone.css('width', $(el_item).outerWidth()); $wrapper.find('.files').css('display', 'block'); } @@ -2556,7 +2547,7 @@ const TabFiles = { ui.helper.addClass('selected'); // Clone other selected items with proper container structure - const viewClass = _this.currentView === 'grid' ? 'files-grid-view' : 'files-list-view'; + const viewClass = _this.viewClass(); $(el_item).siblings('.row.selected').each(function () { const $clone = $(this).clone(); const $wrapper = $(`
`); @@ -2987,32 +2978,71 @@ const TabFiles = { }, /** - * Toggles between list and grid view modes. + * Whether the current view mode is one of the grid variants. * - * Persists the preference to storage. + * Both 'grid' and 'grid-sm' share the files-grid-view container class + * (and all grid behavior); 'grid-sm' adds a size-modifier class on top. * + * @returns {boolean} + */ + isGridView () { + return this.currentView === 'grid' || this.currentView === 'grid-sm'; + }, + + /** + * Container classes for the current view mode, e.g. for drag-ghost wrappers. + * + * @returns {string} Space-separated class list for the .files container + */ + viewClass () { + if ( this.currentView === 'grid-sm' ) return 'files-grid-view files-grid-view-sm'; + if ( this.currentView === 'grid' ) return 'files-grid-view'; + return 'files-list-view'; + }, + + /** + * Displays the view mode selection menu (list / compact grid / grid). + * + * @param {MouseEvent} e - The click event from the view button * @returns {void} */ - toggleView () { - const $filesContainer = this.$el_window.find('.files-tab .files'); - const $toggleBtn = this.$el_window.find('.view-toggle-btn'); - const $tabContent = this.$el_window.find('.files-tab'); + showViewMenu (e) { + const _this = this; - if ( this.currentView === 'list' ) { - this.currentView = 'grid'; - $filesContainer.removeClass('files-list-view').addClass('files-grid-view'); - $tabContent.addClass('files-grid-mode'); - $toggleBtn.html(icons.list); - $toggleBtn.attr('title', 'Switch to list view'); - } else { - this.currentView = 'list'; - $filesContainer.removeClass('files-grid-view').addClass('files-list-view'); - $tabContent.removeClass('files-grid-mode'); - $toggleBtn.html(icons.grid); - $toggleBtn.attr('title', 'Switch to grid view'); - } + const viewOptions = [ + { mode: 'list', label: 'List' }, + { mode: 'grid-sm', label: 'Compact Grid' }, + { mode: 'grid', label: 'Grid' }, + ]; - puter.kv.set('view_mode', this.currentView); + const items = viewOptions.map(opt => { + return { + html: `${opt.label}`, + checked: _this.currentView === opt.mode, + onClick: () => { + _this.setView(opt.mode); + }, + }; + }); + + UIContextMenu({ + items: items, + position: { left: e.pageX, top: e.pageY }, + }); + }, + + /** + * Switches to the given view mode and persists the preference. + * + * @param {string} mode - 'list', 'grid', or 'grid-sm' + * @returns {void} + */ + setView (mode) { + if ( this.currentView === mode ) return; + this.currentView = mode; + this.applyViewMode(); + + puter.kv.set('view_mode', mode); // Refresh content to update icons for the new view mode if ( this.currentPath ) { @@ -3020,6 +3050,32 @@ const TabFiles = { } }, + /** + * Applies the current view mode's classes and button icon to the DOM. + * + * @returns {void} + */ + applyViewMode () { + const $filesContainer = this.$el_window.find('.files-tab .files'); + const $toggleBtn = this.$el_window.find('.view-toggle-btn'); + const $tabContent = this.$el_window.find('.files-tab'); + + $filesContainer.removeClass('files-list-view files-grid-view files-grid-view-sm'); + if ( this.isGridView() ) { + $filesContainer.addClass(this.viewClass()); + $tabContent.addClass('files-grid-mode'); + } else { + $filesContainer.addClass('files-list-view'); + $tabContent.removeClass('files-grid-mode'); + } + + // The button shows the active view's icon and opens the view menu + const btnIcon = this.currentView === 'grid' ? icons.grid + : this.currentView === 'grid-sm' ? icons.gridSmall + : icons.list; + $toggleBtn.html(btnIcon); + }, + /** * Toggles select mode for mobile multi-file selection. * diff --git a/src/gui/src/css/dashboard.css b/src/gui/src/css/dashboard.css index bb120a918..db70537b3 100644 --- a/src/gui/src/css/dashboard.css +++ b/src/gui/src/css/dashboard.css @@ -2263,6 +2263,41 @@ body.myapps-reordering .myapps-tile { text-align: center; } +/* --- Compact grid view --- + The container carries both files-grid-view and files-grid-view-sm, so all + grid rules above apply; this block only shrinks tile/icon/badge sizing. */ + +.dashboard-section-files .files-tab .files.files-grid-view.files-grid-view-sm { + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + gap: 8px; +} + +.dashboard-section-files .files-tab .files.files-grid-view.files-grid-view-sm .row { + padding: 8px; + gap: 6px; +} + +.dashboard-section-files .files-tab .files.files-grid-view.files-grid-view-sm .row .item-icon { + width: 80px; + height: 80px; + border-radius: 6px; +} + +.dashboard-section-files .files-tab .files.files-grid-view.files-grid-view-sm .row .item-icon svg { + width: 40px; + height: 40px; +} + +.dashboard-section-files .files-tab .files.files-grid-view.files-grid-view-sm .row .item-name { + font-size: 12px; +} + +.dashboard-section-files .files-tab .files.files-grid-view.files-grid-view-sm .row img.item-badge { + width: 16px !important; + height: 16px !important; + margin: 3px; +} + .dashboard-section-files .files-tab.files-grid-mode .header .columns { display: none; }