mirror of
https://github.com/HeyPuter/puter.git
synced 2026-08-25 23:46:51 +00:00
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:
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user