Reach Shared and Trash from the dashboard Files tab on phones
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s

Below 480px the Files tab hides its directories sidebar, which was the only
entry point to Shared (a query, not a directory) and Trash (filtered out of
the Home listing). Home now carries a row for each; CSS shows them only at
the widths where the sidebar is hidden, using the complementary breakpoint so
the two never both show or both disappear.

The rows borrow the item markup for layout but aren't `.item`, so sorted
insert, selection restore, share-link selection and socket updates ignore
them. The footer count and keyboard select-all exclude them explicitly, and
the placeholder-removal sweeps leave them in place. They navigate on tap and
offer the same menu as their sidebar entry via the ⋯ button or long-press;
the Trash icon tracks empty/full alongside the sidebar's.

Also guard the `window.user.directories` lookup in renderDirectory: it is
undefined for some sessions, and since `puter://shared` doesn't look like a
path it always reached that branch, threw outside the try, and left the
spinner up with navigation stuck — from the desktop sidebar as well.
This commit is contained in:
jelveh
2026-08-26 08:18:13 -07:00
parent defa37c106
commit e430ade7bd
3 changed files with 141 additions and 6 deletions
+99 -6
View File
@@ -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 = `
<div class="item-checkbox"><span class="checkbox-icon"></span></div>
<div class="item-icon"><img src="${html_encode(place.icon)}"/></div>
<div class="item-badges"></div>
<div class="item-name-wrapper">
<pre class="item-name">${html_encode(place.label)}</pre>
</div>
<div class="col-spacer"></div>
<div class="item-metadata">
<div class="item-size"></div>
<div class="col-spacer"></div>
<div class="item-modified"></div>
</div>
<div class="col-spacer"></div>
<div class="item-more">${icons.more}</div>
`;
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}']`);
+41
View File
@@ -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 */
+1
View File
@@ -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);
};
/**