mirror of
https://github.com/HeyPuter/puter.git
synced 2026-08-24 23:17:23 +00:00
fix(gui): open add-an-app choices through the deep-link intro, not a + tile morph
60686643b made App Center and the AI builder morph out of the + tile the
user clicked, which answers where the window came from but not the
question these two options exist to answer: where the APP went. The grid
never showed it arriving, so the user still ended a launch with nothing
to find, no tile for minimize to fly back to, and no lesson learned.
Choosing either option now plays exactly what an /app/<name> landing
plays (beginDeepLinkLaunch): when the app already has a tile the intro
travels to it, plays the click flourish, and the window grows out of the
app's own slot; when it doesn't, the tile is INSTALLED first — the slot
opens, the progress stroke draws, the icon springs in — and only then
does the window morph out of the tile that just arrived. Same beats,
same exposure decay, same interruptibility, same duplicate-launch claim.
The app info is prefetched in parallel and handed to launch_app, the
same contract as initgui's landing path, so the intro (which may need to
draw the arriving tile from it) never costs a second round-trip.
This also reverts 60686643b's anchor plumbing (UIWindow's
dashboard_tile_el option and the dashboard_tile_in_view export): the
intro installs a real tile for the by-app-name morph to find, so no
stand-in anchor is needed.
This commit is contained in:
@@ -2,7 +2,7 @@ import UIContextMenu from '../UIContextMenu.js';
|
||||
import UIAlert from '../UIAlert.js';
|
||||
import launch_app from '../../helpers/launch_app.js';
|
||||
import revokeAppSessions from '../../helpers/revoke_app_sessions.js';
|
||||
import { begin_dashboard_tile_launch, dashboard_tile_in_view, settle_dashboard_tile_launch } from '../UIWindow.js';
|
||||
import { begin_dashboard_tile_launch, settle_dashboard_tile_launch } from '../UIWindow.js';
|
||||
import { isTouchPrimaryDevice } from './ContextMenu/ContextMenu.js';
|
||||
import { reconcileAppOrder, serializeAppOrder, mergeSavedOrder, APPS_ORDER_KV_KEY } from './appOrder.js';
|
||||
import { parseRemovedApps, serializeRemovedApps, REMOVED_APPS_KV_KEY } from './removedApps.js';
|
||||
@@ -654,33 +654,46 @@ function showAddAppModal ({ $el_window }) {
|
||||
else close();
|
||||
});
|
||||
|
||||
// Opened exactly as a tile click opens it, click flourish and
|
||||
// grow-out-of-the-icon morph included. The anchor is resolved HERE, once,
|
||||
// so the icon half and the window half can never disagree about which
|
||||
// tile this launch came from: the app's own tile when it already has one
|
||||
// on screen (minimize will fly the window back to it, so that is where
|
||||
// opening should come from), and otherwise the + tile the user clicked —
|
||||
// which is the whole point for an app they don't have yet, since
|
||||
// UIWindow's by-app-name lookup can't find a tile that doesn't exist.
|
||||
// Opened exactly like an /app/<name> landing: the same pedagogical
|
||||
// click→morph→open intro plays (see beginDeepLinkLaunch) — and since
|
||||
// choosing App Center or the AI builder is usually asking for an app the
|
||||
// grid doesn't have yet, the intro is also what INSTALLS it: the tile
|
||||
// arrives in the grid with the install choreography, and only then does
|
||||
// the window grow out of the app's own slot. The user watches the app
|
||||
// they asked for become a tile they can find again; the window never
|
||||
// just appears from nowhere.
|
||||
const openApp = appName => {
|
||||
if ( focusExistingAppWindow(appName) ) return;
|
||||
const tile = dashboard_tile_in_view(appName)
|
||||
|| $el_window.find('.myapps-add-tile')[0]
|
||||
|| null;
|
||||
begin_dashboard_tile_launch(tile);
|
||||
launch_app({
|
||||
name: appName,
|
||||
maximized: true,
|
||||
window_options: {
|
||||
morph_from_dashboard_tile: true,
|
||||
dashboard_tile_el: tile,
|
||||
},
|
||||
})
|
||||
.catch(err => {
|
||||
// Same duplicate-launch guard as the tile click handler — a second
|
||||
// trip through the modal mid-intro must not spawn a second instance.
|
||||
if ( TabApps._launchingApps.has(appName) ) return;
|
||||
// Fetched in parallel with the intro, which may need it to DRAW the
|
||||
// arriving tile, then handed to launch_app so the intro never costs
|
||||
// a second app-info round-trip — the same contract as the landing's
|
||||
// prefetch in initgui. A failed prefetch hands nothing over;
|
||||
// launch_app refetches and fails the way it always did.
|
||||
const app_info_promise = puter.apps.get(appName, { icon_size: 128 })
|
||||
.catch(() => null);
|
||||
(async () => {
|
||||
let tile = null;
|
||||
try {
|
||||
tile = await TabApps.beginDeepLinkLaunch(appName, $el_window, app_info_promise);
|
||||
} catch ( _e ) {
|
||||
// No intro — still launch.
|
||||
}
|
||||
const app_obj = await app_info_promise;
|
||||
launch_app({
|
||||
name: appName,
|
||||
maximized: true,
|
||||
...(app_obj ? { app_obj } : {}),
|
||||
window_options: { morph_from_dashboard_tile: true },
|
||||
}).catch(err => {
|
||||
console.error(`Failed to launch ${appName}:`, err);
|
||||
UIAlert(i18n('something_went_wrong'));
|
||||
})
|
||||
.finally(() => settle_dashboard_tile_launch(tile));
|
||||
}).finally(() => {
|
||||
TabApps.settleDeepLinkLaunch(appName, tile);
|
||||
});
|
||||
})();
|
||||
};
|
||||
|
||||
$overlay.on('click', '.myapps-add-option', function () {
|
||||
|
||||
+13
-29
@@ -716,14 +716,7 @@ async function UIWindow (options) {
|
||||
if ( options.morph_from_dashboard_tile && window.animate_window_opening
|
||||
&& options.is_visible && ! options.fadeIn
|
||||
&& ! (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) ) {
|
||||
// A named tile wins over the by-app-name lookup: the launcher knows
|
||||
// which icon the user actually clicked, and it is the icon whose
|
||||
// click feedback has already played. The grid's add-an-app tile uses
|
||||
// this to open apps that have no tile of their own — an app you
|
||||
// don't have yet can't be found by name (see showAddAppModal).
|
||||
const tile = (options.dashboard_tile_el && dashboard_tile_visible(options.dashboard_tile_el))
|
||||
? options.dashboard_tile_el
|
||||
: dashboard_tile_in_view(options.app);
|
||||
const tile = dashboard_tile_in_view(options.app);
|
||||
if ( tile ) {
|
||||
// .window is display:none from the stylesheet until the show()
|
||||
// further down; the morph needs the window laid out to measure
|
||||
@@ -4161,14 +4154,23 @@ $.fn.focusWindow = function (event) {
|
||||
* the user will actually look for the app (and where opening it from will put
|
||||
* it back). See buildGroupTileHtml for the data-group-apps this reads.
|
||||
*/
|
||||
export function dashboard_tile_in_view (app_name) {
|
||||
function dashboard_tile_in_view (app_name) {
|
||||
if ( ! app_name || typeof CSS === 'undefined' || ! CSS.escape ) return null;
|
||||
const in_view = tile => {
|
||||
const rect = tile.getBoundingClientRect();
|
||||
if ( rect.width <= 0 || rect.height <= 0 ) return false;
|
||||
const scroller = tile.closest('.myapps-pager-scroller');
|
||||
const clip = (scroller || tile.parentElement).getBoundingClientRect();
|
||||
const cx = rect.left + rect.width / 2;
|
||||
const cy = rect.top + rect.height / 2;
|
||||
return cx >= clip.left && cx <= clip.right && cy >= clip.top && cy <= clip.bottom;
|
||||
};
|
||||
|
||||
const tiles = document.querySelectorAll(
|
||||
`.dashboard-section-apps.active .myapps-tile[data-app-name="${CSS.escape(app_name)}"]`,
|
||||
);
|
||||
for ( const tile of tiles ) {
|
||||
if ( dashboard_tile_visible(tile) ) return tile;
|
||||
if ( in_view(tile) ) return tile;
|
||||
}
|
||||
|
||||
const folders = document.querySelectorAll('.dashboard-section-apps.active .myapps-group-tile');
|
||||
@@ -4179,29 +4181,11 @@ export function dashboard_tile_in_view (app_name) {
|
||||
} catch ( _e ) {
|
||||
continue;
|
||||
}
|
||||
if ( Array.isArray(names) && names.includes(app_name) && dashboard_tile_visible(folder) ) return folder;
|
||||
if ( Array.isArray(names) && names.includes(app_name) && in_view(folder) ) return folder;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a tile is rendered AND on the pager page currently in view. The
|
||||
* page test is the point: pages sit side by side in a horizontal scroller,
|
||||
* so an off-page tile has a real rendered box just past the scroller's clip
|
||||
* edge, and a window morphing out of one would grow from nowhere the user
|
||||
* is looking.
|
||||
*/
|
||||
function dashboard_tile_visible (tile) {
|
||||
if ( ! tile || ! tile.getBoundingClientRect ) return false;
|
||||
const rect = tile.getBoundingClientRect();
|
||||
if ( rect.width <= 0 || rect.height <= 0 ) return false;
|
||||
const scroller = tile.closest('.myapps-pager-scroller');
|
||||
const clip = (scroller || tile.parentElement).getBoundingClientRect();
|
||||
const cx = rect.left + rect.width / 2;
|
||||
const cy = rect.top + rect.height / 2;
|
||||
return cx >= clip.left && cx <= clip.right && cy >= clip.top && cy <= clip.bottom;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Dashboard app URL ownership
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user