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.
This commit is contained in:
jelveh
2026-07-21 14:02:59 -07:00
parent 645409eddb
commit 6a9c8e3ac4
4 changed files with 76 additions and 3 deletions
+26 -3
View File
@@ -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 = '<div class="dashboard-tab-content myapps-tab">';
@@ -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));
}
});
+4
View File
@@ -65,6 +65,10 @@ async function UIDashboard (options) {
// eslint-disable-next-line no-unused-vars
options = options ?? {};
// Mark dashboard mode on <body> 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];
+32
View File
@@ -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';
+14
View File
@@ -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;
}