From 6a9c8e3ac4f8cf0a0ebc7ae826fa005ea3f8fa4f Mon Sep 17 00:00:00 2001 From: jelveh Date: Tue, 21 Jul 2026 14:02:59 -0700 Subject: [PATCH] feat(gui): open dashboard apps maximized in-page with minimize + single instance Apps launched from the dashboard's Apps tab now open as maximized Puter windows in the same page (via launch_app) instead of a new browser tab. External website shortcuts still open in a new tab since arbitrary sites can't reliably be iframed. Dashboard app windows keep their minimize button (fullpage mode normally hides it since there's no taskbar). With no taskbar item to animate toward, hideWindow now hides the window in place and flags it with data-minimized_in_place; showWindow un-hides it via that flag. An explicit flag is used because data-orig-* can't distinguish the two minimize paths - drag/maximize handlers set those attrs too. Tiles are single-instance: clicking a tile un-hides a minimized instance or focuses a visible one instead of launching a duplicate, and launches in flight swallow repeat clicks. Also fixes .window-body-app height in dashboard mode: fullpage mode sizes it to 100% assuming no titlebar, which clipped the bottom 29px of every app. --- src/gui/src/UI/Dashboard/TabApps.js | 29 +++++++++++++++++++--- src/gui/src/UI/Dashboard/UIDashboard.js | 4 ++++ src/gui/src/UI/UIWindow.js | 32 +++++++++++++++++++++++++ src/gui/src/css/dashboard.css | 14 +++++++++++ 4 files changed, 76 insertions(+), 3 deletions(-) diff --git a/src/gui/src/UI/Dashboard/TabApps.js b/src/gui/src/UI/Dashboard/TabApps.js index fe82c639a..095ae1e86 100644 --- a/src/gui/src/UI/Dashboard/TabApps.js +++ b/src/gui/src/UI/Dashboard/TabApps.js @@ -1,5 +1,6 @@ import UIContextMenu from '../UIContextMenu.js'; import UIAlert from '../UIAlert.js'; +import launch_app from '../../helpers/launch_app.js'; import { isTouchPrimaryDevice } from './ContextMenu/ContextMenu.js'; import { reconcileAppOrder, serializeAppOrder, mergeSavedOrder, APPS_ORDER_KV_KEY } from './appOrder.js'; @@ -313,6 +314,7 @@ const TabApps = { _pendingLoad: null, _savedOrderNames: null, _orderSavedAtSeq: 0, + _launchingApps: new Set(), html () { let h = '
'; @@ -349,8 +351,9 @@ const TabApps = { }); // Handle app tile clicks. External apps carry a target link (their - // index_url) and open the app's website directly; everything else - // opens the Puter app page — matching the Home tab. + // index_url) and open the app's website directly in a new browser tab + // (an external site can't be reliably iframed); everything else + // launches the app as a maximized window in this same page. $el_window.on('click', '.myapps-tile', function (e) { e.preventDefault(); e.stopPropagation(); @@ -364,7 +367,27 @@ const TabApps = { if ( targetLink && targetLink !== '' ) { window.open(targetLink, '_blank', 'noopener,noreferrer'); } else if ( appName ) { - window.open(`/app/${appName}`, '_blank', 'noopener,noreferrer'); + // One instance per app when launched from here: un-hide a + // minimized instance / focus a visible one rather than + // launching a duplicate. + const $existing = $(`.window[data-app="${html_encode(appName)}"]`); + if ( $existing.length ) { + const $win = $existing.last(); + const minimized = $win.attr('data-is_minimized'); + if ( minimized === '1' || minimized === 'true' ) { + $win.showWindow(); + } else { + $win.focusWindow(); + } + return; + } + // A second click while the first launch's fetches are still in + // flight has no window to find yet — swallow it instead of + // spawning a duplicate instance. + if ( self._launchingApps.has(appName) ) return; + self._launchingApps.add(appName); + launch_app({ name: appName, maximized: true }) + .finally(() => self._launchingApps.delete(appName)); } }); diff --git a/src/gui/src/UI/Dashboard/UIDashboard.js b/src/gui/src/UI/Dashboard/UIDashboard.js index e5655bc88..65ff6a3cf 100644 --- a/src/gui/src/UI/Dashboard/UIDashboard.js +++ b/src/gui/src/UI/Dashboard/UIDashboard.js @@ -65,6 +65,10 @@ async function UIDashboard (options) { // eslint-disable-next-line no-unused-vars options = options ?? {}; + // Mark dashboard mode on so window chrome can adapt — e.g. app + // windows keep their minimize button even though fullpage-mode hides it. + $('body').addClass('dashboard-mode'); + // Create mutable tabs array from built-in tabs const tabs = [...builtinTabs]; diff --git a/src/gui/src/UI/UIWindow.js b/src/gui/src/UI/UIWindow.js index 4978f2892..2dcd064d6 100644 --- a/src/gui/src/UI/UIWindow.js +++ b/src/gui/src/UI/UIWindow.js @@ -3821,6 +3821,25 @@ $.fn.showWindow = async function (options) { if ( $(this).hasClass('window') ) { // show window const el_window = this; + + // A window minimized with no taskbar to animate toward (dashboard + // mode) was simply hidden in place by hideWindow — its geometry + // was never disturbed, so just un-hide it. (The explicit flag is + // used because data-orig-* can't distinguish the two minimize + // paths: drag/maximize handlers set it too.) + if ( $(el_window).attr('data-minimized_in_place') === '1' ) { + $(el_window).attr({ + 'data-is_minimized': false, + 'data-minimized_in_place': '0', + }); + $(el_window).fadeIn(150); + $(el_window).css('z-index', ++window.last_window_zindex); + setTimeout(() => { + $(this).focusWindow(); + }, 80); + return; + } + $(el_window).css({ 'transition': 'top 0.2s, left 0.2s, bottom 0.2s, right 0.2s, width 0.2s, height 0.2s', top: `${$(el_window).attr('data-orig-top') }px`, @@ -3963,6 +3982,19 @@ $.fn.hideWindow = async function (options) { // get taskbar item location let taskbar_item_pos = $(`.taskbar .taskbar-item[data-app="${$(this).attr('data-app')}"]`).position(); + // No taskbar item to animate toward (e.g. dashboard mode, which + // has no taskbar): simply hide the window in place, geometry + // untouched. showWindow() un-hides it via the + // data-minimized_in_place flag. + if ( ! taskbar_item_pos ) { + $(this).attr({ + 'data-is_minimized': true, + 'data-minimized_in_place': '1', + }); + $(this).fadeOut(150); + return; + } + // Calculate animation target based on taskbar position let animationTarget = {}; const taskbarPosition = window.taskbar_position || 'bottom'; diff --git a/src/gui/src/css/dashboard.css b/src/gui/src/css/dashboard.css index ec039bc5c..f8a1c87f9 100644 --- a/src/gui/src/css/dashboard.css +++ b/src/gui/src/css/dashboard.css @@ -4198,3 +4198,17 @@ body.myapps-reordering .myapps-tile { .submenu-arrow { margin-left: auto; } + +/* Dashboard mode runs fullpage (which normally hides the minimize button + because there's no taskbar to restore from), but here a minimized app can + be brought back from its Apps-tab tile — so keep the button available. */ +body.dashboard-mode .window-minimize-btn { + display: inline; +} + +/* Fullpage mode sizes .window-body-app to 100% because fullpage windows have + no titlebar — but dashboard-mode app windows keep their 29px head, so + without this the bottom of every app is clipped by that much. */ +body.dashboard-mode .window-body-app { + height: calc(100% - 29px) !important; +}