diff --git a/src/gui/src/IPC.js b/src/gui/src/IPC.js index bd682f1a0..4a7dce974 100644 --- a/src/gui/src/IPC.js +++ b/src/gui/src/IPC.js @@ -618,6 +618,17 @@ const ipc_listener = async (event, handled) => { // floating control pill instead of a head) $(el_window).find('.window-head-title').html(html_encode(event.data.new_title)); $(el_window).find('.dashboard-app-pill-title').text(event.data.new_title); + // In dashboard mode the app's title is also the browser-tab title, + // and data-name is what a restore re-applies (see showWindow's + // push) — keep both fresh. The tab title only changes while this + // window's app actually owns the URL. + if ( window.is_dashboard_mode ) { + $(el_window).attr('data-name', event.data.new_title); + const app = $(el_window).attr('data-app'); + if ( app && window.location.pathname === `/app/${encodeURIComponent(app)}` ) { + document.title = event.data.new_title; + } + } // send confirmation to requester window target_iframe.contentWindow.postMessage({ original_msg_id: msg_id, diff --git a/src/gui/src/UI/Dashboard/TabApps.js b/src/gui/src/UI/Dashboard/TabApps.js index 2b9020c2e..fda2d8ecc 100644 --- a/src/gui/src/UI/Dashboard/TabApps.js +++ b/src/gui/src/UI/Dashboard/TabApps.js @@ -182,6 +182,11 @@ function showUninstallModal ({ appName, appTitle, appUid, self, $el_window }) { // restores its position if it comes back. const removedIndex = self._apps.findIndex(a => a.name === appName); const removedApp = removedIndex === -1 ? null : self._apps[removedIndex]; + // A running instance would be stranded: the tile is a headless + // app's only switcher, so once it's gone a minimized window could + // never be restored OR quit. Close the app's windows first (close + // also consumes the app's URL entry if it owns one). + $(`.window[data-app="${html_encode(appName)}"]`).close(); self._invalidateInFlightLoads(); close(); diff --git a/src/gui/src/UI/UIWindow.js b/src/gui/src/UI/UIWindow.js index d90abdb13..6573b7df5 100644 --- a/src/gui/src/UI/UIWindow.js +++ b/src/gui/src/UI/UIWindow.js @@ -4159,6 +4159,7 @@ function push_dashboard_app_url (app_name, title) { } window.history.pushState({ dashboard_app: app_name }, '', `/app/${encodeURIComponent(app_name)}`); dashboard_url_app = app_name; + dashboard_url_pop_pending = false; if ( title ) document.title = title; } @@ -4171,15 +4172,28 @@ function push_dashboard_app_url (app_name, title) { * when this app doesn't own the URL (caller falls back to hiding * directly, e.g. an app stacked under another app's entry). */ +// True while a pop's history.back() is in flight (issued but its +// popstate not yet processed). The URL doesn't change until the popstate +// lands, so without this latch a double-click on minimize — or a close +// racing a minimize — would issue TWO back()s, and the second would pop +// the dashboard's own entry and navigate clean out of the page. +let dashboard_url_pop_pending = false; + function pop_dashboard_app_url (app_name) { if ( ! window.is_dashboard_mode || ! app_name ) return false; if ( dashboard_app_url_current() !== app_name ) return false; + // Duplicate request for an entry already being popped: report it + // handled so the caller doesn't ALSO hide the window. + if ( dashboard_url_pop_pending ) return true; + dashboard_url_pop_pending = true; window.history.back(); return true; } window.addEventListener('popstate', () => { if ( ! window.is_dashboard_mode ) return; + // Any traversal settles a pending pop (see pop_dashboard_app_url). + dashboard_url_pop_pending = false; const new_app = dashboard_app_url_current(); const prev_app = dashboard_url_app; // Same app on both sides means the traversal wasn't ours (e.g. an app diff --git a/src/gui/src/css/dashboard.css b/src/gui/src/css/dashboard.css index 43eb677dc..1aea08579 100644 --- a/src/gui/src/css/dashboard.css +++ b/src/gui/src/css/dashboard.css @@ -4242,7 +4242,7 @@ body.dashboard-mode .window-dashboard-headless .window-body-app { background: rgba(24, 24, 28, 0.62); -webkit-backdrop-filter: blur(14px) saturate(140%); backdrop-filter: blur(14px) saturate(140%); - border: 1px solid rgba(255, 255, 255, 0.16); + border: 1px solid rgba(255, 255, 255, 0.22); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.28), 0 1px 3px rgba(0, 0, 0, 0.2); color: #fff; -webkit-user-select: none; @@ -4276,7 +4276,9 @@ body.dashboard-mode .window-dashboard-headless .window-body-app { } .dashboard-app-pill.collapsed { - opacity: 0.55; + /* Subtle but findable over both light and dark app content — the + white hairline border carries the dark-on-dark case. */ + opacity: 0.65; padding: 3px 12px; gap: 0; }