mirror of
https://github.com/HeyPuter/puter.git
synced 2026-08-25 07:27:04 +00:00
Dashboard files: clicking an open file returns to its window
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.
This commit is contained in:
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user