Compare commits

...
Author SHA1 Message Date
Nariman Jelveh 2bbd8a417f Remove the desktop wallpaper
The desktop no longer shows a wallpaper. It shows the page's plain
background color: white, or #1e1e1e in dark mode. The sign-in dialogs
and account picker that opened over the wallpaper now open over that
color too.

This removes the default wallpaper image and its credit, its preload in
both page shells, the "Set as Desktop Background" item on images'
right-click menu, the GUI's set_desktop_background and
refresh_desktop_background, and the set_as_background i18n key.
Desktop icon labels were white with a shadow to read on the wallpaper;
they now use the same gray as labels inside windows, and a light gray
in dark mode.

The backend drops POST /set-desktop-bg, and /whoami no longer returns
desktop_bg_url, desktop_bg_color or desktop_bg_fit. The user table's
desktop_bg_* columns stay; dropping them needs a migration.
2026-11-11 17:33:53 -07:00
20 changed files with 44 additions and 334 deletions

No files matched your search

+21 -4
View File
@@ -261,6 +261,26 @@ describe('whoami extension — handleWhoami', () => {
expect(body).toHaveProperty('taskbar_items');
});
it('leaves out a desktop background saved on the account', async () => {
const user = await seedUser();
await server.stores.user.update(user.id as number, {
desktop_bg_url: 'https://cdn.test/wallpaper.png',
desktop_bg_color: '#000000',
desktop_bg_fit: 'cover',
});
const { res, captured } = makeRes();
await runWithContext(
{ actor: { user: { uuid: user.uuid, id: user.id as number } } },
() => handleWhoami(makeReq(), res),
);
const body = captured.body as Record<string, unknown>;
expect(body).not.toHaveProperty('desktop_bg_url');
expect(body).not.toHaveProperty('desktop_bg_color');
expect(body).not.toHaveProperty('desktop_bg_fit');
});
it('reports the SMS-to-card fallback only once a send has opened it', async () => {
const user = await server.stores.user.create({
username: `wuser_${Math.random().toString(36).slice(2, 8)}`,
@@ -322,7 +342,7 @@ describe('whoami extension — handleWhoami', () => {
expect(flags.payment_bypass).toBeUndefined();
});
it('strips desktop_bg_*, created_ts and human_readable_age fields for app actors', async () => {
it('strips created_ts and human_readable_age fields for app actors', async () => {
const user = await seedUser();
const { res, captured } = makeRes();
@@ -340,9 +360,6 @@ describe('whoami extension — handleWhoami', () => {
expect(body.app_name).toBe('app-test-actor');
// is_user_token is stripped for app actors.
expect(body.is_user_token).toBeUndefined();
expect(body.desktop_bg_url).toBeUndefined();
expect(body.desktop_bg_color).toBeUndefined();
expect(body.desktop_bg_fit).toBeUndefined();
expect(body.human_readable_age).toBeUndefined();
// Account age, in either form, is not exposed to apps.
expect(body.created_ts).toBeUndefined();
+1 -5
View File
@@ -81,9 +81,6 @@ export const handleWhoami = async (
delete details.email;
delete details.unconfirmed_email;
}
delete details.desktop_bg_url;
delete details.desktop_bg_color;
delete details.desktop_bg_fit;
delete details.human_readable_age;
delete details.created_ts;
delete details.is_user_token;
@@ -121,8 +118,7 @@ export const handleWhoami = async (
}
const subscription = details.subscription as
| { offering?: Record<string, unknown> }
| undefined;
{ offering?: Record<string, unknown> } | undefined;
if (subscription?.offering) {
delete subscription.offering.group;
delete subscription.offering.benefits;
@@ -29,14 +29,13 @@ const ALLOWED_SORT_ORDER = ['asc', 'desc'];
*
* Two categories:
*
* - User-level: desktop background, taskbar items (UserStore)
* - User-level: taskbar items (UserStore)
* - Folder-level: layout, sort_by/sort_order (fsentries table)
*/
/**
* Desktop preference writes — background, taskbar, layout, sort order. All four
* persist to the user row on every call, and the GUI fires them on direct user
* action, so a per-minute ceiling well above human speed is enough to catch a
* stuck client.
* Desktop preference writes — taskbar, layout, sort order. Each persists on
* every call, and the GUI fires them on direct user action, so a per-minute
* ceiling well above human speed is enough to catch a stuck client.
*/
const PREFERENCE_WRITE_LIMIT = {
scope: 'desktop-preference',
@@ -47,62 +46,6 @@ const PREFERENCE_WRITE_LIMIT = {
export class DesktopController extends PuterController {
registerRoutes(router) {
// -- Desktop background --------------------------------------
router.post(
'/set-desktop-bg',
{
subdomain: 'api',
requireUserActor: true,
allowFullAccessToken: true,
rateLimit: PREFERENCE_WRITE_LIMIT,
},
async (req, res) => {
const { url, color, fit } = req.body ?? {};
const patch = {};
if (url !== undefined) {
if (url !== null && typeof url !== 'string') {
throw new HttpError(
400,
'`url` must be a string or null',
{ legacyCode: 'bad_request' },
);
}
patch.desktop_bg_url = url;
}
if (color !== undefined) {
if (color !== null && typeof color !== 'string') {
throw new HttpError(
400,
'`color` must be a string or null',
{ legacyCode: 'bad_request' },
);
}
patch.desktop_bg_color = color;
}
if (fit !== undefined) {
if (fit !== null && typeof fit !== 'string') {
throw new HttpError(
400,
'`fit` must be a string or null',
{ legacyCode: 'bad_request' },
);
}
patch.desktop_bg_fit = fit;
}
if (Object.keys(patch).length === 0) {
throw new HttpError(400, 'No fields provided', {
legacyCode: 'bad_request',
});
}
await this.stores.user.update(req.actor.user.id, patch);
res.json({});
},
);
// -- Taskbar items -------------------------------------------
router.post(
@@ -130,102 +130,11 @@ const callRoute = async (
});
};
// ── /set-desktop-bg ─────────────────────────────────────────────────
describe('DesktopController POST /set-desktop-bg', () => {
it('persists url/color/fit on the user row', async () => {
const { actor, userId } = await makeUser();
const { res, captured } = makeRes();
await callRoute(
'post',
'/set-desktop-bg',
makeReq({
body: {
url: 'https://cdn.test/wallpaper.png',
color: '#000000',
fit: 'cover',
},
actor,
}),
res,
);
expect(captured.body).toEqual({});
const refreshed = await server.stores.user.getById(userId);
expect(refreshed?.desktop_bg_url).toBe(
'https://cdn.test/wallpaper.png',
);
expect(refreshed?.desktop_bg_color).toBe('#000000');
expect(refreshed?.desktop_bg_fit).toBe('cover');
});
it('persists only the supplied fields (partial update)', async () => {
const { actor, userId } = await makeUser();
const { res } = makeRes();
await callRoute(
'post',
'/set-desktop-bg',
makeReq({ body: { color: '#ffffff' }, actor }),
res,
);
const refreshed = await server.stores.user.getById(userId);
expect(refreshed?.desktop_bg_color).toBe('#ffffff');
// Untouched fields remain at their column default.
expect(refreshed?.desktop_bg_url).toBeFalsy();
expect(refreshed?.desktop_bg_fit).toBeFalsy();
});
it('passes through `null` to clear a field', async () => {
const { actor, userId } = await makeUser();
// First populate, then clear.
const populate = makeRes();
await callRoute(
'post',
'/set-desktop-bg',
makeReq({
body: { url: 'https://cdn.test/x.png' },
actor,
}),
populate.res,
);
const before = await server.stores.user.getById(userId);
expect(before?.desktop_bg_url).toBe('https://cdn.test/x.png');
const clear = makeRes();
await callRoute(
'post',
'/set-desktop-bg',
makeReq({ body: { url: null }, actor }),
clear.res,
);
const after = await server.stores.user.getById(userId);
expect(after?.desktop_bg_url).toBeNull();
});
it('throws 400 when url is not a string or null', async () => {
const { actor } = await makeUser();
const { res } = makeRes();
await expect(
callRoute(
'post',
'/set-desktop-bg',
makeReq({ body: { url: 123 }, actor }),
res,
),
).rejects.toMatchObject({ statusCode: 400 });
});
it('throws 400 when no fields are provided', async () => {
const { actor } = await makeUser();
const { res } = makeRes();
await expect(
callRoute(
'post',
'/set-desktop-bg',
makeReq({ body: {}, actor }),
res,
),
).rejects.toMatchObject({ statusCode: 400 });
describe('DesktopController routes', () => {
it('has no desktop background route', () => {
expect(
router.routes.some((r) => r.path === '/set-desktop-bg'),
).toBe(false);
});
});
+1 -1
View File
@@ -2864,7 +2864,7 @@ export class AuthService extends PuterService {
#actorUserFromRow(user: UserRow) {
// Strip the password hash; pass everything else through so callers
// can read metadata, desktop_bg_*, otp_enabled, etc. without
// can read metadata, otp_enabled, etc. without
// re-fetching the user. Mirrors what /whoami exposes off of UserRow.
const { password: _password, ...rest } = user;
return {
@@ -517,8 +517,6 @@ export class PuterHomepageService extends PuterService {
<meta name="theme-color" content="#ffffff">
${bundled ? `<link rel="stylesheet" href="${guiCss}">` : ''}
<link rel="preload" as="image" href="https://puter-assets.b-cdn.net/wallpaper.webp">
<script>
if ( ! window.service_script ) {
window.service_script_api_promise = (() => {
-3
View File
@@ -170,9 +170,6 @@ export async function buildUserDetails(
cardFallbackDepsFrom(clients),
)
: false,
desktop_bg_url: user.desktop_bg_url,
desktop_bg_color: user.desktop_bg_color,
desktop_bg_fit: user.desktop_bg_fit,
is_temp: user.password === null && user.email === null,
is_user_token: true,
// Null until the account asks for a code; never minted here.
-2
View File
@@ -1,7 +1,5 @@
## Credits
The default wallpaper is created by [Milad Fakurian](https://unsplash.com/photos/blue-orange-and-yellow-wallpaper-E8Ufcyxz514) and published on [Unsplash](https://unsplash.com/).
Icons by [Papirus](https://github.com/PapirusDevelopmentTeam/papirus-icon-theme) under GPL-3.0 license.
Icons by [Iconoir](https://iconoir.com/) under MIT license.
-39
View File
@@ -1651,42 +1651,6 @@ $(document).on('contextmenu taphold', '.taskbar', function (event) {
return false;
});
window.set_desktop_background = function (options) {
if ( options.fit ) {
let fit = options.fit;
if ( fit === 'cover' || fit === 'contain' ) {
$('body').css('background-size', fit);
$('body').css('background-repeat', 'no-repeat');
$('body').css('background-position', 'center center');
}
else if ( fit === 'center' ) {
$('body').css('background-size', 'auto');
$('body').css('background-repeat', 'no-repeat');
$('body').css('background-position', 'center center');
}
else if ( fit === 'repeat' ) {
$('body').css('background-size', 'auto');
$('body').css('background-repeat', 'repeat');
}
window.desktop_bg_fit = fit;
}
if ( options.url ) {
$('body').css('background-image', `url(${options.url})`);
window.desktop_bg_url = options.url;
window.desktop_bg_color = undefined;
}
else if ( options.color ) {
$('body').css({
'background-image': 'none',
'background-color': options.color,
});
window.desktop_bg_color = options.color;
window.desktop_bg_url = undefined;
}
};
window.update_taskbar = function () {
let items = [];
$('.taskbar-item-sortable[data-keep-in-taskbar="true"]').each(function (index) {
@@ -1760,9 +1724,6 @@ window.exit_fullpage_mode = (el_window) => {
// reset dektop height to take into account the taskbar height
$('.desktop').css('height', `calc(100vh - ${window.taskbar_height + window.toolbar_height}px)`);
// refresh desktop background
window.refresh_desktop_background();
};
window.reset_window_size_and_position = (el_window) => {
-43
View File
@@ -26,7 +26,6 @@ import truncate_filename from '../helpers/truncateFilename.js';
import launch_app from '../helpers/launchApp.js';
import open_item from '../helpers/openItem.js';
import publish_as_website from '../helpers/publishAsWebsite.js';
import mime from '../lib/mime.js';
import { isWeblinkName, weblinkChangeIconMenuItem } from '../helpers/weblink.js';
import { is_owned_by_me } from '../helpers/pathOwner.js';
import { can_rename, can_restructure, invalidate_shared_roots } from '../helpers/sharedAccess.js';
@@ -1311,48 +1310,6 @@ async function UIItem (options) {
});
}
// -------------------------------------------
// Set as Wallpaper
// -------------------------------------------
const mime_type = mime.getType($(el_item).attr('data-name')) ??
'application/octet-stream';
if ( !is_trashed && !is_trash && !options.is_dir && mime_type.startsWith('image/') ) {
menu_items.push({
html: i18n('set_as_background'),
onClick: async function () {
const read_url = await puter.fs.sign(undefined, { uid: $(el_item).attr('data-uid'), action: 'read' });
window.set_desktop_background({
url: read_url.items.read_url,
fit: window.desktop_bg_fit,
});
try {
$.ajax({
url: `${window.api_origin }/set-desktop-bg`,
type: 'POST',
data: JSON.stringify({
url: window.desktop_bg_url,
color: window.desktop_bg_color,
fit: window.desktop_bg_fit,
}),
async: true,
contentType: 'application/json',
headers: {
'Authorization': `Bearer ${window.auth_token}`,
},
statusCode: {
401: function (xhr) {
window.handle401(xhr);
},
},
});
$(el_window).close();
resolve(true);
} catch ( err ) {
// Ignore
}
},
});
}
// -------------------------------------------
// Zip
// -------------------------------------------
if ( !is_trash && !is_trashed && !$(el_item).attr('data-path').endsWith('.zip') ) {
+12 -3
View File
@@ -122,9 +122,6 @@ html, body {
}
body {
background: no-repeat center center fixed;
background-position: center;
background-size: cover;
background-color: #ffffff;
overflow: hidden;
}
@@ -911,6 +908,18 @@ span.header-sort-icon img {
margin-left: 3px;
}
/* The desktop is a plain page color, so its labels need text that reads on it. */
.desktop .item .item-name {
color: rgb(73, 73, 73);
text-shadow: none;
}
@media (prefers-color-scheme: dark) {
.desktop .item .item-name {
color: #e6e6e6;
}
}
.device-phone .item-container-list .item .item-name {
line-height: 42px;
border-bottom: 1px solid #e3e3e3;
-31
View File
@@ -2189,37 +2189,6 @@ window.move_items = async function (el_items, dest_path, is_undo = false) {
}
};
/**
* Refreshes the desktop background based on the user's settings.
* If the user has set a custom desktop background URL or color, it will use that.
* If not, it defaults to a specific wallpaper image.
*
* @global
* @function
* @fires set_desktop_background - Calls this global function to set the desktop background.
*
* @example
* // This will refresh the desktop background according to the user's preference or defaults.
* window.refresh_desktop_background();
*/
window.refresh_desktop_background = function () {
if ( window.user && (window.user.desktop_bg_url !== null || window.user.desktop_bg_color !== null) ) {
window.set_desktop_background({
url: window.user.desktop_bg_url,
fit: window.user.desktop_bg_fit,
color: window.user.desktop_bg_color,
});
}
// default background
else {
let wallpaper = (window.gui_env === 'prod') ? 'https://puter-assets.b-cdn.net/wallpaper.webp' : '/src/images/wallpaper.webp';
window.set_desktop_background({
url: wallpaper,
fit: 'cover',
});
}
};
window.determine_website_url = function (fsentry_path) {
// The address an item has under a site serving a folder above it. The
// site's folder and address come with its root_dir.
-3
View File
@@ -480,9 +480,6 @@ const bg = {
window_title_pictures: 'Снимки',
window_title_puter: 'Puter',
window_folder_empty: 'Тази папка е празна',
// Website Management
set_as_background: 'Задай като фон на работния плот',
},
};
-1
View File
@@ -601,7 +601,6 @@ const cs = {
"window_title_puter": "Puter",
"item_associated_websites_plural": "Přidružené webové stránky",
"window_click_to_go_forward": "Kliknutím přejdete vpřed.",
"set_as_background": "Nastavit jako pozadí plochy",
"perm_folder_pictures": "vaší složce Obrázky",
"perm_folder_documents": "vaší složce Dokumenty",
"perm_folder_videos": "vaší složce Videa",
-5
View File
@@ -1017,11 +1017,6 @@ const en = {
'window_title_puter': 'Puter',
'window_folder_empty': 'This folder is empty',
// Website Management
'set_as_background': 'Set as Desktop Background',
// Permission Descriptions
'perm_fs_see': 'see that {{name}} exists in {{path}}',
'perm_fs_dir_see': 'see that the folder {{name}} in {{path}} exists, and whether anything exists in it or its subfolders',
-4
View File
@@ -463,10 +463,6 @@ const fr = {
'window_title_puter': 'Puter',
'window_folder_empty': 'Ce dossier est vide',
// Website Management
'set_as_background': 'Définir comme arrière-plan',
// Permission Descriptions
'perm_email_read': 'voir votre adresse e-mail',
'perm_folder_desktop': 'votre dossier Bureau',
-1
View File
@@ -446,7 +446,6 @@ const zhtw = {
'tar': "Tar 壓縮", // In English: "Tar"
'download_as_tar': "下載為 Tar", // In English: "Download as Tar"
'tarring': "正在建立 Tar 壓縮檔 %strong%", // In English: "Tarring %strong%"
'set_as_background': "設為桌面背景", // In English: "Set as Desktop Background"
'error_user_or_path_not_found': "找不到使用者或路徑。", // In English: "User or path not found."
'error_invalid_username': "無效的使用者名稱。", // In English: "Invalid username."
},
Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

-27
View File
@@ -2035,18 +2035,6 @@ window.initgui = async function (options) {
}
}
//--------------------------------------------------------------------------------------
// Desktop background (early)
// Set before action=login/signup so the signed-in dialog has the desktop behind it.
// Skipped for the full-page form, which it would only flash behind before covering.
// -------------------------------------------------------------------------------------
const authActionIsPage =
action === 'password-recovery' ||
(!window.is_auth() && (action === 'login' || action === 'signup'));
if (!window.is_fullpage_mode && !window.embedded_in_popup && !authActionIsPage) {
window.refresh_desktop_background();
}
//--------------------------------------------------------------------------------------
// Action: Password recovery
//--------------------------------------------------------------------------------------
@@ -2630,15 +2618,6 @@ window.initgui = async function (options) {
!window.is_auth() &&
(!window.first_visit_ever || window.disable_temp_users || is_app_landing_page || is_share_link);
// -------------------------------------------------------------------------------------
// Desktop Background
// If we're in fullpage/emebedded/Auth Popup mode, we don't want to load the custom background
// because it's not visible anyway and it's a waste of bandwidth. Same behind the sign-in page.
// -------------------------------------------------------------------------------------
const showsSignInPage = needsSignIn && window.logged_in_users.length === 0;
if (!window.is_fullpage_mode && !window.embedded_in_popup && !showsSignInPage) {
window.refresh_desktop_background();
}
// -------------------------------------------------------------------------------------
// Un-authed but not first visit -> try to log in/sign up
// -------------------------------------------------------------------------------------
@@ -2692,12 +2671,6 @@ window.initgui = async function (options) {
const needs_action = action === 'authme' || action === 'copyauth';
const reload_on_success = needs_action;
if (window.logged_in_users.length > 0) {
// dashboard mode skips the wallpaper (fullpage), but the account
// picker is a dialog, not a page — restore the wallpaper so it
// isn't on a blank page
if (window.is_dashboard_mode) {
window.refresh_desktop_background();
}
await UIAuthAccountPicker({
redirect_url: needs_action ? window.location.href : undefined,
});
-3
View File
@@ -295,9 +295,6 @@ function generateDevHtml (options) {
<meta name="msapplication-TileImage" content="/favicons/ms-icon-144x144.png">
<meta name="theme-color" content="#ffffff">`;
// preload images when applicable
h += '<link rel="preload" as="image" href="./images/wallpaper.webp">';
// service-script hook: initgui resolves this promise during boot, so the
// shim must exist before the bundle runs (same shim the backend injects).
h += `