diff --git a/src/gui/src/UI/Dashboard/TabFiles.js b/src/gui/src/UI/Dashboard/TabFiles.js
deleted file mode 100644
index 3474280e0..000000000
--- a/src/gui/src/UI/Dashboard/TabFiles.js
+++ /dev/null
@@ -1,3913 +0,0 @@
-/**
- * Copyright (C) 2024-present Puter Technologies Inc.
- *
- * This file is part of Puter.
- *
- * Puter is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published
- * by the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- */
-
-/* eslint-disable no-invalid-this */
-/* eslint-disable @stylistic/quotes */
-import path from '../../lib/path.js';
-import open_item from '../../helpers/open_item.js';
-import UIContextMenu from '../UIContextMenu.js';
-import UIWindowProgress from '../UIWindowProgress.js';
-import UIAlert from '../UIAlert.js';
-import generate_file_context_menu from '../../helpers/generate_file_context_menu.js';
-import truncate_filename from '../../helpers/truncate_filename.js';
-import update_title_based_on_uploads from '../../helpers/update_title_based_on_uploads.js';
-import item_icon from '../../helpers/item_icon.js';
-import new_context_menu_item from '../../helpers/new_context_menu_item.js';
-import ContextMenuModal from './ContextMenu/ContextMenu.js';
-
-const icons = {
- document: ``,
- files: ``,
- folder: ``,
- more: ``,
- newFolder: ``,
- upload: ``,
- trash: ``,
- download: ``,
- cut: ``,
- copy: ``,
- restore: ``,
- list: ``,
- grid: ``,
- sort: ``,
- select: ``,
- done: ``,
- worker: ``,
-};
-
-const { html_encode, SelectionArea } = window;
-
-/**
- * TabFiles - File browser tab component for the Puter Dashboard.
- *
- * Provides a full-featured file management interface including:
- * - Directory navigation with breadcrumb path
- * - List and grid view modes
- * - File sorting by name, size, or modification date
- * - Drag-and-drop file operations (move, copy, shortcut)
- * - Context menus for file/folder operations
- * - File upload with progress tracking
- * - Trash folder support with restore/permanent delete
- *
- * @module TabFiles
- */
-const TabFiles = {
- id: 'files',
- label: 'Files',
- icon: icons.files,
-
- /**
- * Generates the HTML template for the files tab.
- *
- * @returns {string} HTML string containing the file browser structure
- */
- html () {
- let h = `
-
-
-
-
-
Home
-
Desktop
-
Documents
-
Pictures
-
Public
-
Videos
-
Trash
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
${i18n('name')}
-
-
${i18n('size')}
-
-
${i18n('modified')}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `;
- return h;
- },
-
- /**
- * Initializes the files tab with event listeners and state.
- *
- * Sets up folder click handlers, drag-and-drop zones, context menus,
- * and restores persisted preferences (view mode, sort settings, column widths).
- *
- * @param {jQuery} $el_window - The jQuery-wrapped window/container element
- * @returns {Promise}
- */
- async init ($el_window) {
- this.showSpinner();
- const _this = this;
- window.dashboard_object = _this;
-
- // Dashboard-compatible item creator for use by helpers.js and socket handlers.
- // Wraps renderItem() with a directory check so items are only added
- // when the user is viewing the relevant directory.
- window.UIDashboardFileItem = async function (file) {
- if ( ! _this.currentPath ) return;
- if ( _this.renderingDirectory ) return;
- if ( _this._creatingItem ) return;
-
- const parentDir = path.dirname(file.path);
- if ( _this.currentPath !== parentDir ) return;
-
- // If item already exists in view, update in-place.
- const $existingRow = $(`.files-tab .files .item[data-uid='${file.uid}']`);
- if ( $existingRow.length > 0 ) {
- const displayName = file.name || '';
- $existingRow.attr('data-name', displayName);
- $existingRow.attr('data-path', file.path || '');
- $existingRow.attr('data-size', file.size || 0);
- $existingRow.attr('data-modified', file.modified || 0);
- $existingRow.attr('data-type', file.type || '');
- $existingRow.find('.item-name').text(displayName);
- $existingRow.find('.item-name-editor').val(displayName);
- if (
- _this.currentView === 'grid' &&
- typeof file.thumbnail === 'string' &&
- file.thumbnail.length > 0
- ) {
- $existingRow.find('.item-icon img').attr('src', file.thumbnail);
- }
- return;
- }
-
- await _this.renderItem(file);
-
- // Get the newly appended row (it's always last after renderItem)
- const $newRow = _this.$el_window.find(`.files-tab .files .item[data-uid='${file.uid}']`);
- if ( $newRow.length === 0 ) return;
-
- // Insert at correct sorted position
- _this.insertAtSortedPosition($newRow, file);
-
- // Apply column widths to match existing rows
- _this.applyColumnWidths();
-
- // Highlight animation to indicate newly added item
- $newRow.addClass('item-newly-added');
- };
-
- this.renderingDirectory = false;
- this._creatingItem = false;
- this.activeMenuFileUid = null;
- this.currentPath = null;
- this.currentPath = null;
- this.folderDwellTimer = null;
- this.folderDwellTarget = null;
- this.springLoadedActive = false;
- this.springLoadedOriginalPath = null;
- this.previewOpen = false;
- this.previewCurrentUid = null;
- this.typeSearchTerm = '';
- this.typeSearchTimeout = null;
- this.selectModeActive = false;
- this.currentView = await puter.kv.get('view_mode') || 'list';
-
- // Sorting state
- this.sortColumn = await puter.kv.get('sort_column') || 'name';
- this.sortDirection = await puter.kv.get('sort_direction') || 'asc';
-
- // Column widths state (for resizing)
- const savedWidths = await puter.kv.get('column_widths');
- this.columnWidths = savedWidths ? JSON.parse(savedWidths) : {
- name: null, // auto/flex
- size: 100,
- modified: 120,
- };
-
- // Add touch-device class for touch devices to show .item-more button
- // Use multiple detection methods since user-agent sniffing can miss devices
- if ( window.isMobile.phone || window.isMobile.tablet || navigator.maxTouchPoints > 0 ) {
- $el_window.find('.files-tab').addClass('touch-device');
- }
-
- // Create click handler for each folder item
- $el_window.find('[data-folder]').each(function () {
- const folderElement = this;
-
- folderElement.onclick = async () => {
- const folderPath = folderElement.getAttribute('data-path');
- _this.pushNavHistory(folderPath);
- _this.renderDirectory(folderPath);
- };
-
- // Context menu for sidebar folders
- $(folderElement).on('contextmenu taphold', async (e) => {
- if ( e.type === 'taphold' && !window.isMobile.phone && !window.isMobile.tablet ) {
- return;
- }
- e.preventDefault();
- e.stopPropagation();
- $(folderElement).addClass('context-menu-active');
- const folderPath = folderElement.getAttribute('data-path');
- const items = _this.generateFolderContextMenu(folderPath);
-
- if ( window.isMobile.phone || window.isMobile.tablet ) {
- const modal = new ContextMenuModal({
- onClose: () => $(folderElement).removeClass('context-menu-active'),
- });
- modal.show(items, folderElement.getBoundingClientRect());
- } else {
- const menu = UIContextMenu({ items: items, position: { left: e.pageX, top: e.pageY } });
- menu.onClose = () => {
- $(folderElement).removeClass('context-menu-active');
- };
- }
- });
-
- // Make sidebar folders droppable
- $(folderElement).droppable({
- accept: '.row',
- tolerance: 'pointer',
-
- drop: async function (event, ui) {
- // Clear dwell timer to prevent folder from opening after drop
- clearTimeout(_this.folderDwellTimer);
- _this.folderDwellTimer = null;
- _this.folderDwellTarget = null;
-
- // Block if ctrl and trashed
- const draggedPath = $(ui.draggable).attr('data-path');
- if ( event.ctrlKey && draggedPath?.startsWith(`${window.trash_path}/`) ) {
- return;
- }
-
- ui.helper.data('dropped', true);
-
- // Get target folder path
- const folderName = folderElement.getAttribute('data-folder');
- const directories = Object.keys(window.user.directories);
- const targetPath = directories.find(f => f.endsWith(folderName));
-
- if ( ! targetPath ) return;
-
- // Collect all items to move
- const itemsToMove = [ui.draggable[0]];
-
- // Add other selected items
- $('.item-selected-clone').each(function () {
- const sourceId = $(this).attr('data-id');
- const sourceItem = document.querySelector(`.row[data-id="${sourceId}"]`);
- if ( sourceItem ) itemsToMove.push(sourceItem);
- });
-
- // Perform operation based on modifier keys
- if ( event.ctrlKey ) {
- // Copy
- await window.copy_items(itemsToMove, targetPath);
- }
- else if ( event.altKey && window.feature_flags?.create_shortcut ) {
- // Create shortcuts
- for ( const item of itemsToMove ) {
- const itemPath = $(item).attr('data-path');
- const itemName = itemPath.split('/').pop();
- const isDir = $(item).attr('data-is_dir') === '1';
- const shortcutTo = $(item).attr('data-shortcut_to') || $(item).attr('data-uid');
- const shortcutToPath = $(item).attr('data-shortcut_to_path') || itemPath;
-
- await window.create_shortcut(itemName, isDir, targetPath, null, shortcutTo, shortcutToPath);
- }
- }
- else {
- // Move
- await window.move_items(itemsToMove, targetPath);
- }
- },
-
- over: function (_event, ui) {
- if ( $(ui.draggable).hasClass('row') ) {
- $(folderElement).addClass('active');
-
- const folderPath = folderElement.getAttribute('data-path');
-
- // Don't auto-open the current directory or trash
- if ( folderPath === _this.currentPath ||
- folderPath === window.trash_path ) {
- return;
- }
-
- // Clear any existing dwell timer
- clearTimeout(_this.folderDwellTimer);
-
- // Add visual feedback animation
- $(folderElement).addClass('dwell-opening');
- _this.folderDwellTarget = folderElement;
-
- // Start dwell timer — navigate into folder after 700ms
- _this.folderDwellTimer = setTimeout(async () => {
- _this.folderDwellTimer = null;
- _this.folderDwellTarget = null;
- if ( ! _this.springLoadedActive ) {
- _this.springLoadedOriginalPath = _this.currentPath;
- }
- _this.springLoadedActive = true;
- $('.drag-cancel-zone').show();
- $(folderElement).removeClass('dwell-opening active');
-
- _this.pushNavHistory(folderPath);
- await _this.renderDirectory(folderPath);
-
- // Refresh jQuery UI droppable detection for the active drag
- if ( $.ui.ddmanager && $.ui.ddmanager.current ) {
- $.ui.ddmanager.current.helper.addClass('ui-draggable-dragging');
- $.ui.ddmanager.prepareOffsets($.ui.ddmanager.current);
- }
- }, 700);
- }
- },
-
- out: function (_event, ui) {
- if ( $(ui.draggable).hasClass('row') ) {
- // Clear dwell timer
- if ( _this.folderDwellTarget === folderElement ) {
- clearTimeout(_this.folderDwellTimer);
- _this.folderDwellTimer = null;
- _this.folderDwellTarget = null;
- }
- $(folderElement).removeClass('dwell-opening');
-
- // Only remove active if it's not the currently selected folder
- const folderName = folderElement.getAttribute('data-folder');
- const directories = Object.keys(window.user.directories);
- const folderUid = window.user.directories[directories.find(f => f.endsWith(folderName))];
-
- if ( folderUid !== _this.currentPath ) {
- $(folderElement).removeClass('active');
- }
- }
- },
- });
-
- // Add native file drop support to sidebar folders
- $(folderElement).dragster({
- enter: function (_dragsterEvent, event) {
- const e = event.originalEvent;
- if ( ! e.dataTransfer?.types?.includes('Files') ) {
- return;
- }
-
- const folderPath = folderElement.getAttribute('data-path');
-
- // Don't allow drop on trash
- if ( folderPath === window.trash_path ) {
- return;
- }
-
- $(folderElement).addClass('native-drop-target');
- },
-
- leave: function (_dragsterEvent, _event) {
- $(folderElement).removeClass('native-drop-target');
- },
-
- drop: async function (_dragsterEvent, event) {
- const e = event.originalEvent;
- $(folderElement).removeClass('native-drop-target');
-
- if ( ! e.dataTransfer?.types?.includes('Files') ) {
- return;
- }
-
- const folderPath = folderElement.getAttribute('data-path');
-
- // Block uploads to trash
- if ( folderPath === window.trash_path ) {
- return;
- }
-
- if ( e.dataTransfer?.items?.length > 0 ) {
- _this.uploadFiles(e.dataTransfer.items, folderPath);
- }
-
- e.stopPropagation();
- e.preventDefault();
- return false;
- },
- });
- });
-
- // Clear selection when clicking empty area (but not after rubber band selection)
- $el_window.find('.dashboard-tab-content').on('click', (e) => {
- // Skip if this click is the end of a rubber band selection
- if ( _this.rubberBandSelectionJustEnded ) {
- _this.rubberBandSelectionJustEnded = false;
- return;
- }
- if ( e.target === this || e.target.classList.contains('files') ) {
- document.querySelectorAll('.files-tab .row.selected').forEach(r => {
- r.classList.remove('selected');
- });
- _this.updateFooterStats();
- }
- });
-
- // Right-click on background shows folder context menu
- $el_window.find('.files').on('contextmenu taphold', async (e) => {
- // Dismiss taphold on non-touch devices
- if ( e.type === 'taphold' && !window.isMobile.phone && !window.isMobile.tablet ) {
- return;
- }
- // Only trigger if clicking directly on .files container (not on a row)
- if ( e.target.classList.contains('files') ||
- e.target.classList.contains('files-list-view') ||
- e.target.classList.contains('files-grid-view') ) {
- e.preventDefault();
- e.stopPropagation();
- // Clear selection when right-clicking background
- document.querySelectorAll('.files-tab .row.selected').forEach(r => {
- r.classList.remove('selected');
- });
- _this.updateFooterStats();
- const items = await _this.generateFolderContextMenu();
- if ( window.isMobile.phone || window.isMobile.tablet ) {
- const modal = new ContextMenuModal();
- modal.show(items, e.target.getBoundingClientRect());
- } else {
- UIContextMenu({ items: items, position: { left: e.pageX, top: e.pageY } });
- }
- }
- });
-
- // Store reference to $el_window for later use (must be before createHeaderEventListeners)
- this.$el_window = $el_window;
-
- this.createHeaderEventListeners($el_window);
- this.createSelectionActionListeners($el_window);
- this.initRubberBandSelection();
- this.initNativeFileDrop();
-
- // Apply initial view mode from persisted preferences
-
- const $filesContainer = this.$el_window.find('.files-tab .files');
- const $tabContent = this.$el_window.find('.files-tab');
- if ( this.currentView === 'grid' ) {
- $filesContainer.addClass('files-grid-view');
- $tabContent.addClass('files-grid-mode');
- this.$el_window.find('.view-toggle-btn').html(icons.list);
- } else {
- $filesContainer.addClass('files-list-view');
- this.$el_window.find('.view-toggle-btn').html(icons.grid);
- }
-
- // Check for initial file path from URL routing
- if ( window.dashboard_initial_file_path ) {
- const initialPath = window.dashboard_initial_file_path;
- delete window.dashboard_initial_file_path; // Clear so it only runs once
- this.pushNavHistory(initialPath);
- this.renderDirectory(initialPath, { skipUrlUpdate: true });
- } else {
- // Auto-select Documents folder on initialization
- const documentsFolder = $el_window.find('[data-folder="Documents"]');
- if ( documentsFolder.length ) {
- documentsFolder.trigger('click');
- }
- }
-
- // Setup keyboard shortcuts
- this.setupKeyboardShortcuts();
-
- // Refresh current directory when the user returns to this browser tab
- document.addEventListener('visibilitychange', () => {
- if ( document.visibilityState === 'visible' && this.currentPath ) {
- this.renderDirectory(this.currentPath, { skipNavHistory: true, skipUrlUpdate: true });
- }
- });
- },
-
- /**
- * Called when the Files tab becomes active.
- * Updates the URL hash to reflect the current file path.
- *
- * @param {jQuery} _$el_window - The jQuery-wrapped window/container element (unused)
- * @returns {void}
- */
- onActivate (_$el_window) {
- // Update URL to show current path when Files tab becomes active
- if ( this.currentPath && window.is_dashboard_mode ) {
- this.updateDashboardUrl(this.currentPath);
- }
- },
-
- /**
- * Checks if the Dashboard Files tab is currently active and visible.
- *
- * @returns {boolean} True if Dashboard is visible and Files tab is active
- */
- isDashboardFilesActive () {
- if ( !this.$el_window || !this.$el_window.is(':visible') ) return false;
- const filesSection = this.$el_window.find('.dashboard-section-files');
- return filesSection.hasClass('active');
- },
-
- /**
- * Sets up Dashboard-specific keyboard shortcuts.
- *
- * Handles arrow navigation, selection, copy/cut/paste, delete, rename, etc.
- */
- setupKeyboardShortcuts () {
- const _this = this;
-
- $(document).on('keydown.tabfiles', async function (e) {
- // Only handle if Dashboard Files tab is active
- if ( ! _this.isDashboardFilesActive() ) return;
-
- const focused_el = document.activeElement;
-
- // Skip if user is typing in an input/textarea (except for Escape)
- if ( $(focused_el).is('input, textarea') && e.which !== 27 ) return;
-
- // When a context menu is open, yield control to keyboard.js
- if ( $('.context-menu').length > 0 ) {
- if ( (e.which >= 37 && e.which <= 40) || e.which === 13 || e.which === 27 ) {
- return;
- }
- if ( !e.ctrlKey && !e.metaKey && e.key.length === 1 ) {
- return;
- }
- }
-
- const $container = _this.$el_window.find('.files-tab .files');
- const $allRows = $container.find('.row');
- const $selectedRows = $container.find('.row.selected');
-
- // F2 - Rename selected item
- if ( e.which === 113 ) {
- const $selectedRow = $selectedRows.first();
- if ( $selectedRow.length > 0 ) {
- e.preventDefault();
- e.stopPropagation();
- const $nameEditor = $selectedRow.find('.item-name-editor');
- const $itemName = $selectedRow.find('.item-name');
- if ( $nameEditor.length > 0 ) {
- $itemName.hide();
- $nameEditor.show().addClass('item-name-editor-active').focus().select();
- }
- }
- return false;
- }
-
- // Enter - Open selected items
- if ( e.which === 13 && !$(focused_el).hasClass('item-name-editor') ) {
- if ( $selectedRows.length > 0 ) {
- e.preventDefault();
- e.stopPropagation();
- $selectedRows.each(function () {
- const isDir = $(this).attr('data-is_dir') === '1';
- const itemPath = $(this).attr('data-path');
- if ( isDir ) {
- _this.pushNavHistory(itemPath);
- _this.renderDirectory(itemPath);
- } else {
- open_item({ item: this });
- }
- });
- }
- return false;
- }
-
- // Escape - Cancel drag, clear selection, or cancel rename
- if ( e.which === 27 ) {
- // Cancel active drag operation
- if ( window.an_item_is_being_dragged ) {
- e.preventDefault();
- e.stopPropagation();
-
- if ( _this.springLoadedActive ) {
- _this.navigateBackFromSpringLoad();
- }
- _this.springLoadedActive = false;
- _this.springLoadedOriginalPath = null;
-
- // Force jQuery UI to end the drag
- $(document).trigger('mouseup');
-
- // Cleanup
- $('.drag-cancel-zone').remove();
- $('.item-selected-clone').remove();
- $('.draggable-count-badge').remove();
- window.an_item_is_being_dragged = false;
- $('.window-app-iframe').css('pointer-events', 'auto');
- return false;
- }
-
- if ( $(focused_el).hasClass('item-name-editor') ) {
- // Cancel rename - handled by item's own keyup handler
- return;
- }
- $selectedRows.removeClass('selected');
- _this.updateFooterStats();
- return false;
- }
-
- // Delete - Move to trash or permanently delete
- if ( e.keyCode === 46 || (e.keyCode === 8 && (e.ctrlKey || e.metaKey)) ) {
- if ( $selectedRows.length > 0 ) {
- e.preventDefault();
- e.stopPropagation();
-
- // Check if any items are in trash (for permanent delete)
- const trashedItems = $selectedRows.filter(function () {
- return $(this).attr('data-path')?.startsWith(`${window.trash_path}/`);
- });
-
- if ( trashedItems.length > 0 ) {
- // Permanent delete with confirmation
- const alert_resp = await UIAlert({
- message: i18n('confirm_delete_multiple_items'),
- buttons: [
- { label: i18n('delete'), type: 'primary' },
- { label: i18n('cancel') },
- ],
- });
- if ( alert_resp === 'Delete' ) {
- for ( const row of trashedItems.toArray() ) {
- await window.delete_item(row);
- }
- }
- } else {
- // Move to trash
- await window.move_items($selectedRows.toArray(), window.trash_path);
- }
- }
- return false;
- }
-
- // Ctrl/Cmd + A - Select all
- if ( (e.ctrlKey || e.metaKey) && e.which === 65 ) {
- e.preventDefault();
- e.stopPropagation();
- $allRows.addClass('selected');
- if ( $allRows.length > 0 ) {
- window.active_element = $allRows.last().get(0);
- window.latest_selected_item = $allRows.last().get(0);
- }
- _this.updateFooterStats();
- return false;
- }
-
- // Ctrl/Cmd + C - Copy
- if ( (e.ctrlKey || e.metaKey) && e.which === 67 ) {
- if ( $selectedRows.length > 0 ) {
- e.preventDefault();
- e.stopPropagation();
- window.clipboard = [];
- window.clipboard_op = 'copy';
- $selectedRows.each(function () {
- if ( $(this).attr('data-path') !== window.trash_path ) {
- window.clipboard.push({
- path: $(this).attr('data-path'),
- uid: $(this).attr('data-uid'),
- metadata: $(this).attr('data-metadata'),
- });
- }
- });
- }
- return false;
- }
-
- // Ctrl/Cmd + X - Cut
- if ( (e.ctrlKey || e.metaKey) && e.which === 88 ) {
- if ( $selectedRows.length > 0 ) {
- e.preventDefault();
- e.stopPropagation();
- window.clipboard = [];
- window.clipboard_op = 'move';
- $selectedRows.each(function () {
- window.clipboard.push({
- path: $(this).attr('data-path'),
- uid: $(this).attr('data-uid'),
- });
- });
- }
- return false;
- }
-
- // Ctrl/Cmd + V - Paste
- if ( (e.ctrlKey || e.metaKey) && e.which === 86 ) {
- if ( window.clipboard.length > 0 && _this.currentPath ) {
- e.preventDefault();
- e.stopPropagation();
- // Don't allow paste in Trash unless it's a move operation
- if ( _this.currentPath.startsWith(window.trash_path) && window.clipboard_op !== 'move' ) {
- return false;
- }
- if ( window.clipboard_op === 'copy' ) {
- window.copy_clipboard_items(_this.currentPath, null);
- } else {
- _this.moveClipboardItems(_this.currentPath).then(() => {
- _this.renderDirectory(_this.currentPath);
- });
- }
- }
- return false;
- }
-
- // Arrow keys - Navigate items
- if ( e.which >= 37 && e.which <= 40 ) {
- e.preventDefault();
- e.stopPropagation();
-
- if ( $allRows.length === 0 ) return false;
-
- // If nothing selected, select first item
- if ( $selectedRows.length === 0 ) {
- const $first = $allRows.first();
- $first.addClass('selected');
- window.active_element = $first.get(0);
- window.latest_selected_item = $first.get(0);
- $first.get(0).scrollIntoView({ block: 'nearest' });
- _this.updateFooterStats();
- return false;
- }
-
- // Find current item and calculate next
- const $current = $(window.latest_selected_item || $selectedRows.last().get(0));
- const currentIndex = $allRows.index($current);
- let nextIndex = currentIndex;
-
- // Calculate grid dimensions for grid view
- const isGridView = $container.hasClass('files-grid-view');
- let cols = 1;
- if ( isGridView && $allRows.length > 1 ) {
- const firstTop = $allRows.eq(0).offset().top;
- for ( let i = 1; i < $allRows.length; i++ ) {
- if ( $allRows.eq(i).offset().top !== firstTop ) {
- cols = i;
- break;
- }
- }
- if ( cols === 1 ) cols = $allRows.length; // All on one row
- }
-
- // Calculate next index based on arrow key
- switch ( e.which ) {
- case 37: // Left
- nextIndex = Math.max(0, currentIndex - 1);
- break;
- case 38: // Up
- nextIndex = Math.max(0, currentIndex - cols);
- break;
- case 39: // Right
- nextIndex = Math.min($allRows.length - 1, currentIndex + 1);
- break;
- case 40: // Down
- nextIndex = Math.min($allRows.length - 1, currentIndex + cols);
- break;
- }
-
- if ( nextIndex !== currentIndex ) {
- const $next = $allRows.eq(nextIndex);
-
- if ( ! e.shiftKey ) {
- // Normal navigation - clear selection
- $allRows.removeClass('selected');
- }
-
- $next.addClass('selected');
- window.active_element = $next.get(0);
- window.latest_selected_item = $next.get(0);
- $next.get(0).scrollIntoView({ block: 'nearest' });
- _this.updateFooterStats();
-
- // If preview is open, switch to newly selected file
- if ( _this.previewOpen && !e.shiftKey ) {
- const newUid = $next.attr('data-uid');
- if ( newUid !== _this.previewCurrentUid ) {
- _this.showImagePreview($next);
- }
- }
- }
-
- return false;
- }
-
- // Space - Toggle image preview
- if ( e.which === 32 ) {
- e.preventDefault();
- e.stopPropagation();
-
- // If preview is open, close it
- if ( _this.previewOpen ) {
- _this.closeImagePreview();
- return false;
- }
-
- // Open preview for single selected image file
- if ( $selectedRows.length === 1 ) {
- const $row = $selectedRows.first();
- const isDir = $row.attr('data-is_dir') === '1';
- if ( ! isDir ) {
- _this.showImagePreview($row);
- }
- }
- return false;
- }
-
- // Type-to-select: letter/number keys search items by name
- if ( !e.ctrlKey && !e.metaKey && e.key.length === 1 ) {
- e.preventDefault();
- e.stopImmediatePropagation();
-
- if ( _this.typeSearchTerm !== '' ) {
- clearTimeout(_this.typeSearchTimeout);
- }
-
- _this.typeSearchTimeout = setTimeout(() => {
- _this.typeSearchTerm = '';
- }, 700);
-
- _this.typeSearchTerm += e.key.toLocaleLowerCase();
-
- let matches = [];
- const $currentSelected = $selectedRows.first();
-
- // If selected item already matches, keep it
- if ( $currentSelected.length === 1 ) {
- const selectedName = ($currentSelected.attr('data-name') || '').toLowerCase();
- if ( selectedName.startsWith(_this.typeSearchTerm) ) {
- return false;
- }
- }
-
- // Search all rows for matches
- for ( let j = 0; j < $allRows.length; j++ ) {
- const name = ($allRows.eq(j).attr('data-name') || '').toLowerCase();
- if ( name.startsWith(_this.typeSearchTerm) ) {
- matches.push($allRows.get(j));
- }
- }
-
- if ( matches.length > 0 ) {
- // If multiple matches and one is selected, cycle past it
- if ( $currentSelected.length > 0 && matches.length > 1 ) {
- let match_index;
- for ( let i = 0; i < matches.length - 1; i++ ) {
- if ( $(matches[i]).is($currentSelected) ) {
- match_index = i;
- break;
- }
- }
- if ( match_index !== undefined ) {
- matches.splice(0, match_index + 1);
- }
- }
-
- // Deselect all, select the match
- $allRows.removeClass('selected');
- $(matches[0]).addClass('selected');
- window.active_element = matches[0];
- window.latest_selected_item = matches[0];
- matches[0].scrollIntoView({ block: 'nearest' });
- _this.updateFooterStats();
- }
-
- return false;
- }
- });
- },
-
- /**
- * Shows an image preview popover for the selected file.
- *
- * Fetches a signed URL for the actual image and displays it in a centered
- * popover. The popover can be dismissed by pressing spacebar or clicking outside.
- *
- * @param {jQuery} $row - The selected row element
- * @returns {Promise}
- */
- async showImagePreview ($row) {
- const uid = $row.attr('data-uid');
- const fileName = $row.attr('data-name');
- const filePath = $row.attr('data-path');
-
- // Check if it's an image file
- const extension = fileName.split('.').pop().toLowerCase();
- const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg'];
- if ( ! imageExtensions.includes(extension) ) {
- return;
- }
-
- // Get read URL for the actual image
- const imageUrl = await puter.fs.getReadURL(filePath);
-
- // Remove any existing preview
- $('.image-preview-popover').remove();
-
- const $filesContainer = this.$el_window.find('.files-tab .files');
- const containerWidth = $filesContainer.width();
- const containerOffset = $filesContainer.offset();
-
- const previewHtml = `
-