diff --git a/src/gui/src/UI/Dashboard/TabFiles.js b/src/gui/src/UI/Dashboard/TabFiles.js index 9a520a009..c1c769507 100644 --- a/src/gui/src/UI/Dashboard/TabFiles.js +++ b/src/gui/src/UI/Dashboard/TabFiles.js @@ -238,7 +238,7 @@ const TabFiles = { // If the directory was empty, drop the "No files in this directory." // placeholder before inserting the first real row — otherwise it // stays and overlaps the new item. - _this.$el_window.find('.files-tab .files > div:not(.item)').remove(); + _this.$el_window.find('.files-tab .files > div:not(.item):not(.place-row)').remove(); await _this.renderItem(file); @@ -692,7 +692,7 @@ const TabFiles = { } const $container = _this.$el_window.find('.files-tab .files'); - const $allRows = $container.find('.row'); + const $allRows = $container.find('.row:not(.place-row)'); const $selectedRows = $container.find('.row.selected'); // F2 - Rename selected item @@ -2039,7 +2039,7 @@ const TabFiles = { // Drop the "No files in this directory." placeholder before inserting // the first row, otherwise it stays and overlaps the new item. - this.$el_window.find('.files-tab .files > div:not(.item)').remove(); + this.$el_window.find('.files-tab .files > div:not(.item):not(.place-row)').remove(); await this.renderItem(placeholder); const $row = this.$el_window.find(`.files-tab .files .item[data-uid='${placeholder.uid}']`); @@ -2280,7 +2280,10 @@ const TabFiles = { this.currentPath = target; } else { let path = null; - Object.entries(window.user.directories).forEach(o => { + // Not every session carries `directories` (the Shared view's + // puter:// target lands here too); a throw would leave the + // spinner up and renderingDirectory stuck. + Object.entries(window.user.directories || {}).forEach(o => { if ( o[1] === target ) { path = o[0]; } @@ -2415,6 +2418,10 @@ const TabFiles = { clearListing(); } + // Shared and Trash have no rows of their own, so below 480px — where + // the directories sidebar is hidden — Home carries a row for each. + this.renderPlaceRows(); + if ( directoryContents.length === 0 ) { this.renderEmptyPlaceholderIfNeeded(); this.updateFooterStats(); @@ -2444,6 +2451,91 @@ const TabFiles = { this.renderingDirectory = false; }, + /** + * Prepends "Shared" and "Trash" rows to the Home listing. Neither has a + * row of its own — Shared is a query, and Trash is filtered out of the + * listing — so the directories sidebar is their only entry point, and + * below 480px that sidebar is hidden. CSS shows these rows only at those + * widths; while the sidebar is visible they stay out of the way. + * + * The rows borrow the item markup for layout but are not `.item`, so + * nothing that walks items (sorted insert, selection restore, share-link + * select, placeholder removal) treats them as files. They only navigate + * and offer the same menu as their sidebar entry. + * + * @returns {void} + */ + renderPlaceRows () { + if ( this.currentPath !== window.home_path ) return; + const _this = this; + const $files = this.$el_window.find('.files-tab .files'); + + // The sidebar Trash icon already tracks empty/full (see + // update_trash_icons, which keeps this row's icon in step too). + const trashIcon = $('.directories [data-folder="Trash"] img').attr('src') || window.icons['trash.svg']; + const places = [ + { name: 'Shared', label: i18n('shared'), path: window.shared_path, icon: window.icons['folder-shared.svg'] }, + { name: 'Trash', label: i18n('trash'), path: window.trash_path, icon: trashIcon }, + ]; + + for ( const place of places ) { + const row = document.createElement('div'); + row.setAttribute('class', 'row folder place-row'); + row.setAttribute('data-place', place.name); + row.setAttribute('data-path', place.path); + row.setAttribute('data-name', place.label); + row.setAttribute('data-is_dir', '1'); + row.innerHTML = ` +
+
+
+
+
${html_encode(place.label)}
+
+
+
+
+
+
+
+
+
${icons.more}
+ `; + + const openMenu = (e) => { + e.preventDefault(); + e.stopPropagation(); + const items = _this.generateFolderContextMenu(place.path); + if ( window.isMobile.phone || window.isMobile.tablet || isTouchPrimaryDevice() ) { + const modal = new ContextMenuModal(); + modal.show(items, row.getBoundingClientRect(), { title: place.label }); + } else { + const releaseCtxState = _this.markRowContextMenuOpen(row); + const menu = UIContextMenu({ items: items, position: { left: e.pageX, top: e.pageY } }); + menu.onClose = releaseCtxState; + } + }; + + row.onclick = (e) => { + if ( e.target.closest('.item-more') ) { + openMenu(e); + return; + } + _this.pushNavHistory(place.path); + _this.renderDirectory(place.path); + }; + + $(row).on('contextmenu taphold', (e) => { + if ( e.type === 'taphold' && !window.isMobile.phone && !window.isMobile.tablet && !isTouchPrimaryDevice() ) { + return; + } + openMenu(e); + }); + + $files.append(row); + } + }, + /** * Renders a single file or folder item as a row in the file list. * @@ -3478,7 +3570,8 @@ const TabFiles = { const $selectionActions = this.$el_window.find('.files-selection-actions'); if ( ! $footer.length ) return; - const allRows = this.$el_window.find('.files-tab .row').toArray(); + // Place rows (Shared/Trash on Home) navigate; they aren't items. + const allRows = this.$el_window.find('.files-tab .row:not(.place-row)').toArray(); const selectedRows = this.$el_window.find('.files-tab .row.selected').toArray(); const totalCount = allRows.length; @@ -4152,7 +4245,7 @@ const TabFiles = { const result = await uploadPromise; if ( targetPath === _this.currentPath ) { // Remove empty-directory placeholder if present - _this.$el_window.find('.files-tab .files > div:not(.item)').remove(); + _this.$el_window.find('.files-tab .files > div:not(.item):not(.place-row)').remove(); // Add the new file incrementally await _this.renderItem(result); const $newRow = _this.$el_window.find(`.files-tab .files .item[data-uid='${result.uid}']`); diff --git a/src/gui/src/css/dashboard.css b/src/gui/src/css/dashboard.css index 9754d5354..3e2c8aae5 100644 --- a/src/gui/src/css/dashboard.css +++ b/src/gui/src/css/dashboard.css @@ -4733,6 +4733,15 @@ body.myapps-reordering .myapps-tile { } /* Mobile phone optimizations */ +/* The Shared/Trash rows on Home exist for phones, where the directories + sidebar below is hidden; while the sidebar is visible they'd be redundant. + Same breakpoint as the sidebar rule so the two never both show or both hide. */ +@media (min-width: 481px) { + .dashboard-section-files .files-tab .files .row.place-row { + display: none !important; + } +} + @media (max-width: 480px) { .dashboard-content.files { padding: 0; @@ -4957,6 +4966,38 @@ body.myapps-reordering .myapps-tile { left: 8px; z-index: 2; } + + /* Place rows (Shared/Trash at the top of Home — TabFiles.renderPlaceRows) + stand in for the hidden sidebar. A divider under the pair separates them + from Home's own folders; they only navigate, so select mode gives them + no checkbox and keeps their layout. */ + .dashboard-section-files .files-tab .files.files-list-view .row.place-row[data-place="Trash"] { + border-bottom: 1px solid var(--dashboard-border); + border-radius: 0; + margin-bottom: 4px; + } + + .dashboard-section-files .files-tab.select-mode-active .files .row.place-row .item-checkbox { + display: none; + } + + .dashboard-section-files .files-tab.select-mode-active .files.files-list-view .row.place-row { + grid-template-columns: 48px 1fr !important; + } + + .dashboard-section-files .files-tab.select-mode-active .files.files-list-view .row.place-row .item-icon { + grid-column: 1; + } + + .dashboard-section-files .files-tab.select-mode-active .files.files-list-view .row.place-row .item-name-wrapper, + .dashboard-section-files .files-tab.select-mode-active .files.files-list-view .row.place-row .item-metadata { + grid-column: 2; + } + + /* Home with the places on screen isn't empty. */ + .dashboard-section-files .files-tab .files .row.place-row ~ .files-empty-notice { + display: none !important; + } } /* Full HD */ diff --git a/src/gui/src/helpers.js b/src/gui/src/helpers.js index af15fcb18..31a65d70c 100644 --- a/src/gui/src/helpers.js +++ b/src/gui/src/helpers.js @@ -1693,6 +1693,7 @@ window.update_trash_icons = function (is_empty) { $(`.item[data-path="${html_encode(window.trash_path)}" i], .item[data-shortcut_to_path="${html_encode(window.trash_path)}" i]`).find('.item-icon > img').attr('src', icon); $(`.window[data-path="${html_encode(window.trash_path)}" i]`).find('.window-head-icon').attr('src', icon); $('.directories [data-folder="Trash"] img').attr('src', icon); + $('.files-tab .place-row[data-place="Trash"] .item-icon > img').attr('src', icon); }; /**