Restore Files tab in dashboard

Reinstates the Files tab (TabFiles) that was removed in 973d046d, bringing
back the dashboard file browser: directory navigation, list/grid views,
sorting, drag-and-drop, context menus, upload, and trash support.

- Restore src/gui/src/UI/Dashboard/TabFiles.js (identical to pre-removal)
- Re-wire UIDashboard.js: tab registration, initial file path, item socket
  handlers, and route-based file navigation
- Re-add file-tab CSS in dashboard.css and the selection-area rule in style.css
- Re-add dashboard-mode item creation hooks in helpers.js move_items
- Re-add #files/<path> deep-link parsing in initgui.js parseDashboardRoute,
  while preserving Apps as the default dashboard tab
This commit is contained in:
jelveh
2026-07-16 17:22:41 -07:00
parent 0e1be72f92
commit 67f8f0f5f5
6 changed files with 5389 additions and 6 deletions
File diff suppressed because it is too large Load Diff
+107
View File
@@ -44,6 +44,7 @@ import UIWindowFeedback from '../UIWindowFeedback.js';
// Import tab modules
import TabHome from './TabHome.js';
import TabFiles from './TabFiles.js';
import TabApps from './TabApps.js';
import TabUsage from './TabUsage.js';
import TabAccount from './TabAccount.js';
@@ -53,6 +54,7 @@ import TabSecurity from './TabSecurity.js';
const builtinTabs = [
TabHome,
TabApps,
TabFiles,
'-',
TabUsage,
TabAccount,
@@ -165,6 +167,11 @@ async function UIDashboard (options) {
$el_window.find('.dashboard-sidebar').addClass('collapsed');
}
// Set initial file path BEFORE tabs are initialized (so TabFiles.init() can use it)
if ( window.dashboard_initial_route?.tab === 'files' && window.dashboard_initial_route?.path ) {
window.dashboard_initial_file_path = window.dashboard_initial_route.path;
}
// Initialize all tabs
for ( const tab of tabs ) {
if ( tab.init ) {
@@ -236,6 +243,97 @@ async function UIDashboard (options) {
}
});
// Trash status updates
window.socket.on('trash.is_empty', async (msg) => {
// Update sidebar Trash icon
const trashIcon = msg.is_empty ? window.icons['trash.svg'] : window.icons['trash-full.svg'];
$('.directories [data-folder=\'Trash\'] img').attr('src', trashIcon);
// If currently viewing trash and it's empty, clear the file list
const dashboard = window.dashboard_object;
if ( msg.is_empty && dashboard && dashboard.currentPath === window.trash_path ) {
$('.files-tab .files').empty();
}
});
// =========================================================================
// Item event handlers
// Incremental DOM updates using UIDashboardFileItem for item creation and
// direct jQuery manipulation for removals/updates. Mirrors UIDesktop's
// approach but adapted for Dashboard's list-view structure.
// =========================================================================
window.socket.on('item.moved', async (resp) => {
if ( resp.original_client_socket_id === window.socket.id ) return;
// Fade out old item from view
$(`.item[data-uid='${resp.uid}']`).fadeOut(150, function () {
$(this).remove();
});
// Create new item at destination if user is viewing that directory
if ( window.UIDashboardFileItem ) {
window.UIDashboardFileItem(resp);
}
});
window.socket.on('item.removed', async (item) => {
if ( item.original_client_socket_id === window.socket.id ) return;
if ( item.descendants_only ) return;
$(`.item[data-path='${html_encode(item.path)}']`).fadeOut(150, function () {
$(this).remove();
});
});
window.socket.on('item.renamed', async (item) => {
if ( item.original_client_socket_id === window.socket.id ) return;
const $el = $(`.item[data-uid='${item.uid}']`);
if ( $el.length === 0 ) return;
// Update data attributes
$el.attr('data-name', html_encode(item.name));
$el.attr('data-path', html_encode(item.path));
// Update displayed name
$el.find('.item-name').text(item.name);
$el.find('.item-name-editor').val(item.name);
});
window.socket.on('item.updated', async (item) => {
const $el = $(`.item[data-uid='${item.uid}']`);
if ( $el.length === 0 ) return;
// Update data attributes
$el.attr('data-name', html_encode(item.name));
$el.attr('data-path', html_encode(item.path));
$el.attr('data-size', item.size);
$el.attr('data-modified', item.modified);
$el.attr('data-type', html_encode(item.type));
// Update displayed name
$el.find('.item-name').text(item.name);
$el.find('.item-name-editor').val(item.name);
if (
window.dashboard_object?.currentView === 'grid'
&& typeof item.thumbnail === 'string'
&& item.thumbnail.length > 0
) {
$el.find('.item-icon img').attr('src', item.thumbnail);
}
});
window.socket.on('item.added', async (item) => {
if ( !item || Object.keys(item).length === 0 ) return;
if ( item.original_client_socket_id === window.socket.id ) return;
if ( window.UIDashboardFileItem ) {
window.UIDashboardFileItem(item);
}
});
// Apply initial route from URL - activate the correct tab
if ( window.dashboard_initial_route ) {
const route = window.dashboard_initial_route;
@@ -269,6 +367,7 @@ async function UIDashboard (options) {
const handleRouteChange = () => {
const route = window.parseDashboardRoute();
const tab = route.tab;
const filePath = route.path;
// Switch to correct tab
const $targetTab = $el_window.find(`.dashboard-sidebar-item[data-section="${tab}"]`);
@@ -290,6 +389,14 @@ async function UIDashboard (options) {
// Scroll content area to top
$el_window.find('.dashboard-content').scrollTop(0);
// If files tab with path, navigate without adding to history
if ( tab === 'files' && filePath ) {
const filesTab = tabs.find(t => t.id === 'files');
if ( filesTab?.renderDirectory ) {
filesTab.renderDirectory(filePath, { skipUrlUpdate: true, skipNavHistory: true });
}
}
};
// Listen for both hashchange and popstate to handle all navigation scenarios
File diff suppressed because it is too large Load Diff
+13
View File
@@ -1919,6 +1919,19 @@ span.header-sort-icon img {
border: none;
}
/* TabFiles rubber band selection area */
.tabfiles-selection-area {
background-color: rgba(59, 130, 246, 0.15);
border: 1px solid var(--select-color);
position: absolute;
pointer-events: none;
z-index: 1000;
}
.dashboard-section-files .files {
position: relative;
}
.container {
user-select: none;
}
+8
View File
@@ -1890,6 +1890,10 @@ window.move_items = async function (el_items, dest_path, is_undo = false) {
suggested_apps: fsentry.suggested_apps,
};
UIItem(options);
// In dashboard mode, also create item via dashboard's renderer
if ( window.is_dashboard_mode && window.UIDashboardFileItem ) {
window.UIDashboardFileItem(fsentry);
}
moved_items.push({ 'options': options, 'original_path': $(el_item).attr('data-path') });
// this operation may have created some missing directories,
@@ -1915,6 +1919,10 @@ window.move_items = async function (el_items, dest_path, is_undo = false) {
suggested_apps: dir.suggested_apps,
});
}
// In dashboard mode, also create parent dirs via dashboard's renderer
if ( window.is_dashboard_mode && window.UIDashboardFileItem ) {
window.UIDashboardFileItem(dir);
}
window.sort_items(item_container);
});
+12 -6
View File
@@ -532,15 +532,21 @@ if (jQuery) {
/**
* Parses the dashboard URL hash into a route object.
* Apps is the default tab (root URL / no hash); Home is reached via #home.
* Hash format: #home or #usage or #account etc.
* @returns {{ tab: string }} Route object with tab name
* Hash format: #files/username/Documents or #home or #usage or #account etc.
* @returns {{ tab: string, path: string|null }} Route object with tab name and optional file path
*/
function parseDashboardRoute() {
const hash = decodeURIComponent(window.location.hash.slice(1));
if (!hash) return { tab: 'apps' };
const hash = decodeURIComponent(window.location.hash.slice(1)); // Remove '#' and decode URL encoding
if (!hash) return { tab: 'apps', path: null };
const tab = hash.split('/').filter(Boolean)[0];
return { tab: tab || 'apps' };
const parts = hash.split('/').filter(Boolean); // ['files', 'username', 'Documents']
const tab = parts[0]; // 'files', 'usage', 'account', 'security'
if (tab === 'files' && parts.length > 1) {
const filePath = `/${parts.slice(1).join('/')}`; // /username/Documents
return { tab: 'files', path: filePath };
}
return { tab: tab || 'apps', path: null };
}
// Make parseDashboardRoute available globally for hashchange handler