diff --git a/package-lock.json b/package-lock.json index 6f045c2cc..d625481b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" }, diff --git a/src/gui/src/UI/UIItem.js b/src/gui/src/UI/UIItem.js index e644edd09..aa08e10c4 100644 --- a/src/gui/src/UI/UIItem.js +++ b/src/gui/src/UI/UIItem.js @@ -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)); - } + }, }); } diff --git a/src/gui/src/UI/UITaskbar.js b/src/gui/src/UI/UITaskbar.js index 021c60870..3d1ac378e 100644 --- a/src/gui/src/UI/UITaskbar.js +++ b/src/gui/src/UI/UITaskbar.js @@ -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'), diff --git a/src/gui/src/helpers/item_icon.js b/src/gui/src/helpers/item_icon.js index cc062437b..d36d8b76a 100644 --- a/src/gui/src/helpers/item_icon.js +++ b/src/gui/src/helpers/item_icon.js @@ -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 ) { diff --git a/src/gui/src/helpers/open_item.js b/src/gui/src/helpers/open_item.js index ca884084f..8e3612d0c 100644 --- a/src/gui/src/helpers/open_item.js +++ b/src/gui/src/helpers/open_item.js @@ -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? //---------------------------------------------------------------- diff --git a/src/gui/src/i18n/translations/en.js b/src/gui/src/i18n/translations/en.js index e5c8f30ec..f29c93f15 100644 --- a/src/gui/src/i18n/translations/en.js +++ b/src/gui/src/i18n/translations/en.js @@ -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',