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);
};
/**