Files
puter/src/backend/util/taskbarItems.ts
T
Daniel Salazar 799fc4ac3c feat: send app icons as a subdomain URL plus an API fallback (#3734)
App payloads carried only the /app-icon endpoint URL, which 302s to the
icons hosting subdomain. Networks that mangle that redirect render no icon
at all, and every icon load pays a round trip for the hop.

Ship the direct subdomain URL as `iconCdnUrl` alongside it (taskbar items,
installedApps, recent/recommended launch apps, suggested apps), and have the
GUI load that first with the endpoint URL as a one-shot retry - desktop
taskbar, start menu, dashboard app grid and recents. Only rows whose `icon`
column is already an http(s) URL get one: a data: column means the resize
pipeline has not written anything to the subdomain yet.

Also folds the four copies of the generated-size list into one exported
APP_ICON_SIZES.
2026-09-03 13:32:55 -07:00

133 lines
4.1 KiB
TypeScript

/*
* Copyright (C) 2024-present Puter Technologies Inc.
*
* This file is part of Puter.
*
* Puter is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import type { AppIconHostConfig } from './appIcon.js';
import { getAppIconCdnUrl, getAppIconUrl } from './appIcon.js';
interface TaskbarEntry {
name?: string;
id?: number;
uid?: string;
type?: string;
}
interface TaskbarOptions {
iconSize?: number;
noIcons?: boolean;
}
interface TaskbarDeps {
apiBaseUrl?: string;
config?: AppIconHostConfig;
clients: {
db: {
write: (query: string, params?: unknown[]) => Promise<unknown>;
};
};
stores: {
app: {
getByName: (
name: string,
) => Promise<Record<string, unknown> | null>;
getByUid: (uid: string) => Promise<Record<string, unknown> | null>;
getById: (id: number) => Promise<Record<string, unknown> | null>;
};
user: {
invalidateById: (id: number) => Promise<unknown>;
};
};
}
const DEFAULT_TASKBAR_ITEMS: TaskbarEntry[] = [
{ name: 'app-center', type: 'app' },
{ name: 'builder', type: 'app' },
{ name: 'dev-center', type: 'app' },
{ name: 'editor', type: 'app' },
{ name: 'code', type: 'app' },
{ name: 'camera', type: 'app' },
{ name: 'recorder', type: 'app' },
{ name: 'browser', type: 'app' },
];
export async function getTaskbarItems(
user: Record<string, unknown>,
deps: TaskbarDeps,
options: TaskbarOptions = {},
): Promise<Array<Record<string, unknown>>> {
let raw: TaskbarEntry[];
if (!user.taskbar_items) {
raw = DEFAULT_TASKBAR_ITEMS;
await deps.clients.db.write(
'UPDATE `user` SET `taskbar_items` = ? WHERE `id` = ?',
[JSON.stringify(raw), user.id],
);
await deps.stores.user.invalidateById(user.id as number);
} else {
try {
raw =
typeof user.taskbar_items === 'string'
? JSON.parse(user.taskbar_items as string)
: (user.taskbar_items as TaskbarEntry[]);
} catch {
raw = [];
}
}
const items: Array<Record<string, unknown>> = [];
for (const entry of raw) {
if (entry.type !== 'app') continue;
if (entry.name === 'explorer') continue;
let app: Record<string, unknown> | null = null;
if (entry.name) app = await deps.stores.app.getByName(entry.name);
else if (entry.uid) app = await deps.stores.app.getByUid(entry.uid);
else if (entry.id) app = await deps.stores.app.getById(entry.id);
if (!app) continue;
const item: Record<string, unknown> = {
uid: app.uid,
uuid: app.uid,
name: app.name,
title: app.title,
icon: app.icon ?? null,
godmode: Boolean(app.godmode),
maximize_on_start: Boolean(app.maximize_on_start),
index_url: app.index_url,
description: app.description,
};
if (options.noIcons) {
delete item.icon;
} else {
item.icon =
getAppIconUrl(app, deps, options.iconSize) ?? app.icon ?? null;
// Direct subdomain URL for the client to try before `icon`.
item.iconCdnUrl = deps.config
? getAppIconCdnUrl(app, deps.config, options.iconSize)
: null;
}
items.push(item);
}
return items;
}