feat(start-menu): add "Add to Desktop" option for all apps (#2386)
Docker Image CI / build-and-push-image (push) Has been cancelled
Maintain Release Merge PR / update-release-pr (push) Has been cancelled
release-please / release-please (push) Has been cancelled
test / test-backend (24.x) (push) Has been cancelled
test / API tests (node env, api-test) (24.x) (push) Has been cancelled
test / puterjs (node env, vitest) (24.x) (push) Has been cancelled

This commit is contained in:
Het Savani
2026-02-11 21:59:14 -08:00
committed by GitHub
parent 1d22ab5fb7
commit 5cd731db3d
6 changed files with 104 additions and 13 deletions
+28
View File
@@ -1305,6 +1305,7 @@
"version": "7.28.5",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -1621,6 +1622,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
@@ -1659,6 +1661,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
@@ -3431,6 +3434,7 @@
"node_modules/@jimp/custom": {
"version": "0.22.12",
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/core": "^0.22.12"
}
@@ -3461,6 +3465,7 @@
"node_modules/@jimp/plugin-blit": {
"version": "0.22.12",
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/utils": "^0.22.12"
},
@@ -3471,6 +3476,7 @@
"node_modules/@jimp/plugin-blur": {
"version": "0.22.12",
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/utils": "^0.22.12"
},
@@ -3491,6 +3497,7 @@
"node_modules/@jimp/plugin-color": {
"version": "0.22.12",
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/utils": "^0.22.12",
"tinycolor2": "^1.6.0"
@@ -3528,6 +3535,7 @@
"node_modules/@jimp/plugin-crop": {
"version": "0.22.12",
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/utils": "^0.22.12"
},
@@ -3631,6 +3639,7 @@
"node_modules/@jimp/plugin-resize": {
"version": "0.22.12",
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/utils": "^0.22.12"
},
@@ -3641,6 +3650,7 @@
"node_modules/@jimp/plugin-rotate": {
"version": "0.22.12",
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/utils": "^0.22.12"
},
@@ -3654,6 +3664,7 @@
"node_modules/@jimp/plugin-scale": {
"version": "0.22.12",
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/utils": "^0.22.12"
},
@@ -3894,6 +3905,7 @@
"node_modules/@opentelemetry/api": {
"version": "1.9.0",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=8.0.0"
}
@@ -5726,6 +5738,7 @@
"version": "8.48.0",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.48.0",
"@typescript-eslint/types": "8.48.0",
@@ -6357,6 +6370,7 @@
"node_modules/acorn": {
"version": "8.15.0",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -7014,6 +7028,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.25",
"caniuse-lite": "^1.0.30001754",
@@ -7212,6 +7227,7 @@
"node_modules/chai": {
"version": "4.5.0",
"license": "MIT",
"peer": true,
"dependencies": {
"assertion-error": "^1.1.0",
"check-error": "^1.0.3",
@@ -8505,6 +8521,7 @@
"node_modules/eslint": {
"version": "9.39.1",
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -14325,6 +14342,7 @@
"version": "8.17.1",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -15766,6 +15784,7 @@
"version": "5.9.3",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -15938,6 +15957,7 @@
"version": "7.2.6",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -16029,6 +16049,7 @@
"version": "4.0.14",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vitest/expect": "4.0.14",
"@vitest/mocker": "4.0.14",
@@ -16142,6 +16163,7 @@
"version": "5.103.0",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.8",
@@ -16189,6 +16211,7 @@
"version": "5.1.4",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@discoveryjs/json-ext": "^0.5.0",
"@webpack-cli/configtest": "^2.1.1",
@@ -16387,6 +16410,7 @@
"node_modules/winston": {
"version": "3.18.3",
"license": "MIT",
"peer": true,
"dependencies": {
"@colors/colors": "^1.6.0",
"@dabh/diagnostics": "^2.0.8",
@@ -16504,6 +16528,7 @@
"node_modules/ws": {
"version": "8.18.3",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10.0.0"
},
@@ -16693,6 +16718,7 @@
"node_modules/zod": {
"version": "3.25.76",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -16813,6 +16839,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.4.1.tgz",
"integrity": "sha512-O2yRJce1GOc6PAy3QxFM4NzFiWzvScDC1/5ihYBL6BUEVdq0XMWN01sppE+H6bBXbaFYipjwFLEWLg5PaSOThA==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=8.0.0"
}
@@ -16822,6 +16849,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.49.1.tgz",
"integrity": "sha512-kaNl/T7WzyMUQHQlVq7q0oV4Kev6+0xFwqzofryC66jgGMacd0QH5TwfpbUwSTby+SdAdprAe5UKMvBw4tKS5Q==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/api": "^1.0.0"
},
+13 -13
View File
@@ -35,12 +35,12 @@ import mime from '../lib/mime.js';
const AI_APP_NAME = 'ai';
const parseItemMetadataForAI = (metadata) => {
if (!metadata) {
if ( ! metadata ) {
return undefined;
}
try {
return JSON.parse(metadata);
} catch (error) {
} catch ( error ) {
console.warn('Failed to parse item metadata for AI payload.', error);
return undefined;
}
@@ -67,17 +67,17 @@ const buildAIPayloadFromItems = ($elements) => {
const ensureAIAppIframe = async () => {
let $aiWindow = $(`.window[data-app="${AI_APP_NAME}"]`);
if ($aiWindow.length === 0) {
if ( $aiWindow.length === 0 ) {
try {
await launch_app({ name: AI_APP_NAME });
} catch (error) {
} catch ( error ) {
console.error('Failed to launch AI app.', error);
return null;
}
$aiWindow = $(`.window[data-app="${AI_APP_NAME}"]`);
}
if ($aiWindow.length === 0) {
if ( $aiWindow.length === 0 ) {
return null;
}
@@ -88,12 +88,12 @@ const ensureAIAppIframe = async () => {
const sendSelectionToAIApp = async ($elements) => {
const items = buildAIPayloadFromItems($elements);
if (items.length === 0) {
if ( items.length === 0 ) {
return;
}
const aiIframe = await ensureAIAppIframe();
if (!aiIframe || !aiIframe.contentWindow) {
if ( !aiIframe || !aiIframe.contentWindow ) {
await UIAlert({
message: i18n('ai_app_unavailable'),
});
@@ -107,7 +107,7 @@ const sendSelectionToAIApp = async ($elements) => {
}, '*');
};
function UIItem(options){
function UIItem (options) {
const matching_appendto_count = $(options.appendTo).length;
if ( matching_appendto_count > 1 ) {
$(options.appendTo).each(function () {
@@ -138,7 +138,7 @@ function UIItem(options){
options.shortcut_to_path = options.shortcut_to_path ?? '';
options.immutable = (options.immutable === false || options.immutable === 0 || options.immutable === undefined ? 0 : 1);
options.sort_container_after_append = (options.sort_container_after_append !== undefined ? options.sort_container_after_append : false);
const is_shared_with_me = (options.path !== `/${window.user.username}` && !options.path.startsWith(`/${window.user.username}/`));
const is_shared_with_me = (options.path && options.path !== `/${window.user.username}` && !options.path.startsWith(`/${window.user.username}/`));
let website_url = window.determine_website_url(options.path);
@@ -988,9 +988,9 @@ function UIItem(options){
// -------------------------------------------
menu_items.push({
html: i18n('open_in_ai'),
onClick: async function(){
onClick: async function () {
await sendSelectionToAIApp($selected_items);
}
},
});
// -------------------------------------------
// -
@@ -1332,9 +1332,9 @@ function UIItem(options){
// -------------------------------------------
menu_items.push({
html: i18n('open_in_ai'),
onClick: async function(){
onClick: async function () {
await sendSelectionToAIApp($(el_item));
}
},
});
}
+20
View File
@@ -236,6 +236,26 @@ async function UITaskbar (options) {
const $existingTaskbarItem = $(`.taskbar-item[data-app="${e.currentTarget.dataset.appName}"]`);
const isPinned = $existingTaskbarItem.length > 0 && $existingTaskbarItem.attr('data-keep-in-taskbar') === 'true';
items.push({
html: i18n('add_to_desktop'),
onClick: async function () {
try {
const fileName = `${e.currentTarget.dataset.appTitle}.app`;
const content = JSON.stringify({
app: e.currentTarget.dataset.appName,
title: e.currentTarget.dataset.appTitle,
icon: e.currentTarget.dataset.appIcon,
});
await puter.fs.upload(new File([content], fileName), window.desktop_path, {
generateThumbnails: true,
});
} catch ( err ) {
console.error('Failed to add shortcut to desktop:', err);
// Consider showing a user-facing error notification
}
},
});
if ( ! isPinned ) {
items.push({
html: i18n('keep_in_taskbar'),
+16
View File
@@ -93,6 +93,22 @@ const item_icon = async (fsentry) => {
return { image: trash_img, type: 'icon' };
}
// --------------------------------------------------
// .app files
// --------------------------------------------------
else if ( fsentry.name && fsentry.name.toLowerCase().endsWith('.app') ) {
try {
const content = await puter.fs.read({ path: fsentry.path });
const text = typeof content === 'string' ? content : await content.text();
const data = JSON.parse(text);
if ( data.icon ) {
return { image: data.icon, type: 'icon' };
}
} catch (e) {
// ignore
}
return { image: window.icons['app.svg'], type: 'icon' };
}
// --------------------------------------------------
// Directories
// --------------------------------------------------
else if ( fsentry.is_dir ) {
+26
View File
@@ -36,6 +36,32 @@ const open_item = async function (options) {
const associated_app_name = $(el_item).attr('data-associated_app_name');
const file_uid = $(el_item).attr('data-uid');
//----------------------------------------------------------------
// Is this an app shortcut?
//----------------------------------------------------------------
const app_name = $(el_item).attr('data-app');
if ( app_name ) {
launch_app({ name: app_name });
return;
}
//----------------------------------------------------------------
// Is this an .app file?
//----------------------------------------------------------------
if ( item_path && item_path.toLowerCase().endsWith('.app') ) {
try {
const content = await puter.fs.read({ path: item_path });
const text = typeof content === 'string' ? content : await content.text();
const data = JSON.parse(text);
if ( data.app ) {
launch_app({ name: data.app });
return;
}
} catch (e) {
console.error('Error reading .app file:', e);
}
}
//----------------------------------------------------------------
// Is this a shortcut whose source is perma-deleted?
//----------------------------------------------------------------
+1
View File
@@ -168,6 +168,7 @@ const en = {
item: 'item',
items_in_trash_cannot_be_renamed: 'This item can\'t be renamed because it\'s in the trash. To rename this item, first drag it out of the Trash.',
jpeg_image: 'JPEG image',
add_to_desktop: 'Add to Desktop',
keep_in_taskbar: 'Keep in Taskbar',
language: 'Language',
license: 'License',