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; +}