From 32e417fe790a52f50d504a1a55c919396c338822 Mon Sep 17 00:00:00 2001 From: jelveh Date: Wed, 29 Jul 2026 19:03:33 -0700 Subject: [PATCH] Dashboard files: clicking an open file returns to its window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In dashboard mode there is no taskbar, so a minimized app window's only switcher was its Apps-tab tile — invisible from the Files tab. Opening a file, minimizing, and clicking the file again launched a second instance of the app, stranding the first (and any unsaved edits) somewhere unreachable. Make the file row itself the switcher: - launch_app stamps the opened file's uid on the window (data-file_uid; the signature's uid wins so shortcuts resolve to their target) - open_item (dashboard mode) restores/focuses an existing window that has the file open instead of launching a duplicate, mirroring the Apps-tab tile's single-instance behavior — keyed by file, not app, so opening a different file still gets its own instance - re-clicks while a launch's fetches are still in flight are swallowed (same idea as TabApps._launchingApps, keyed by file uid, TTL'd so a failed launch can't swallow clicks forever) - dashboard file opens now default to maximized, so they get the same headless full-tab chrome + control drawer as tile launches instead of a floating titlebar window - rows show a dot while their file is open in a (possibly minimized) window — under the name in grid view, inline after it in list view — driven by the existing dashboard-app-windows-changed event Desktop mode is untouched: the reuse branch and maximized default are gated on is_dashboard_mode, and opening the same file twice there still creates two windows as before. --- src/gui/src/UI/Dashboard/TabFiles.js | 31 ++++++++++++++ src/gui/src/UI/UIWindow.js | 1 + src/gui/src/css/dashboard.css | 20 +++++++++ src/gui/src/helpers/launch_app.js | 5 +++ src/gui/src/helpers/open_item.js | 63 ++++++++++++++++++++++++++-- 5 files changed, 116 insertions(+), 4 deletions(-) diff --git a/src/gui/src/UI/Dashboard/TabFiles.js b/src/gui/src/UI/Dashboard/TabFiles.js index 9f3324157..1cd00577d 100644 --- a/src/gui/src/UI/Dashboard/TabFiles.js +++ b/src/gui/src/UI/Dashboard/TabFiles.js @@ -564,6 +564,14 @@ const TabFiles = { // Setup keyboard shortcuts this.setupKeyboardShortcuts(); + // Rows double as the switcher for app windows opened from this tab + // (open_item restores a file's existing window instead of launching + // a duplicate): a dot marks files that are open in a — possibly + // minimized — app window. UIWindow fires this on window open/close. + document.addEventListener('dashboard-app-windows-changed', () => { + this.updateOpenFileDots(); + }); + // Refresh current directory when the user returns to this browser tab document.addEventListener('visibilitychange', async () => { if ( document.visibilityState !== 'visible' || !this.currentPath ) return; @@ -2145,6 +2153,7 @@ const TabFiles = { this.applyColumnWidths(); this.updateFooterStats(); this.updateNavButtonStates(); + this.updateOpenFileDots(); this.hideSpinner(); this.renderingDirectory = false; }, @@ -2253,6 +2262,28 @@ const TabFiles = { this.createItemListeners(row, file); }, + /** + * Toggles the open-file dot on rows whose file is currently open in an + * app window — visible or minimized. Matches each row's uid (shortcuts + * resolve to their target) against the data-file_uid that launch_app + * stamps on app windows. In dashboard mode the row doubles as that + * window's switcher (clicking it restores instead of relaunching — see + * open_item.js), so the dot marks where a click will return, not launch. + * + * @returns {void} + */ + updateOpenFileDots () { + if ( ! this.$el_window ) return; + const open_uids = new Set(); + $('.window[data-file_uid]').each(function () { + open_uids.add($(this).attr('data-file_uid')); + }); + this.$el_window.find('.files-tab .files .row').each(function () { + const uid = ($(this).attr('data-shortcut_to') || $(this).attr('data-uid') || '').toLowerCase(); + $(this).toggleClass('file-is-open', open_uids.has(uid)); + }); + }, + /** * Attaches event listeners to a file/folder row element. * diff --git a/src/gui/src/UI/UIWindow.js b/src/gui/src/UI/UIWindow.js index 5d28a99ec..a7791bcaf 100644 --- a/src/gui/src/UI/UIWindow.js +++ b/src/gui/src/UI/UIWindow.js @@ -275,6 +275,7 @@ async function UIWindow (options) { data-element_uuid="${html_encode(options.element_uuid)}" data-parent_uuid="${html_encode(options.parent_uuid)}" ${options.parent_instance_id ? `data-parent_instance_id="${options.parent_instance_id}"` : ''} + ${options.file_uid ? `data-file_uid="${html_encode(String(options.file_uid).toLowerCase())}"` : ''} data-id ="${win_id}" data-iframe_msg_uid ="${html_encode(options.iframe_msg_uid)}" data-is_dir ="${options.is_dir}" diff --git a/src/gui/src/css/dashboard.css b/src/gui/src/css/dashboard.css index 5952bf1c8..7fb19c522 100644 --- a/src/gui/src/css/dashboard.css +++ b/src/gui/src/css/dashboard.css @@ -1766,6 +1766,26 @@ body.myapps-reordering .myapps-tile { line-height: 32px; } +/* Open-file dot: marks rows whose file is open in an app window (visible or + minimized) — the row doubles as that window's switcher (clicking it + restores the window; see open_item.js), sibling of the Apps-tab tiles' + running dot. Kept current by updateOpenFileDots in TabFiles.js. */ +.dashboard-section-files .files-tab .files .row.file-is-open .item-name-wrapper::after { + content: ''; + flex: 0 0 auto; + width: 4px; + height: 4px; + border-radius: 50%; + background: var(--dashboard-text-tertiary, #71717a); +} + +/* Grid view: the name wrapper is a block, not a flex row — center the dot + under the (up to two-line) name, mirroring the tile dot under its icon. */ +.dashboard-section-files .files-tab .files.files-grid-view .row.file-is-open .item-name-wrapper::after { + display: block; + margin: 4px auto 0; +} + /* On touch devices, item-name/icon/badges must not be direct event targets so that pointerdown's isDragHandle check can't match them and accidentally select items when the user is scrolling. Events pass through to the .row. */ diff --git a/src/gui/src/helpers/launch_app.js b/src/gui/src/helpers/launch_app.js index c12004750..f14976f3e 100644 --- a/src/gui/src/helpers/launch_app.js +++ b/src/gui/src/helpers/launch_app.js @@ -648,6 +648,11 @@ const launch_app = async (options) => { width: window_width, app: options.name, iframe_credentialless: credentialless, + // The file this instance was launched to open, stamped on the + // window as data-file_uid so open_item can restore this window + // when the same file is opened again (the signature's uid wins: + // it's resolved, e.g. a shortcut's uid becomes its target's). + file_uid: file_signature?.uid ?? options.file_uid, is_visible: !app_info.background, is_maximized: options.maximized, is_fullpage: options.is_fullpage, diff --git a/src/gui/src/helpers/open_item.js b/src/gui/src/helpers/open_item.js index 8c03c328e..7bdce3cc2 100644 --- a/src/gui/src/helpers/open_item.js +++ b/src/gui/src/helpers/open_item.js @@ -24,6 +24,24 @@ import launch_app from './launch_app.js'; import path from '../lib/path.js'; import item_icon from './item_icon.js'; +// Files whose app launch is still in flight, by file uid. Between the click +// and the window's creation there is nothing in the DOM to restore, so a +// re-click during that stretch would mint a duplicate instance — mark the +// launch and swallow re-clicks instead (same idea as TabApps._launchingApps). +// Entries carry a timestamp so a launch that dies without settling (an +// unexpected throw before launch_app is reached) can't swallow clicks forever. +const launching_file_uids = new Map(); +const LAUNCH_GUARD_TTL = 15000; +const file_launch_in_flight = (uid) => { + const ts = launching_file_uids.get(uid); + if ( ! ts ) return false; + if ( Date.now() - ts > LAUNCH_GUARD_TTL ) { + launching_file_uids.delete(uid); + return false; + } + return true; +}; + const open_item = async function (options) { let el_item = options.item; const $el_parent_window = $(el_item).closest('.window'); @@ -35,6 +53,10 @@ const open_item = async function (options) { const shortcut_to_path = $(el_item).attr('data-shortcut_to_path'); const associated_app_name = $(el_item).attr('data-associated_app_name'); const file_uid = $(el_item).attr('data-uid'); + // Normalized identity of the file being opened (shortcuts resolve to + // their target) — matched against the data-file_uid that launch_app + // stamps on app windows. + const target_file_uid = (!is_dir && uid) ? String(uid).toLowerCase() : null; //---------------------------------------------------------------- // Is this an app shortcut? @@ -235,16 +257,40 @@ Please try recreating the link.`); } } //---------------------------------------------------------------- + // Dashboard: is this file already open in an app window? In + // dashboard mode there is no taskbar — the file's row is where a + // user goes looking for a window they minimized — so restore that + // window (edits intact) instead of minting a second instance. + //---------------------------------------------------------------- + else if ( window.is_dashboard_mode && !associated_app_name && target_file_uid + && ($(`.window[data-file_uid="${html_encode(target_file_uid)}"]`).length || file_launch_in_flight(target_file_uid)) ) { + const $win = $(`.window[data-file_uid="${html_encode(target_file_uid)}"]`).last(); + // No window yet means the first click's launch is still in + // flight — swallow the re-click instead of duplicating it. + if ( $win.length ) { + const minimized = $win.attr('data-is_minimized'); + if ( minimized === '1' || minimized === 'true' ) { + $win.showWindow(); + } else { + $win.focusWindow(); + } + } + } + //---------------------------------------------------------------- // Does the user have a preference for this file type? //---------------------------------------------------------------- else if ( !associated_app_name && !is_dir && window.user_preferences[`default_apps${path.extname(item_path).toLowerCase()}`] ) { - launch_app({ + const launch_promise = launch_app({ name: window.user_preferences[`default_apps${path.extname(item_path).toLowerCase()}`], file_path: item_path, window_title: path.basename(item_path), - maximized: options.maximized, + maximized: options.maximized ?? window.is_dashboard_mode, file_uid: file_uid, }); + if ( target_file_uid ) { + launching_file_uids.set(target_file_uid, Date.now()); + launch_promise.finally(() => launching_file_uids.delete(target_file_uid)); + } } //---------------------------------------------------------------- // Is there an app associated with this item? @@ -252,6 +298,7 @@ Please try recreating the link.`); else if ( associated_app_name !== '' ) { launch_app({ name: associated_app_name, + maximized: options.maximized ?? window.is_dashboard_mode, }); } //---------------------------------------------------------------- @@ -289,6 +336,11 @@ Please try recreating the link.`); const fsuid = uid.toLowerCase(); let open_item_meta; + // The stretch from here to the launch is where a re-click would + // find no window to restore yet — mark the file as launching so + // the dashboard reuse branch above swallows re-clicks meanwhile. + if ( target_file_uid ) launching_file_uids.set(target_file_uid, Date.now()); + // get all info needed to open an item try { open_item_meta = await $.ajax({ @@ -320,6 +372,8 @@ Please try recreating the link.`); // download //--------------------------------------------- if ( suggested_apps.length === 0 ) { + // Not launching after all — lift the in-flight guard. + if ( target_file_uid ) launching_file_uids.delete(target_file_uid); //--------------------------------------------- // If .zip file, unzip it //--------------------------------------------- @@ -355,16 +409,17 @@ Please try recreating the link.`); // First suggested app is default app to open this item //--------------------------------------------- else { - launch_app({ + const launch_promise = launch_app({ name: suggested_apps[0].name, token: open_item_meta.token, file_path: item_path, app_obj: suggested_apps[0], window_title: path.basename(item_path), file_uid: fsuid, - maximized: options.maximized, + maximized: options.maximized ?? window.is_dashboard_mode, file_signature: open_item_meta.signature, }); + if ( target_file_uid ) launch_promise.finally(() => launching_file_uids.delete(target_file_uid)); } } };