Compare commits

...
Author SHA1 Message Date
Nariman Jelveh f87afdea5a Stop ?puter.fullpage= from booting the desktop
The root and /app/<name> with ?puter.fullpage= now open in the dashboard
like a plain /app/<name>, with the app maximized. The dashboard always
shows apps full page, so the parameter no longer overrides that. Iframe
embeds and popups, which still boot the desktop, read it as before.

bootsDashboard no longer takes the query string, since no parameter
decides the interface anymore.
2026-11-12 11:38:54 -07:00
Nariman Jelveh 2e74e865f1 Remove the desktop's ?app=explorer link handler
?app=explorer[&path=<folder>] no longer boots the desktop and opens an
Explorer window. ?app= is ignored and no longer forces the desktop, so a
root URL that carries it opens the dashboard, and /app/<name>?app=... now
launches its app in the dashboard instead of booting the desktop and
launching nothing.
2026-11-12 11:36:13 -07:00
Nariman Jelveh 0475f41440 Redirect /desktop/app links and stop opening share links on the desktop
/desktop/app/<name> now redirects to /app/<name>, keeping its query
string, so the app opens in the dashboard. The backend no longer serves
an app shell there, OIDC sign-in no longer returns to it, and the GUI no
longer strips a leading /desktop from its route segments. The rate-limits
page lists only /app/<name>, the route the limit still applies to.

/desktop?shared=<path> no longer opens the shared item on the desktop.
The desktop ignores ?shared=: it doesn't ask a guest to sign in for it,
doesn't switch to the account a share email names, and OIDC sign-in from
/desktop doesn't carry it along. Share emails link to /?shared=, which
the dashboard handles as before. Opening a share notification on the
desktop still opens the item.
2026-11-12 11:34:20 -07:00
Nariman Jelveh 3650813e3c Redirect /@username links to the dashboard
puter.com/@<username> no longer opens that user's Public folder on the
desktop. The backend now redirects /@:username to /, so old links land
on the dashboard instead of booting the desktop.

This removes the GUI's /@ handler and the error_invalid_username i18n
key only it used. error_user_or_path_not_found stays, since share links
still use it.
2026-11-12 11:26:11 -07:00
Nariman Jelveh 5bc75ea4b2 Stop granting Camera and Recorder Desktop access on desktop boot
The desktop no longer grants Camera and Recorder write access to the
user's Desktop folder the first time it loads. Those apps now ask
through the normal permission prompt, as they already did for users
who only used the dashboard. Grants already given stay in place.

puter.js's GUI boot cache no longer batch-fetches the
has_set_default_app_user_permissions flag that kept the grant to one
run. Values already saved under it stay in KV, unread.
2026-11-12 11:23:31 -07:00
Nariman Jelveh 9416f29c5c Remove the desktop's ?download= link handler
Opening puter.com/?download=<url> or /desktop?download=<url> no longer
asks to save that remote file to the Desktop. The parameter is ignored,
and a root URL that carries it boots the dashboard like any other root
URL instead of forcing the desktop.

This removes the handler in UIDesktop, its UIWindowProgress import, the
download entry in the GUI's dashboard routing, and the
confirm_download_file_to_desktop, downloading, downloading_file,
uploading_file and error_download_failed i18n keys only it used.

The decision of whether a page load boots the dashboard moves from an
inline block in initgui into helpers/bootsDashboard.js, so it is tested.
2026-11-12 11:21:17 -07:00
Nariman Jelveh bfef42236a Remove the desktop's Hide Desktop Icons toggle
The desktop's right-click menu no longer has a Hide/Show Desktop Icons
item. Without the wallpaper, hiding the icons left an empty page. This
removes the menu item, toggleDesktopIcons/hideDesktopIcons/
showDesktopIcons, the boot-time read of the saved setting, and the
.desktop-icons-hidden CSS rule.

puter.js's GUI boot cache no longer batch-fetches desktop_icons_hidden,
menubar_style or user_preferences.clock_visible, which nothing reads
anymore. Values already saved under those keys stay in KV, unread.

Also drops the desktop's BroadcastChannel, whose message handler was
empty and which nothing else referenced.
2026-11-12 11:17:17 -07:00
Nariman Jelveh 0bc4680725 Remove the desktop's Publish Website window
The right-click menu on desktop folders and on an Explorer window's
background no longer has a Publish as Website item. This removes
UIWindowPublishWebsite, the publishAsWebsite helper that opened it, both
menu items, the window's CSS, window.extractProtocol (only the window used
it), and the dir_published_as_website and pick_name_for_website i18n keys.

The dashboard keeps publishing websites through its own dialog.
generateFileContextMenu no longer falls back to the removed window, so it
offers Publish as Website only when the caller passes onPublishWebsite,
which the dashboard's Files tab does. The website badge on desktop items
and the backend hosting API are unchanged.
2026-11-12 11:12:54 -07:00
72 changed files with 289 additions and 912 deletions

No files matched your search

@@ -79,6 +79,7 @@ interface CapturedResponse {
statusCode: number;
body: unknown;
contentType?: string;
location?: string;
}
const makeReq = (init: {
@@ -87,6 +88,7 @@ const makeReq = (init: {
actor?: Actor;
hostname?: string;
protocol?: string;
originalUrl?: string;
}): Request => {
return {
body: {},
@@ -94,6 +96,7 @@ const makeReq = (init: {
headers: {},
params: init.params ?? {},
path: init.path ?? '/',
originalUrl: init.originalUrl ?? init.path ?? '/',
hostname: init.hostname ?? 'test.local',
protocol: init.protocol ?? 'http',
actor: init.actor,
@@ -131,6 +134,11 @@ const makeRes = () => {
captured.contentType = value;
return res;
}),
redirect: vi.fn((location: string) => {
captured.statusCode = 302;
captured.location = location;
return res;
}),
};
return { res: res as unknown as Response, captured };
};
@@ -214,7 +222,7 @@ describe('HomepageController shell routes', () => {
expect(String(captured.body)).toMatch(/<!DOCTYPE html>/i);
});
it('serves the shell on /settings, /dashboard, /desktop, /action, /@:username', async () => {
it('serves the shell on /settings, /dashboard, /desktop, /action', async () => {
for (const path of [
'/settings',
'/settings/*splat',
@@ -223,7 +231,6 @@ describe('HomepageController shell routes', () => {
'/desktop',
'/desktop/',
'/action/*splat',
'/@:username',
]) {
const { res, captured } = makeRes();
await callRoute('get', path, makeReq({ path }), res);
@@ -231,6 +238,19 @@ describe('HomepageController shell routes', () => {
expect(String(captured.body)).toMatch(/<!DOCTYPE html>/i);
}
});
it('redirects /@:username to the dashboard', async () => {
const { res, captured } = makeRes();
await callRoute(
'get',
'/@:username',
makeReq({ path: '/@alice', params: { username: 'alice' } }),
res,
);
expect(captured.statusCode).toBe(302);
expect(captured.location).toBe('/');
expect(captured.body).toBeUndefined();
});
});
// ── /app/:name ──────────────────────────────────────────────────────
@@ -360,47 +380,21 @@ describe('HomepageController GET /app/:name', () => {
expect(html).not.toContain('owner_user_id');
});
it('serves the same app shell under /desktop/app/:name', async () => {
const { userId } = await makeUser();
const name = `desk-${Math.random().toString(36).slice(2, 10)}`;
await server.stores.app.create(
{
name,
title: 'Desktop App',
description: 'opens on the desktop instead of the dashboard',
index_url: `https://example.com/${name}/`,
approved_for_listing: 1,
},
{ ownerUserId: userId },
);
const { res, captured } = makeRes();
await callRoute(
'get',
'/desktop/app/:name',
makeReq({ params: { name }, path: `/desktop/app/${name}` }),
res,
);
expect(captured.statusCode).toBe(200);
const html = String(captured.body);
expect(html).toMatch(/<!DOCTYPE html>/i);
expect(html).toContain('Desktop App');
});
it('returns 404 under /desktop/app/:name when the app is unknown', async () => {
it('redirects /desktop/app/:name to /app/:name, keeping the query', async () => {
const { res, captured } = makeRes();
await callRoute(
'get',
'/desktop/app/:name',
makeReq({
params: { name: 'no-such-app' },
path: '/desktop/app/no-such-app',
params: { name: 'editor' },
path: '/desktop/app/editor',
originalUrl: '/desktop/app/editor?file=%2Falice%2Fa.txt&x=1',
}),
res,
);
expect(captured.statusCode).toBe(404);
expect(String(captured.body)).toMatch(/<!DOCTYPE html>/i);
expect(captured.statusCode).toBe(302);
expect(captured.location).toBe('/app/editor?file=%2Falice%2Fa.txt&x=1');
expect(captured.body).toBeUndefined();
});
it('omits index_url even for a public app — the shell is not the launch authority', async () => {
@@ -434,19 +428,17 @@ describe('HomepageController GET /app/:name', () => {
// -- app landing rate limit --
describe('HomepageController app landing rate limit', () => {
it('rate-limits /app/:name and /desktop/app/:name per network', () => {
for (const path of ['/app/:name', '/desktop/app/:name']) {
const route = router.routes.find(
(r) => r.method === 'get' && r.path === path,
);
if (!route) throw new Error(`No GET ${path} route`);
expect(route.options.rateLimit).toEqual({
scope: 'app-landing',
limit: 600,
window: 60_000,
key: 'ip',
});
}
it('rate-limits /app/:name per network', () => {
const route = router.routes.find(
(r) => r.method === 'get' && r.path === '/app/:name',
);
if (!route) throw new Error('No GET /app/:name route');
expect(route.options.rateLimit).toEqual({
scope: 'app-landing',
limit: 600,
window: 60_000,
key: 'ip',
});
});
});
@@ -274,12 +274,19 @@ export class HomepageController extends PuterController {
router.get('/action/*splat', {}, (req, res) => sendShell(req, res));
router.get('/@:username', {}, (req, res) => sendShell(req, res));
// Old links to a user's public folder land on the dashboard.
router.get('/@:username', {}, (_req, res) => res.redirect('/'));
// Old desktop app links open the app in the dashboard, query intact.
router.get('/desktop/app/:name', {}, (req, res) => {
const separator = req.originalUrl.indexOf('?');
const query =
separator === -1 ? '' : req.originalUrl.slice(separator);
const name = encodeURIComponent(String(req.params.name ?? ''));
res.redirect(`/app/${name}${query}`);
});
// -- /app/:name - app metadata baked into the shell ----------
// `/desktop/app/:name` is the same landing booted on the desktop
// instead of the dashboard; the GUI strips the prefix and treats the
// rest of the path identically, so both render the same shell.
const sendAppShell = async (
req: express.Request,
@@ -334,11 +341,6 @@ export class HomepageController extends PuterController {
{ rateLimit: APP_LANDING_LIMIT },
sendAppShell,
);
router.get(
'/desktop/app/:name',
{ rateLimit: APP_LANDING_LIMIT },
sendAppShell,
);
// -- /show/* - launch explorer with the requested file path --
@@ -372,26 +372,6 @@ describe('OIDCController GET /auth/oidc/:provider/start', () => {
);
});
it('bakes a whitelisted desktop app landing (/desktop/app/<name>) into the state redirect_uri', async () => {
const { res, captured } = makeRes();
await callRoute(
'get',
'/auth/oidc/:provider/start',
makeReq({
params: { provider: 'custom' },
query: { return_to: '/desktop/app/my-App_2' },
}),
res,
);
const state = new URL(captured.redirectUrl ?? '').searchParams.get(
'state',
);
const decoded = oidc().verifyState(state!);
expect(String(decoded?.redirect_uri)).toBe(
`${TEST_ORIGIN}/desktop/app/my-App_2`,
);
});
it('ignores a non-whitelisted return_to', async () => {
const bad_values = [
'/app/evil/extra',
@@ -400,6 +380,7 @@ describe('OIDCController GET /auth/oidc/:provider/start', () => {
'//evil.test',
'/settings',
`/app/${'a'.repeat(101)}`,
'/desktop/app/my-App_2',
'/desktop/app/evil/extra',
'/desktop/app/',
'/dashboard/app/name',
@@ -102,16 +102,15 @@ function signupOptionsFromState(
}
// GUI pages an OIDC flow may return to: the root (where a share email lands),
// /desktop, /dashboard, and direct app landings (/app/<name> and its
// desktop-booted twin /desktop/app/<name>, mirroring APP_NAME_REGEX in
// AppDriver). Strict whitelist — never a client-supplied URL (no open
// redirect).
// /desktop, /dashboard, and direct app landings (/app/<name>, mirroring
// APP_NAME_REGEX in AppDriver). Strict whitelist — never a client-supplied URL
// (no open redirect).
function isWhitelistedReturnPath(path: string): boolean {
return (
path === '/' ||
path === '/desktop' ||
path === '/dashboard' ||
/^(\/desktop)?\/app\/[a-zA-Z0-9_-]{1,100}$/.test(path)
/^\/app\/[a-zA-Z0-9_-]{1,100}$/.test(path)
);
}
+1 -1
View File
@@ -418,7 +418,7 @@ For long-lived mounts, sign in with a `-token` username and an API token as the
| ------------------------------------------------------------------ | ------------------------------- | ----------- |
| `nameAvailable` | 60/min | |
| App icon | 12,000/min | Per network |
| App landing page (`/app/<name>`, `/desktop/app/<name>`) | 600/min | Per network |
| App landing page (`/app/<name>`) | 600/min | Per network |
| Names per batch app lookup (`GET /apps/:names`, `POST /query/app`) | 200, up to 200 characters each | |
| `metadata` | 16 KiB, measured as JSON | |
| `filetypeAssociations` | 200 entries, 60 characters each | |
+1 -1
View File
@@ -144,7 +144,7 @@ try {
startServer(1);
app.get(['/', '/app/*splat', '/action/*splat', '/desktop', '/desktop/app/*splat', '/dashboard'], (req, res) => {
app.get(['/', '/app/*splat', '/action/*splat', '/desktop', '/dashboard'], (req, res) => {
res.send(generateDevHtml({
env: env,
api_origin: apiOrigin,
+1 -312
View File
@@ -24,9 +24,7 @@ import UIItem from './UIItem.js';
import UIAlert from './UIAlert.js';
import UIWindow from './UIWindow.js';
import UIWindowSaveAccount from './UIWindowSaveAccount.js';
import UIAuthLogin from './Auth/UIAuthLogin.js';
import UIWindowProgress from './UIWindowProgress.js';
import new_context_menu_item from '../helpers/newContextMenuItem.js';
import refresh_item_container from '../helpers/refreshItemContainer.js';
import { atPath, retargetNavHistory, retargetPaths, underPath } from '../helpers/pathElements.js';
@@ -35,7 +33,6 @@ import UINotification from './UINotification.js';
import launch_app from '../helpers/launchApp.js';
import { urlFileLaunchOptions } from '../helpers/confirmUrlFileAccess.js';
import item_icon from '../helpers/itemIcon.js';
import { SHARED_PATH_PARAM, clear_shared_param } from '../helpers/parseSharedPath.js';
import resolve_shared_item from '../helpers/resolveSharedItem.js';
import {
answerNotificationReceipts,
@@ -72,58 +69,6 @@ async function UIDesktop (options) {
let h = '';
// Set up the desktop channel for communication between different tabs in the same browser
window.channel = new BroadcastChannel('puter-desktop-channel');
channel.onmessage = function (e) {
};
// Initialize desktop icons visibility preference - move this earlier in the initialization
// Add this near the very beginning of the UIDesktop function
window.desktop_icons_hidden = false; // Set default value immediately
// Give Camera and Recorder write permissions to Desktop
puter.kv.get('has_set_default_app_user_permissions').then(async (user_permissions) => {
if ( ! user_permissions ) {
// Camera
try {
await fetch(`${window.api_origin }/auth/grant-user-app`, {
'headers': {
'Content-Type': 'application/json',
'Authorization': `Bearer ${ window.auth_token}`,
},
'body': JSON.stringify({
app_uid: 'app-5584fbf7-ed69-41fc-99cd-85da21b1ef51',
permission: `fs:${html_encode(window.desktop_path)}:write`,
}),
'method': 'POST',
});
} catch ( err ) {
console.error(err);
}
// Recorder
try {
await fetch(`${window.api_origin }/auth/grant-user-app`, {
'headers': {
'Content-Type': 'application/json',
'Authorization': `Bearer ${ window.auth_token}`,
},
'body': JSON.stringify({
app_uid: 'app-7bdca1a4-6373-4c98-ad97-03ff2d608ca1',
permission: `fs:${html_encode(window.desktop_path)}:write`,
}),
'method': 'POST',
});
} catch ( err ) {
console.error(err);
}
// Set flag to true
await puter.kv.set('has_set_default_app_user_permissions', true);
}
// Awaited above so a failed write reaches this: it leaves the flag unset
// and the whole thing is retried next boot, which is what we want.
}).catch(err => console.warn('Could not apply default app permissions:', err));
// connect socket.
window.socket = io(`${window.gui_origin }/`, {
auth: {
@@ -758,16 +703,6 @@ async function UIDesktop (options) {
// Append to <body>
$('body').append(h);
// Initialize the preference early
puter.kv.get('desktop_icons_hidden').then(async (val) => {
window.desktop_icons_hidden = (val === 'true' || val === true);
// Apply the setting immediately if needed
if ( window.desktop_icons_hidden ) {
hideDesktopIcons();
}
}).catch(err => console.warn('Could not load desktop_icons_hidden:', err));
const el_desktop = document.querySelector('.desktop');
window.active_element = el_desktop;
@@ -1000,15 +935,6 @@ async function UIDesktop (options) {
},
},
// -------------------------------------------
// Hide Desktop Icons
// -------------------------------------------
{
html: window.desktop_icons_hidden ? i18n('Show desktop icons') : i18n('Hide desktop icons'),
onClick: function () {
toggleDesktopIcons();
},
},
// -------------------------------------------
// -
// -------------------------------------------
'-',
@@ -1207,34 +1133,9 @@ async function UIDesktop (options) {
window.open('/dashboard/#home', '_blank');
}
// ---------------------------------------------
// Run apps from insta-login URL
// ---------------------------------------------
if ( window.url_query_params.has('app') ) {
let url_app_name = window.url_query_params.get('app');
if ( url_app_name === 'explorer' ) {
let predefined_path = window.home_path;
if ( window.url_query_params.has('path') )
{
predefined_path = window.url_query_params.get('path');
}
// launch explorer
UIWindow({
path: predefined_path,
title: path.basename(predefined_path),
icon: await item_icon({ is_dir: true, path: predefined_path }),
// todo
// uid: $(el_item).attr('data-uid'),
is_dir: true,
// todo
// sort_by: $(el_item).attr('data-sort_by'),
app: 'explorer',
});
}
}
// ---------------------------------------------
// load from direct app URLs: /app/app-name
// ---------------------------------------------
else if ( window.app_launched_from_url ) {
if ( window.app_launched_from_url ) {
if ( ! window.url_query_params.has('c') ) {
let posargs = undefined;
if ( window.app_query_params && window.app_query_params.posargs ) {
@@ -1283,43 +1184,6 @@ async function UIDesktop (options) {
$('.window-menubar-global').hide();
});
//--------------------------------------------------------------------------------------
// Trying to view a user's public folder?
// i.e. https://puter.com/@<username>
//--------------------------------------------------------------------------------------
const url_paths = window.location.pathname.split('/').filter(element => element);
if ( window.url_paths[0]?.startsWith('@') ) {
const username = window.url_paths[0].substring(1);
let item_path = `/${ username }/Public`;
if ( window.url_paths.length > 1 ) {
item_path += `/${ window.url_paths.slice(1).join('/')}`;
}
// GUARD: avoid invalid user directories
{
if ( ! username.match(/^[a-z0-9_]+$/i) ) {
UIAlert({
message: i18n('error_invalid_username'),
});
return;
}
}
let stat;
try {
stat = await puter.fs.stat({ path: item_path, consistency: 'eventual' });
} catch ( e ) {
window.history.replaceState(null, document.title, '/desktop');
UIAlert({
message: i18n('error_user_or_path_not_found'),
type: 'error',
});
return;
}
await open_path_target(item_path, stat);
}
/**
* Open a path as double-clicking would: the associated app for a file, an
* explorer window for a directory.
@@ -1412,157 +1276,6 @@ async function UIDesktop (options) {
await open_path_target(stat.path ?? shared_path, stat);
return true;
}
/**
* Act on a share link. A share to a person only ever reaches a real
* account, so for a temporary session the item is tried first — an item
* open to anyone with the link resolves for it — and only a miss asks
* for a sign-in, rather than burning the link on a "not found" as
* somebody who can't see it. The link stays in the address bar across
* the prompt because login reloads on success, which brings it back for
* the account that can actually open it.
*/
async function handle_shared_link (shared_path) {
if ( window.user?.is_temp ) {
const stat = await resolve_shared_item(puter.fs, shared_path);
if ( stat ) {
clear_shared_param();
await open_path_target(stat.path ?? shared_path, stat);
return;
}
await UIAuthLogin({
reload_on_success: true,
page: true,
});
// Dismissed without signing in: drop the link rather than loop.
if ( window.user?.is_temp ) clear_shared_param();
return;
}
clear_shared_param();
await open_shared_item(shared_path);
}
//--------------------------------------------------------------------------------------
// Opening an item someone shared, on the desktop
// i.e. https://puter.com/desktop?shared=%2F<owner>%2F<uuid>%2F<name>
// (the same link at the root lands in the dashboard's Shared view instead)
//--------------------------------------------------------------------------------------
if ( window.url_query_params.has(SHARED_PATH_PARAM) ) {
await handle_shared_link(window.url_query_params.get(SHARED_PATH_PARAM));
}
//--------------------------------------------------------------------------------------
// Direct download link
// i.e. https://puter.com/?download=<file_url> or https://puter.com/desktop?download=<file_url>
//--------------------------------------------------------------------------------------
const is_bare_desktop_path = window.url_paths.length === 0
|| (window.url_paths.length === 1 && window.url_paths[0]?.toLocaleLowerCase() === 'desktop');
if ( is_bare_desktop_path && window.url_query_params.has('download') ) {
const url = window.url_query_params.get('download');
let file_name = url.split('/').pop().split('?')[0];
let response = await UIAlert({
message: i18n('confirm_download_file_to_desktop', file_name),
type: 'confirm',
buttons: [
{ label: i18n('alert_yes'), value: true, type: 'primary' },
{ label: i18n('alert_no'), value: false, type: 'secondary' },
],
});
if ( ! response )
{
return;
}
let cancelled = false;
let upload_xhr = null;
const abort_controller = new AbortController();
// create progressbar dialog
let progwin = await UIWindowProgress({
title: i18n('downloading'),
icon: window.icons['app-icon-uploader.svg'],
operation_id: window.uuidv4(),
show_progress: true,
on_cancel: () => {
cancelled = true;
abort_controller.abort();
if ( upload_xhr ) {
upload_xhr.abort();
}
},
});
progwin?.set_status(i18n('downloading_file', file_name));
(async () => {
try {
// download the file
const response = await puter.net.fetch(url, {
signal: abort_controller.signal,
});
const total = Number(response.headers.get('content-length'));
const reader = response.body.getReader();
const chunks = [];
let received = 0;
while ( true ) {
const { done, value } = await reader.read();
if ( done || cancelled ) break;
if ( value ) {
// store the chunk
chunks.push(value);
received += value.length;
// calculate progress
const progress = Number.isFinite(total) && total > 0
? received / total
: 0;
// update progressbar
progwin?.set_progress(Math.floor(progress * 100));
}
}
if ( cancelled ) {
progwin?.close();
return;
}
// combine chunks into a blob
let blob = new Blob(chunks, {
type: response.headers.get('content-type') ?? 'application/octet-stream',
});
// reset progressbar
progwin?.set_progress(0);
progwin?.set_status(i18n('uploading_file', file_name));
// upload to user's desktop
await puter.fs.write(`~/Desktop/${file_name}`, blob, {
dedupeName: true,
progress: (_, percent) => {
// update progressbar
progwin?.set_progress(percent);
},
init: (_, xhr) => {
upload_xhr = xhr;
},
});
} catch (e) {
// alert the user if there's a genuine error
if ( !cancelled && e.name !== 'AbortError' ) {
await UIAlert({
message: `${i18n('error_download_failed') }: ${ e.message}`,
type: 'error',
});
}
}
// close progress window
progwin?.close();
})();
}
}
window.enter_fullpage_mode = (el_window) => {
@@ -1597,28 +1310,4 @@ window.reset_window_size_and_position = (el_window) => {
});
};
// Modify the hide/show functions to use CSS rules that will apply to all icons, including future ones
window.hideDesktopIcons = function () {
$('.desktop.item-container').addClass('desktop-icons-hidden');
};
window.showDesktopIcons = function () {
$('.desktop.item-container').removeClass('desktop-icons-hidden');
};
// Add this function to the global scope
window.toggleDesktopIcons = function () {
window.desktop_icons_hidden = !window.desktop_icons_hidden;
if ( window.desktop_icons_hidden ) {
hideDesktopIcons();
} else {
showDesktopIcons();
}
// Save preference
puter.kv.set('desktop_icons_hidden', window.desktop_icons_hidden.toString())
.catch(err => console.warn('Could not save desktop_icons_hidden:', err));
};
export default UIDesktop;
-18
View File
@@ -25,7 +25,6 @@ import path from '../lib/path.js';
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 { 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';
@@ -1242,23 +1241,6 @@ async function UIItem (options) {
}
}
// -------------------------------------------
// Publish As Website
// -------------------------------------------
if ( !is_trashed && !is_trash && options.is_dir ) {
menu_items.push({
html: i18n('publish_as_website'),
disabled: !options.is_dir,
onClick: async function () {
await publish_as_website({
uid: options.uid,
name: $(el_item).attr('data-name'),
path: $(el_item).attr('data-path'),
});
},
});
}
// -------------------------------------------
// Deploy As App
// -------------------------------------------
if ( !is_trashed && !is_trash && options.is_dir ) {
+21 -1
View File
@@ -29,7 +29,6 @@ vi.mock('./UIContextMenu.js', () => ({ default: vi.fn() }));
vi.mock('./UIAlert.js', () => ({ default: vi.fn() }));
vi.mock('../helpers/launchApp.js', () => ({ default: vi.fn() }));
vi.mock('../helpers/openItem.js', () => ({ default: vi.fn() }));
vi.mock('../helpers/publishAsWebsite.js', () => ({ default: vi.fn() }));
vi.mock('../helpers/openWithAI.js', () => ({ default: vi.fn() }));
vi.mock('../helpers/refreshItemContainer.js', () => ({ default: vi.fn() }));
@@ -176,6 +175,27 @@ describe('UIItem Publish as Worker', () => {
});
});
describe('UIItem Publish as Website', () => {
beforeEach(() => {
window.user = { username: 'alice' };
window.trash_path = '/alice/Trash';
});
it('is not offered, even on a folder the user owns', async () => {
const labels = await menuLabels({
path: '/alice/Site',
name: 'Site',
is_dir: true,
owner: 'alice',
shared_with_me: false,
share_mode: undefined,
shared_by: undefined,
});
expect(labels).not.toContain('publish_as_website');
expect(labels).toContain('deploy_as_app');
});
});
describe('UIItem dropped on the desktop', () => {
let stop;
-15
View File
@@ -28,7 +28,6 @@ import { atPath } from '../helpers/pathElements.js';
import { resolve_typed_path } from '../helpers/expandHomePath.js';
import UIWindowAppFeedback from './UIWindowAppFeedback.js';
import launch_app from '../helpers/launchApp.js';
import publish_as_website from '../helpers/publishAsWebsite.js';
import item_icon from '../helpers/itemIcon.js';
import { parent_path_for, shared_crumbs_for } from '../helpers/sharePaths.js';
@@ -2544,20 +2543,6 @@ async function UIWindow (options) {
// -------------------------------------------
menu_items.push('-');
// -------------------------------------------
// Publish As Website
// -------------------------------------------
menu_items.push({
html: i18n('publish_as_website'),
disabled: !options.is_dir,
onClick: async function () {
await publish_as_website({
uid: $(el_window).attr('data-uid'),
name: $(el_window).attr('data-name'),
path: $(el_window).attr('data-path'),
});
},
});
// -------------------------------------------
// Deploy as App
// -------------------------------------------
menu_items.push({
-136
View File
@@ -1,136 +0,0 @@
/*
* 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 UIWindow from './UIWindow.js';
import { underPath } from '../helpers/pathElements.js';
async function UIWindowPublishWebsite (target_dir_uid, target_dir_name, target_dir_path) {
let h = '';
h += '<div class="window-publishWebsite-content" style="padding: 20px; border-bottom: 1px solid #ced7e1;">';
// success
h += '<div class="window-publishWebsite-success">';
h += `<img src="${html_encode(window.icons['c-check.svg'])}" style="width:80px; height:80px; display: block; margin:10px auto;">`;
h += `<p style="text-align:center;">${i18n('dir_published_as_website', `<strong>${html_encode(target_dir_name)}</strong>`, false)}<p>`;
h += `<p style="text-align:center;"><a class="publishWebsite-published-link" target="_blank"></a><img class="publishWebsite-published-link-icon" src="${html_encode(window.icons['launch.svg'])}"></p>`;
h += `<button class="button button-normal button-block button-primary publish-window-ok-btn" style="margin-top:20px;">${i18n('ok')}</button>`;
h += '</div>';
// form
h += '<form class="window-publishWebsite-form">';
// error msg
h += '<div class="publish-website-error-msg"></div>';
// Subdomain input
h += '<div class="puter-subdomain-section" style="overflow: hidden; margin-bottom: 20px;">';
h += `<label style="margin-bottom: 10px; display: block;">${i18n('pick_name_for_website')}</label>`;
h += '<div style="font-family: monospace; display: flex; align-items: center; background: #f8f9fa; padding: 8px; border-radius: 6px; border: 1px solid #dee2e6;">';
h += `<span style="color: #666;">${html_encode(window.extractProtocol(window.url))}://</span>`;
h += '<input class="publish-website-subdomain" style="border: none; background: #ffffff; outline: none; padding: 7px !important; " type="text" autocomplete="subdomain" spellcheck="false" autocorrect="off" autocapitalize="off" data-gramm_editor="false"/>';
h += `<span style="color: #666;">.${html_encode(window.hosting_domain)}</span>`;
h += '</div>';
h += '</div>';
// uid
h += `<input class="publishWebsiteTargetDirUID" type="hidden" value="${html_encode(target_dir_uid)}"/>`;
// Publish
h += `<button class="publish-btn button button-action button-block button-normal">${i18n('publish')}</button>`;
h += '</form>';
h += '</div>';
const el_window = await UIWindow({
title: i18n('window_title_publish_website'),
icon: null,
uid: null,
is_dir: false,
body_content: h,
has_head: true,
selectable_body: false,
draggable_body: false,
allow_context_menu: false,
is_resizable: false,
is_droppable: false,
init_center: true,
allow_native_ctxmenu: true,
allow_user_select: true,
width: 450,
dominant: true,
onAppend: function (this_window) {
$(this_window).find('.publish-website-subdomain').val(window.generate_identifier());
$(this_window).find('.publish-website-subdomain').get(0).focus({ preventScroll: true });
},
window_class: 'window-publishWebsite',
window_css: {
height: 'initial',
},
body_css: {
width: 'initial',
height: '100%',
'background-color': 'rgb(245 247 249)',
'backdrop-filter': 'blur(3px)',
},
});
$(el_window).find('.publish-btn').on('click', async function (e) {
e.preventDefault();
// disable 'Publish' button
$(el_window).find('.publish-btn').prop('disabled', true);
try {
let subdomain = $(el_window).find('.publish-website-subdomain').val();
if ( ! subdomain.trim() ) {
throw new Error('Please enter a subdomain name');
}
const res = await puter.hosting.create(subdomain, target_dir_path);
let url = `https://${ subdomain }.${ window.hosting_domain }/`;
// Show success
$(el_window).find('.window-publishWebsite-form').hide(100, function () {
$(el_window).find('.publishWebsite-published-link').attr('href', url);
$(el_window).find('.publishWebsite-published-link').text(url);
$(el_window).find('.window-publishWebsite-success').show(100);
$(`.item[data-uid="${target_dir_uid}"] .item-has-website-badge`).show();
});
// find all items whose path starts with target_dir_path
underPath('.item', target_dir_path).each(function () {
// show the link badge
$(this).find('.item-has-website-url-badge').show();
// update item's website_url attribute
$(this).attr('data-website_url', url + $(this).attr('data-path').substring(target_dir_path.length));
});
window.update_sites_cache();
} catch ( err ) {
const errorMessage = err.message || (err.error && err.error.message) || 'An error occurred while publishing';
$(el_window).find('.publish-website-error-msg').html(errorMessage);
$(el_window).find('.publish-website-error-msg').fadeIn();
// re-enable 'Publish' button
$(el_window).find('.publish-btn').prop('disabled', false);
}
});
$(el_window).find('.publish-window-ok-btn').on('click', function () {
$(el_window).close();
});
}
export default UIWindowPublishWebsite;
+1 -34
View File
@@ -2036,7 +2036,7 @@ label {
font-size: 14px;
}
.login-error-msg, .signup-error-msg, .publish-website-error-msg, .form-error-msg {
.login-error-msg, .signup-error-msg, .form-error-msg {
display: none;
color: red;
border: 1px solid red;
@@ -2130,34 +2130,6 @@ label {
font-size: 13px;
}
.publish-btn {
margin-top: 20px;
}
.window-publishWebsite-success, .window-give-item-access-success {
display: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
height: auto;
}
.publishWebsite-published-link {
text-decoration: none;
color: #007cff;
}
.publishWebsite-published-link:hover {
text-decoration: underline;
}
.publishWebsite-published-link-icon {
display: inline-block;
width: 12px;
margin-left: 5px;
margin-bottom: -1px;
user-select: none !important;
}
.login-form-title, .signup-form-title {
text-align: center;
margin-top: 10px;
@@ -5222,11 +5194,6 @@ html.dark-mode .usage-table-fade-overlay {
touch-action: pan-y;
}
/* Hide desktop icons when the desktop-icons-hidden class is applied */
.desktop.item-container.desktop-icons-hidden>.item {
visibility: hidden;
}
.progress-report{
font-size:15px;
-4
View File
@@ -2819,10 +2819,6 @@ window.extractSubdomain = function (url) {
return subdomain;
};
window.extractProtocol = function (url) {
var protocol = url.split('://')[0];
return protocol;
};
window.sleep = function (ms) {
return new Promise(resolve => setTimeout(resolve, ms));
};
+6 -6
View File
@@ -67,7 +67,7 @@ const oidc_return_path = () => {
return pathname;
}
// app landing: normalize away a trailing slash to match the backend whitelist
if ( /^(\/desktop)?\/app\/[^/]+\/?$/.test(pathname) ) {
if ( /^\/app\/[^/]+\/?$/.test(pathname) ) {
return pathname.replace(/\/$/, '');
}
return null;
@@ -77,11 +77,11 @@ const oidc_return_path = () => {
* The `return_to` to send along when starting an OIDC flow, or null if the
* current page isn't one the backend will return to. The backend strictly
* whitelists these (never a client-supplied URL): `/desktop`, `/dashboard`,
* and direct app landings (`/app/<name>`, plus the desktop-booted
* `/desktop/app/<name>`), so OIDC login started from an app landing comes back
* to the app — and to the same interface it was opened in.
* and direct app landings (`/app/<name>`), so OIDC login started from an app
* landing comes back to the app.
*
* A share link (`?shared=`, from an email) is carried along with the path: the
* A share link (`?shared=`, from an email) is carried along with the path, except
* from `/desktop`, which doesn't open share links: the
* recipient usually has to sign in before they can see what was shared, and an
* OIDC round trip leaves the origin, so the parameter has to travel through the
* flow or they come back to a bare Home. Only well-formed values go — the
@@ -93,7 +93,7 @@ export const get_oidc_return_to = () => {
const path = oidc_return_path();
if ( path === null ) return null;
const shared = new URLSearchParams(window.location.search ?? '')
const shared = path === '/desktop' ? [] : new URLSearchParams(window.location.search ?? '')
.getAll(SHARED_PATH_PARAM)
.filter(value => parse_shared_path(value) !== null);
if ( shared.length === 0 ) {
+10 -10
View File
@@ -51,17 +51,12 @@ describe('get_oidc_return_to', () => {
expect(at('/app/editor/')).toBe('/app/editor');
});
it('keeps a desktop app landing on the desktop', () => {
expect(at('/desktop/app/editor')).toBe('/desktop/app/editor');
expect(at('/desktop/app/editor/')).toBe('/desktop/app/editor');
});
it('carries a share link so the item survives the round trip', () => {
expect(at('/', share_search(shared_path('Report.pdf')))).toBe(
`/${share_search(shared_path('Report.pdf'))}`,
);
expect(at('/desktop', share_search(shared_path('Report.pdf')))).toBe(
`/desktop${share_search(shared_path('Report.pdf'))}`,
expect(at('/dashboard', share_search(shared_path('Report.pdf')))).toBe(
`/dashboard${share_search(shared_path('Report.pdf'))}`,
);
expect(
at('/', share_search(shared_path('a.txt'), shared_path('b.txt'))),
@@ -83,10 +78,14 @@ describe('get_oidc_return_to', () => {
expect(at('/', share_search('/alice/Documents/Report.pdf'))).toBe(null);
expect(at('/', '?shared=')).toBe(null);
// other parameters are not ours to carry
expect(at('/desktop', '?app=editor')).toBe('/desktop');
expect(at('/dashboard', '?app=editor')).toBe('/dashboard');
expect(
at('/desktop', `${share_search(shared_path('a.txt'))}&app=editor`),
).toBe(`/desktop${share_search(shared_path('a.txt'))}`);
at('/dashboard', `${share_search(shared_path('a.txt'))}&app=editor`),
).toBe(`/dashboard${share_search(shared_path('a.txt'))}`);
});
it('drops a share link on the desktop, which does not open one', () => {
expect(at('/desktop', share_search(shared_path('Report.pdf')))).toBe('/desktop');
});
it('returns null for anything the backend would reject', () => {
@@ -94,6 +93,7 @@ describe('get_oidc_return_to', () => {
expect(at('/settings')).toBe(null);
expect(at('/action/login')).toBe(null);
expect(at('/app/editor/extra')).toBe(null);
expect(at('/desktop/app/editor')).toBe(null);
expect(at('/desktop/app/editor/extra')).toBe(null);
expect(at('/dashboard/app/editor')).toBe(null);
});
+36
View File
@@ -0,0 +1,36 @@
/*
* 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/>.
*/
/**
* Whether a page load boots the dashboard instead of the desktop. The root
* path and `/app/<name>` boot the dashboard unless the page is an iframe embed
* or a popup, which the desktop still handles; `/dashboard` always does.
*
* @param {Object} location
* @param {string} location.pathname
* @param {boolean} location.inIframe
* @param {boolean} location.embeddedInPopup
* @returns {boolean}
*/
export function bootsDashboard ({ pathname, inIframe, embeddedInPopup }) {
if ( pathname === '/dashboard' || pathname === '/dashboard/' ) return true;
const isAppLanding = /^\/app\/[^/]+\/?$/.test(pathname);
if ( pathname !== '/' && !isAppLanding ) return false;
return !embeddedInPopup && !inIframe;
}
@@ -0,0 +1,59 @@
/*
* 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 { describe, it, expect } from 'vitest';
import { bootsDashboard } from './bootsDashboard.js';
const boots = (url, { inIframe = false, embeddedInPopup = false } = {}) => {
const { pathname } = new URL(url, 'https://puter.com');
return bootsDashboard({ pathname, inIframe, embeddedInPopup });
};
describe('bootsDashboard', () => {
it('boots the dashboard at the root, its alias, and app landings', () => {
expect(boots('/')).toBe(true);
expect(boots('/dashboard')).toBe(true);
expect(boots('/dashboard/')).toBe(true);
expect(boots('/app/editor')).toBe(true);
});
it('boots the desktop at /desktop', () => {
expect(boots('/desktop')).toBe(false);
expect(boots('/desktop/app/editor')).toBe(false);
});
it('boots the desktop for flows only the desktop handles', () => {
expect(boots('/', { inIframe: true })).toBe(false);
expect(boots('/', { embeddedInPopup: true })).toBe(false);
});
it('keeps /dashboard on the dashboard even inside an iframe', () => {
expect(boots('/dashboard', { inIframe: true })).toBe(true);
});
it('boots the dashboard whatever the query asks for', () => {
expect(boots('/?shared=%2Fann%2Fuid%2Fnotes.txt')).toBe(true);
expect(boots('/?download=https%3A%2F%2Fexample.com%2Ffile.zip')).toBe(true);
expect(boots('/?app=explorer&path=%2Falice%2FDocuments')).toBe(true);
expect(boots('/app/editor?app=explorer')).toBe(true);
expect(boots('/app/editor?puter.fullpage=true')).toBe(true);
expect(boots('/app/editor?puter.fullpage=false')).toBe(true);
expect(boots('/?puter.fullpage=1')).toBe(true);
});
});
+5 -11
View File
@@ -20,7 +20,6 @@
import UIAlert from '../UI/UIAlert.js';
import UIWindowItemProperties from '../UI/UIWindowItemProperties.js';
import publish_as_website from './publishAsWebsite.js';
import open_item from './openItem.js';
import openWithAI from './openWithAI.js';
import launch_app from './launchApp.js';
@@ -44,7 +43,7 @@ import { is_default_app } from './defaultApp.js';
* @param {boolean} options.openWithInNewTab - Open the "Open With" apps in a new browser tab instead of an in-page window (used by Dashboard)
* @param {Function} options.onShowProperties - Optional custom properties handler (used by Dashboard); receives {name, path, uid, element}
* @param {Function} options.onShare - Opens sharing for the item; Share is left out of the menu without it. Receives {name, path, uid, element}
* @param {Function} options.onPublishWebsite - Optional custom publish-as-website handler (used by Dashboard); receives {uid, name, path}
* @param {Function} options.onPublishWebsite - Publishes the folder as a website; Publish as Website is left out of the menu without it. Receives {uid, name, path}
* @param {Function} options.onPublishWorker - Publishes the file as a worker; Publish as Worker is left out of the menu without it. Receives {uid, name, path}
* @param {Function} options.onPasteInto - Optional custom paste handler (used by Dashboard); receives the folder's path
* @param {Function} options.onRemovedFromShared - Optional, called once Remove from Shared has dropped the user's own access (used by Dashboard)
@@ -135,24 +134,19 @@ const generate_file_context_menu = async function (options) {
// -------------------------------------------
// Publish As Website
// -------------------------------------------
if ( !is_trashed && !is_trash && fsentry.is_dir ) {
if ( !is_trashed && !is_trash && fsentry.is_dir && options.onPublishWebsite ) {
menu_items.push({
html: i18n('publish_as_website'),
// Publishing serves the folder to anyone, for good, so it takes the
// same own-it-or-`manage` right as sharing it does (SubdomainDriver
// enforces that). `write` on someone else's folder is not enough.
disabled: !fsentry.is_dir || fsentry.has_website || !may_share,
onClick: async function () {
const target = {
onClick: function () {
options.onPublishWebsite({
uid: fsentry.uid,
name: $(el_item).attr('data-name'),
path: $(el_item).attr('data-path'),
};
if ( options.onPublishWebsite ) {
options.onPublishWebsite(target);
} else {
await publish_as_website(target);
}
});
},
});
}
@@ -23,7 +23,6 @@ import jQuery from '../lib/jquery-3.6.1/jquery-3.6.1.min.js';
vi.mock('../UI/UIAlert.js', () => ({ default: vi.fn() }));
vi.mock('../UI/UIWindowItemProperties.js', () => ({ default: vi.fn() }));
vi.mock('./publishAsWebsite.js', () => ({ default: vi.fn() }));
vi.mock('./openItem.js', () => ({ default: vi.fn() }));
vi.mock('./openWithAI.js', () => ({ default: vi.fn() }));
vi.mock('./launchApp.js', () => ({ default: vi.fn() }));
@@ -114,6 +113,37 @@ describe('Share', () => {
});
});
describe('Publish as Website', () => {
const publishItem = async (options) => {
is_owned_by_me.mockReturnValueOnce(true);
can_share.mockResolvedValueOnce(true);
const row = document.createElement('div');
row.setAttribute('data-path', '/me/Site');
row.setAttribute('data-name', 'Site');
const items = await generate_file_context_menu({
element: row,
fsentry: { uid: 'dir-1', name: 'Site', path: '/me/Site', is_dir: true },
associated_app_name: 'editor',
...options,
});
return items.find((item) => item !== '-' && item.html === 'publish_as_website');
};
it('hands the folder to onPublishWebsite', async () => {
const onPublishWebsite = vi.fn();
await (await publishItem({ onPublishWebsite })).onClick();
expect(onPublishWebsite).toHaveBeenCalledWith({
uid: 'dir-1',
name: 'Site',
path: '/me/Site',
});
});
it('is left out without onPublishWebsite', async () => {
expect(await publishItem({})).toBeUndefined();
});
});
describe('Publish as Worker', () => {
const publishItem = async (options) => {
is_owned_by_me.mockReturnValueOnce(true);
-3
View File
@@ -106,9 +106,6 @@ const runLaunch = async (options) => {
const launchAttributes = {
'launch.app': options?.name ?? options?.app_obj?.name ?? 'unknown',
'launch.dashboard_mode': !! window.is_dashboard_mode,
// url_paths has the `/desktop` prefix stripped, so this counts the
// desktop-booted landing (`/desktop/app/<name>`) as the app URL
// it is.
'launch.from_app_url':
window.url_paths?.[0]?.toLocaleLowerCase() === 'app'
&& !! window.url_paths?.[1],
+5 -3
View File
@@ -84,8 +84,9 @@ export function shared_link_recipient_uuid (params) {
* that account's saved session, ask whether to sign in to it, or nothing.
*
* Popups, embeds (`embedded`) and `action` flows act for whoever opened them,
* so a link must not change their account. A temporary session is left to the
* share link's own sign-in prompt.
* so a link must not change their account. The desktop (`desktop`) doesn't
* open share links, so it never acts on the hint. A temporary session is left
* to the share link's own sign-in prompt.
*
* @returns {{ switch_to: object } | { ask: true } | null}
*/
@@ -93,10 +94,11 @@ export function shared_link_account_step ({
params,
action,
embedded,
desktop,
current_user,
logged_in_users,
}) {
if ( action || embedded ) return null;
if ( action || embedded || desktop ) return null;
const uuid = shared_link_recipient_uuid(params);
if ( ! uuid || uuid === current_user?.uuid?.toLowerCase() ) return null;
const saved = (logged_in_users ?? []).find(user =>
@@ -127,6 +127,10 @@ describe('shared_link_account_step', () => {
expect(step({ action: 'login' })).toBeNull();
});
it('leaves the desktop alone, since it does not open share links', () => {
expect(step({ desktop: true })).toBeNull();
});
it('leaves a temporary session to the share link sign-in', () => {
const temp = { uuid: CURRENT, is_temp: true };
expect(step({ current_user: temp, logged_in_users: [temp] }))
-40
View File
@@ -1,40 +0,0 @@
/*
* 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 UIWindowPublishWebsite from '../UI/UIWindowPublishWebsite.js';
import ensure_can_publish from './ensureCanPublish.js';
/**
* Opens the "Publish as website" flow for a directory, first walking the user
* through account creation and email confirmation when the deployment requires
* a verified email to publish. Returns without publishing if the user backs out
* of either step.
*
* @param {Object} dir - The directory to publish
* @param {string} dir.uid - UID of the directory
* @param {string} dir.name - Display name of the directory
* @param {string} dir.path - Full path of the directory
* @returns {Promise<void>}
*/
const publish_as_website = async function ({ uid, name, path }) {
if ( ! await ensure_can_publish(window.require_email_verification_to_publish_website) ) return;
await UIWindowPublishWebsite(uid, name, path);
};
export default publish_as_website;
+4 -17
View File
@@ -21,22 +21,9 @@
* Splits a GUI pathname into the route segments the boot code matches on
* (`window.url_paths`), dropping empty segments.
*
* A leading `desktop` segment is dropped with them: `/desktop` only picks the
* interface, and everything after it is the same route it would be at the
* root. So `/desktop/app/<name>` yields `['app', '<name>']` — the app landing
* every route check downstream already understands — and the only difference
* from `/app/<name>` is that the dashboard-mode check (see initgui.js) doesn't
* claim the path, leaving the app to open on the desktop.
*
* @param {string} pathname - the pathname to parse, e.g. `/desktop/app/editor`
* @param {string} pathname - the pathname to parse, e.g. `/app/editor`
* @returns {string[]} route segments
*/
export const parse_url_paths = (pathname) => {
const paths = String(pathname ?? '')
.split('/')
.filter((element) => element);
if ( paths[0]?.toLocaleLowerCase() === 'desktop' ) {
paths.shift();
}
return paths;
};
export const parse_url_paths = (pathname) => String(pathname ?? '')
.split('/')
.filter((element) => element);
+3 -20
View File
@@ -28,29 +28,12 @@ describe('parse_url_paths', () => {
expect(parse_url_paths('')).toEqual([]);
});
it('drops a leading desktop segment so the rest reads as a root route', () => {
it('keeps a leading desktop segment, so /desktop/app/<name> is not an app landing', () => {
expect(parse_url_paths('/desktop')).toEqual(['desktop']);
expect(parse_url_paths('/desktop/app/editor')).toEqual([
'app',
'editor',
]);
expect(parse_url_paths('/DeskTop/app/editor')).toEqual([
'app',
'editor',
]);
expect(parse_url_paths('/desktop')).toEqual([]);
expect(parse_url_paths('/desktop/')).toEqual([]);
});
it('only drops the prefix, not a later or lookalike segment', () => {
expect(parse_url_paths('/app/desktop')).toEqual(['app', 'desktop']);
expect(parse_url_paths('/desktops/app/editor')).toEqual([
'desktops',
'app',
'editor',
]);
expect(parse_url_paths('/desktop/desktop/app')).toEqual([
'desktop',
'app',
'editor',
]);
});
});
-3
View File
@@ -92,7 +92,6 @@ const ar = {
desktop: 'سطح المكتب',
desktop_background_fit: 'ملائمة',
developers: 'المطورين',
dir_published_as_website: ':%strong% تم نشره إلى',
disable_2fa: 'تعطيل المصادقة الثنائية',
disable_2fa_confirm: 'هل أنت متأكد أنك تريد تعطيل المصادقة الثنائية؟',
disable_2fa_instructions: '.أدخل كلمة المرور لتعطيل المصادقة الثنائية',
@@ -100,7 +99,6 @@ const ar = {
dont_allow: 'عدم السماح',
download: 'تنزيل',
download_file: 'تنزيل الملف',
downloading: 'جارٍ التنزيل',
email: 'البريد الإلكتروني',
email_change_confirmation_sent:
'.تم إرسال بريد تأكيد إلى عنوان بريدك الإلكتروني الجديد. يرجى التحقق من صندوق الوارد واتباع التعليمات لإكمال العملية',
@@ -189,7 +187,6 @@ const ar = {
paste_into_folder: 'لصق في المجلد',
path: 'المسار',
personalization: 'تخصيص',
pick_name_for_website: 'اختر اسمًا لموقعك الإلكتروني:',
pictures: 'الصور',
plural_suffix: 's', //this is not necessary for Arabic
powered_by_puter_js: '{{link=docs}}Puter.js{{/link}} مدعوم بواسطة ',
-3
View File
@@ -98,7 +98,6 @@ const bg = {
desktop: 'Работен плот',
desktop_background_fit: 'Побери',
developers: 'Разработчици',
dir_published_as_website: '%strong% беше публикувана на:',
disable_2fa: 'Изключи 2FA',
disable_2fa_confirm: 'Сигурни ли сте, че искате да изключите 2FA?',
disable_2fa_instructions: 'Въведете паролата си, за да изключите 2FA.',
@@ -106,7 +105,6 @@ const bg = {
dont_allow: 'Не разрешавай',
download: 'Изтегли',
download_file: 'Изтегли файл',
downloading: 'Изтегляне',
email: 'Имейл',
email_change_confirmation_sent:
'Потвърдителен имейл беше изпратен на новия ви имейл адрес. Моля, проверете входящата си поща и следвайте инструкциите, за да завършите процеса.',
@@ -198,7 +196,6 @@ const bg = {
paste_into_folder: 'Постави в папката',
path: 'Път',
personalization: 'Персонализация',
pick_name_for_website: 'Изберете име за вашия уебсайт:',
pictures: 'Снимки',
plural_suffix: 'и',
powered_by_puter_js: 'Работи с {{link=docs}}Puter.js{{/link}}',
-3
View File
@@ -85,7 +85,6 @@ const bn = {
desktop: 'ডেস্কটপ',
desktop_background_fit: 'ফিট',
developers: 'ডেভেলপারগণ',
dir_published_as_website: '%strong% প্রকাশিত হয়েছে:',
disable_2fa: '2FA অক্ষম করুন',
disable_2fa_confirm: 'আপনি কি নিশ্চিত যে আপনি 2FA অক্ষম করতে চান?',
disable_2fa_instructions: '2FA অক্ষম করতে আপনার পাসওয়ার্ড লিখুন।',
@@ -93,7 +92,6 @@ const bn = {
dont_allow: 'অনুমতি নেই',
download: 'ডাউনলোড',
download_file: 'ফাইল ডাউনলোড করুন',
downloading: 'ডাউনলোড হচ্ছে',
email: 'ই-মেইল',
email_change_confirmation_sent: 'নতুন ই-মেইল ঠিকানা নিশ্চিতকরণের জন্য একটি নিশ্চিতকরণ ইমেল পাঠানো হয়েছে। আপনার ইনবক্স পরীক্ষা করুন এবং নির্দেশানুযায়ী প্রক্রিয়াটি সম্পন্ন করুন।',
email_invalid: 'ই-মেইল অবৈধ।',
@@ -176,7 +174,6 @@ const bn = {
paste_into_folder: 'ফোল্ডারে পেস্ট করুন',
path: 'পথ',
personalization: 'ব্যক্তিগতীকরণ',
pick_name_for_website: 'আপনার ওয়েবসাইটের জন্য নাম নির্বাচন করুন:',
pictures: 'চিত্র',
plural_suffix: 'গুলি',
powered_by_puter_js: '{{link=docs}}Puter.js{{/link}} দ্বারা প্রচালিত',
-3
View File
@@ -85,7 +85,6 @@ const br = {
desktop: 'Área de Trabalho',
desktop_background_fit: 'Ajustar',
developers: 'Desenvolvedores',
dir_published_as_website: '%strong% foi publicado em:',
disable_2fa: 'Desativar 2FA',
disable_2fa_confirm: 'Tem certeza de que deseja desativar 2FA?',
disable_2fa_instructions: 'Digite sua senha para desativar 2FA.',
@@ -93,7 +92,6 @@ const br = {
dont_allow: 'Não Permitir',
download: 'Baixar',
download_file: 'Baixar Arquivo',
downloading: 'Baixando',
email: 'Email',
email_change_confirmation_sent: 'Um e-mail de confirmação foi enviado para seu novo endereço de e-mail. Por favor, verifique sua caixa de entrada e siga as instruções para concluir o processo.',
email_invalid: 'Email inválido.',
@@ -173,7 +171,6 @@ const br = {
'paste_into_folder': 'Colar na Pasta',
'path': 'Caminho',
'personalization': 'Personalização',
'pick_name_for_website': 'Escolha um nome para o seu site:',
'pictures': 'Imagens',
'plural_suffix': 's',
'powered_by_puter_js': 'Desenvolvido por {{link=docs}}Puter.js{{/link}}',
-8
View File
@@ -131,7 +131,6 @@ const cs = {
"deleting_file": "Mazání %%",
"directory_depth_limit_exceeded": "Tato složka je vnořena příliš hluboko. Než do ní přidáte další složky, přesuňte ji někam blíže ke kořeni.",
"disable_2fa_confirm": "Opravdu chcete deaktivovat 2FA?",
"dir_published_as_website": "%strong% byl publikován pro:",
"documents": "Dokumenty",
"dont_allow": "Nepovolit",
"date_modified": "Datum změny",
@@ -140,15 +139,11 @@ const cs = {
"client_information": "Informace o klientovi",
"download": "Stáhnout",
"download_file": "Stáhnout soubor",
"downloading": "Stahování",
"email": "E-mail",
"confirm_download_file_to_desktop": "Opravdu chcete stáhnout %% na plochu?",
"email_change_confirmation_sent": "Na novou e-mailovou adresu byl odeslán potvrzovací e-mail. Zkontrolujte doručenou poštu a dokončete postup podle pokynů.",
"disk_storage": "Diskové úložiště",
"email_invalid": "E-mail je neplatný.",
"error_download_failed": "Stažení souboru se nezdařilo",
"disable_2fa_instructions": "Pro deaktivaci 2FA zadejte své heslo.",
"downloading_file": "Stahování %%",
"email_required": "E-mail je povinný.",
"empty_trash": "Vysypat koš",
"emptying_trash": "Vysypávání koše…",
@@ -259,7 +254,6 @@ const cs = {
"privacy": "Soukromí",
"powered_by_puter_js": "Používá technologii {{link=docs}}Puter.js{{/link}}",
"proceed_with_account_deletion": "Pokračujte ve smazání účtu",
"pick_name_for_website": "Vyberte název pro svůj web:",
"phone_code_sent_whatsapp": "Zadejte 6místný kód odeslaný přes WhatsApp na",
"phone_code_sent_to": "Zadejte 6místný kód odeslaný na",
"properties": "Vlastnosti",
@@ -437,7 +431,6 @@ const cs = {
"username": "Uživatelské jméno",
"username_required": "Uživatelské jméno je povinné.",
"unzipping": "Rozbalení %strong%",
"uploading_file": "Nahrávání %%",
"untarring": "Rozbalování %strong%",
"visibility": "Viditelnost",
"videos": "Videa",
@@ -611,7 +604,6 @@ const cs = {
"error_user_or_path_not_found": "Uživatel nebo cesta nenalezena.",
"token_copied": "Token zkopírován",
"copy_auth_token": "Kopírovat ověřovací token",
"error_invalid_username": "Neplatné uživatelské jméno.",
"perm_dialog_wants_to": "chce povolení",
"token_label": "Označení",
"token_label_required": "Zadejte štítek pro tento token.",
-3
View File
@@ -85,7 +85,6 @@ const da = {
desktop: 'Skrivebord',
desktop_background_fit: 'Tilpas',
developers: 'Udviklere',
dir_published_as_website: '%strong% er blevet offentliggjort til:',
disable_2fa: 'Deaktiver 2FA',
disable_2fa_confirm: 'Er du sikker på, at du vil deaktivere 2FA?',
disable_2fa_instructions: 'Indtast din adgangskode for at deaktivere 2FA.',
@@ -93,7 +92,6 @@ const da = {
dont_allow: 'Tillad ikke',
download: 'Download',
download_file: 'Download fil',
downloading: 'Downloader',
email: 'E-mail',
email_change_confirmation_sent: 'En bekræftelses-e-mail er sendt til din nye e-mailadresse. Kontroller din indbakke og følg instruktionerne for at fuldføre processen.',
email_invalid: 'E-mail er ugyldig.',
@@ -173,7 +171,6 @@ const da = {
paste_into_folder: 'Sæt ind i mappe',
path: 'Sti',
personalization: 'Personalisering',
pick_name_for_website: 'Vælg et navn til dit websted:',
pictures: 'Billeder',
plural_suffix: 's',
powered_by_puter_js: 'Udviklet af {{link=docs}}Puter.js{{/link}}',
-3
View File
@@ -84,7 +84,6 @@ const de = {
desktop: 'Desktop',
desktop_background_fit: 'Passend',
developers: 'Entwickler',
dir_published_as_website: '%strong% wurde veröffentlicht unter:',
disable_2fa: '2FA deaktivieren',
disable_2fa_confirm: 'Möchten Sie 2FA wirklich deaktivieren?',
disable_2fa_instructions: 'Geben Sie Ihr Passwort ein, um 2FA zu deaktivieren.',
@@ -92,7 +91,6 @@ const de = {
dont_allow: 'Nicht erlauben',
download: 'Herunterladen',
download_file: 'Datei herunterladen',
downloading: 'Lädt herunter',
email: 'E-Mail',
email_change_confirmation_sent: 'Eine Bestätigungs-E-Mail wurde an Ihre neue E-Mail-Adresse gesendet. Bitte überprüfen Sie Ihren Posteingang und folgen Sie den Anweisungen, um den Vorgang abzuschließen.',
email_invalid: 'E-Mail ist ungültig.',
@@ -172,7 +170,6 @@ const de = {
paste_into_folder: 'In Ordner einfügen',
path: 'Pfad',
personalization: 'Personalisierung',
pick_name_for_website: 'Wählen Sie einen Namen für Ihre Webseite:',
pictures: 'Bilder',
plural_suffix: 's',
powered_by_puter_js: 'Betrieben von {{link=docs}}Puter.js{{/link}}',
-3
View File
@@ -53,10 +53,8 @@ const emoji = {
deploy_as_app: '🚀📱',
descending: '🔽',
desktop_background_fit: '🖥️🖼️',
dir_published_as_website: '📂📰🌐',
download: '⬇️',
download_file: '⬇️📄',
downloading: '⬇️➡️',
email: '📧',
email_or_username: '📧👤',
empty_trash: '🗑️🆓',
@@ -102,7 +100,6 @@ const emoji = {
passwords_do_not_match: '🔑❌🔑',
paste: '📋➡️',
paste_into_folder: '📋➡️📂',
pick_name_for_website: '🌐📛❓:',
powered_by_puter_js: '⚙️🔌🔗',
preparing: '🔄🔜',
preparing_for_upload: '🔄🔜',
-8
View File
@@ -215,7 +215,6 @@ const en = {
desktop: 'Desktop',
desktop_background_fit: 'Fit',
developers: 'Developers',
dir_published_as_website: '%strong% has been published to:',
directory_depth_limit_exceeded: 'This folder is nested too deeply. Move it somewhere closer to the top before adding more folders inside it.',
disable_2fa: 'Disable 2FA',
disable_2fa_confirm: 'Are you sure you want to disable 2FA?',
@@ -229,12 +228,8 @@ const en = {
done: 'Done',
dont_allow: 'Don\'t Allow',
download: 'Download',
confirm_download_file_to_desktop: 'Are you sure you want to download %% to your Desktop?',
download_file: 'Download File',
downloading: 'Downloading',
downloading_file: 'Downloading %%',
drag_to_reposition: 'Drag to reposition',
error_download_failed: 'Failed to download file',
email: 'Email',
email_change_confirmation_sent: 'A confirmation email has been sent to your new email address. Please check your inbox and follow the instructions to complete the process.',
email_invalid: 'Email is invalid.',
@@ -420,7 +415,6 @@ const en = {
phone_card_fallback_link: 'Verify with a card instead',
phone_card_fallback_note: "We only check that the card is valid \u2014 you won't be charged.",
phone_card_fallback_unavailable: "Card verification isn't available right now. Please try your phone number again, or email [email protected] and we'll help you finish setting up your account.",
pick_name_for_website: 'Pick a name for your website:',
pictures: 'Pictures',
pixel_ratio: 'Pixel Ratio',
plural_suffix: 's',
@@ -783,7 +777,6 @@ const en = {
untarring: 'Untarring %strong%',
upload: 'Upload',
uploading: 'Uploading',
uploading_file: 'Uploading %%',
upload_here: 'Upload here',
uptime: 'Uptime',
used_of: '{{used}} used of {{available}}',
@@ -1054,7 +1047,6 @@ const en = {
'enable_push_footnote': 'You can turn them off anytime in Settings.',
'error_user_or_path_not_found': 'User or path not found.',
'error_invalid_username': 'Invalid username.',
// Auth token
auth_token: 'Auth Token',
-3
View File
@@ -101,7 +101,6 @@ const es = {
desktop: 'Escritorio',
desktop_background_fit: 'Ajustar',
developers: 'Desarrolladores',
dir_published_as_website: '%strong% ha sido publicado en:',
disable_2fa: 'Deshabilitar 2FA',
disable_2fa_confirm: '¿Estás seguro que quieres deshabilitar 2FA?',
disable_2fa_instructions: 'Ingresa tu contraseña para deshabilitar 2FA.',
@@ -109,7 +108,6 @@ const es = {
dont_allow: 'No permitir',
download: 'Descargar',
download_file: 'Descargar archivo',
downloading: 'Descargando',
email: 'Correo electrónico',
email_change_confirmation_sent:
'Se ha enviado un mensaje de confirmación a tu nueva dirección de correo electrónico. Por favor, revisa tu bandeja de entrada y sigue las instrucciónes para completar el proceso.',
@@ -199,7 +197,6 @@ const es = {
paste_into_folder: 'Pegar en la Carpeta',
path: 'Ruta',
personalization: 'Personalización',
pick_name_for_website: 'Escoge un nombre para tu página web:',
pictures: 'Imagenes',
plural_suffix: 's',
powered_by_puter_js: 'Creado por {{link=docs}}Puter.js{{/link}}',
-3
View File
@@ -94,7 +94,6 @@ const fa = {
desktop: 'دسکتاپ',
desktop_background_fit: 'متناسب',
developers: 'تولیدکنندگان نرم افزار',
dir_published_as_website: '%strong% منتشر شده به:',
disable_2fa: 'غیر فعال کردن احراز هویت دو مرحله ای',
disable_2fa_confirm:
'آیا مطمئن هستید که می‌خواهید احراز هویت دو مرحله ای را غیرفعال کنید؟',
@@ -104,7 +103,6 @@ const fa = {
dont_allow: 'عدم اجازه',
download: 'دانلود',
download_file: 'دانلود فایل',
downloading: 'دانلود',
email: 'ایمیل',
email_change_confirmation_sent:
'یک ایمیل تاییدیه به آدرس ایمیل جدید شما ارسال شده است. لطفاً صندوق ایمیلهای دریافتی خود را بررسی کرده و دستورالعمل‌ها را برای تکمیل فرایند دنبال کنید.',
@@ -193,7 +191,6 @@ const fa = {
paste_into_folder: 'چسباندن در پوشه',
path: 'مسیر',
personalization: 'شخصی سازی',
pick_name_for_website: 'یک نام برای وبسایت خود انتخاب کنید:',
pictures: 'تصاویر',
plural_suffix: 'ها',
powered_by_puter_js: 'پشتیبانی شده توسط {{link=docs}}Puter.js{{/link}}',
-3
View File
@@ -112,7 +112,6 @@ const fi = {
desktop: 'Työpöytä',
desktop_background_fit: 'Sovita',
developers: 'Kehittäjät',
dir_published_as_website: '%strong% on julkaistu osoitteessa:',
disable_2fa: 'Ota kaksivaiheinen tunnistautuminen pois käytöstä',
disable_2fa_confirm:
'Haluatko varmasti poistaa kaksivaiheisen tunnistautumisen käytöstä?',
@@ -122,7 +121,6 @@ const fi = {
dont_allow: 'Älä salli',
download: 'Lataa',
download_file: 'Lataa tiedosto',
downloading: 'Ladataan',
email: 'Sähköpostiosoite',
email_change_confirmation_sent:
'Vahvistusviesti on lähetetty uuteen sähköpostiosoitteeseesi. Tarkista postilaatikkosi ja viimeistele prosessi seuraamalla ohjeita.',
@@ -235,7 +233,6 @@ const fi = {
paste_into_folder: 'Liitä kansioon',
path: 'Polku',
personalization: 'Personointi',
pick_name_for_website: 'Valitse nimi verkkosivustollesi:',
pictures: 'Kuvat',
plural_suffix: 't',
powered_by_puter_js: 'Palvelun tarjoaa {{link=docs}}Puter.js{{/link}}',
-3
View File
@@ -90,7 +90,6 @@ const fil = {
desktop: 'Desktop',
desktop_background_fit: 'Pagkakasya',
developers: 'Mga developer',
dir_published_as_website: 'Na-publish ang %strong% sa:',
disable_2fa: 'I-disable ang 2FA',
disable_2fa_confirm: 'I-disable mo ba ang 2FA?',
disable_2fa_instructions: 'Ilagay ang password mo para i-disable ang 2FA.',
@@ -98,7 +97,6 @@ const fil = {
dont_allow: 'Huwag payagan',
download: 'I-download',
download_file: 'I-download ang file',
downloading: 'Dina-download',
email: 'Email',
email_change_confirmation_sent:
'Napadala na ang confirmation email sa bago mong email. Buksan ang inbox at sundin ang mga hakbang para matapos ang proseso.',
@@ -186,7 +184,6 @@ const fil = {
paste_into_folder: 'I-paste sa folder',
path: 'Lokasyon',
personalization: 'Personalization',
pick_name_for_website: 'Pumili ng pangalan para sa website mo:',
pictures: 'Mga larawan',
plural_suffix: '',
powered_by_puter_js: 'Pinapagana ng {{link=docs}}Puter.js{{/link}}',
-8
View File
@@ -97,7 +97,6 @@ const fr = {
desktop: 'Bureau',
desktop_background_fit: 'Ajuster',
developers: 'Développeurs',
dir_published_as_website: '%strong% a été publié sur :',
disable_2fa: "Désactiver l'A2F",
disable_2fa_confirm: "Êtes-vous sûr de vouloir désactiver l'A2F ?",
disable_2fa_instructions: "Entrez votre mot de passe pour désactiver l'A2F.",
@@ -105,11 +104,7 @@ const fr = {
documents: 'Documents',
dont_allow: 'Ne pas autoriser',
download: 'Télécharger',
confirm_download_file_to_desktop: 'Êtes-vous sûr de vouloir télécharger %% sur votre bureau ?',
download_file: 'Télécharger le fichier',
downloading: 'Téléchargement en cours',
downloading_file: 'Téléchargement de %%',
error_download_failed: 'Échec du téléchargement du fichier',
email: 'E-mail',
email_change_confirmation_sent: 'Un e-mail de confirmation a été envoyé à votre nouvelle adresse e-mail. Veuillez vérifier votre boîte de réception et suivre les instructions pour terminer le processus.',
email_invalid: 'L\'e-mail n\'est pas valide.',
@@ -194,7 +189,6 @@ const fr = {
paste_into_folder: 'Coller dans le dossier',
path: 'Chemin',
personalization: 'Personnalisation',
pick_name_for_website: 'Choisissez un nom pour votre site internet :',
pictures: 'Images',
pixel_ratio: 'Ratio de pixels',
plural_suffix: 's',
@@ -285,7 +279,6 @@ const fr = {
untarring: 'Extraction de %strong% (.tar)',
upload: 'Importer',
uploading: 'Importation en cours',
uploading_file: 'Importation de %%',
upload_here: 'Importer ici',
uptime: 'Temps de disponibilité',
used_of: '{{used}} utilisé sur {{available}}',
@@ -461,7 +454,6 @@ const fr = {
'perm_subdomains_write': 'gérer vos sous-domaines',
'error_user_or_path_not_found': 'Utilisateur ou chemin introuvable.',
'error_invalid_username': 'Nom d\'utilisateur invalide.',
// Auth token
auth_token: 'Jeton d\'authentification',
-3
View File
@@ -91,7 +91,6 @@ const he = {
desktop: 'שולחן העבודה ',
desktop_background_fit: 'התאים',
developers: 'מפתחים',
dir_published_as_website: '%strong% פורסם ל:',
disable_2fa: 'השבתת אימות דו-שלבי',
disable_2fa_confirm: 'האם אתם בטוחים שברצונכם להשבית אימות דו-שלבי?',
disable_2fa_instructions: 'הזינו את הסיסמה שלכם כדי להשבית אימות דו-שלבי.',
@@ -99,7 +98,6 @@ const he = {
dont_allow: 'אל תאפשר',
download: 'הורדה',
download_file: 'הורדת קובץ',
downloading: 'מוריד',
email: 'אימייל',
email_change_confirmation_sent:
'אימייל אישור נשלח לכתובת האימייל החדשה שלך. אנא בדוק את תיבת הדואר הנכנס ופעל לפי ההוראות להשלמת התהליך.',
@@ -186,7 +184,6 @@ const he = {
paste_into_folder: 'הדבק בתיקיה',
path: 'מסלול',
personalization: 'התאמה אישית',
pick_name_for_website: 'בחר שם לאתר האינטרנט שלך:',
pictures: 'תמונות',
plural_suffix: 's',
powered_by_puter_js: 'מונע ע"י {{link=docs}}Puter.js{{/link}}',
-3
View File
@@ -84,7 +84,6 @@ const hi = {
desktop: 'डेस्कटॉप',
desktop_background_fit: 'उपयुक्त',
developers: 'डेवलपर्स',
dir_published_as_website: 'निर्देशिका को एक वेबसाइट के रूप में प्रकाशित किया गया है',
disable_2fa: '2एफए गायब करें',
disable_2fa_confirm: 'क्या आप वाकई 2एफए को गायब करना चाहते हैं?',
disable_2fa_instructions: '2एफए को गायब करने के लिए अपना पासवर्ड दर्ज करें।',
@@ -92,7 +91,6 @@ const hi = {
dont_allow: 'अनुमति न दें',
download: 'डाउनलोड',
download_file: 'डाउनलोड फ़ाइल',
downloading: 'डाउनलोड हो रहा है',
email: 'ईमेल',
email_change_confirmation_sent: 'आपके नए ईमेल पते पर एक पुष्टिकरण ईमेल भेज दिया गया है। कृपया अपना इनबॉक्स जांचें और प्रक्रिया पूरी करने के लिए निर्देशों का पालन करें।',
email_invalid: 'ईमेल अमान्य है।',
@@ -172,7 +170,6 @@ const hi = {
paste_into_folder: 'फ़ोल्डर में पेस्ट करें',
path: 'पथ',
personalization: 'वैयक्तिकरण',
pick_name_for_website: 'अपनी वेबसाइट के लिए एक नाम चुनें:',
pictures: 'चित्रों',
plural_suffix: 'एस',
powered_by_puter_js: '{{link=docs}}Puter.js{{/link}} द्वारा संचालित',
-3
View File
@@ -85,7 +85,6 @@ const hu = {
desktop: 'Asztal',
desktop_background_fit: 'Illesztés',
developers: 'Fejlesztők',
dir_published_as_website: '%strong% közzétéve a következő címen:',
disable_2fa: '2FA letiltása',
disable_2fa_confirm: 'Biztosan letiltod a 2FA-t?',
disable_2fa_instructions: 'Add meg a jelszavad a 2FA letiltásához.',
@@ -93,7 +92,6 @@ const hu = {
dont_allow: 'Nem engedélyez',
download: 'Letöltés',
download_file: 'Fájl letöltése',
downloading: 'Letöltés',
email: 'Email',
email_change_confirmation_sent: 'Megerősítő emailt küldtünk az új email címedre. Kérlek, ellenőrizd a postaládádat és kövesd az utasításokat a folyamat befejezéséhez.',
email_invalid: 'Érvénytelen email cím.',
@@ -173,7 +171,6 @@ const hu = {
paste_into_folder: 'Beillesztés a mappába',
path: 'Útvonal',
personalization: 'Személyre szabás',
pick_name_for_website: 'Válassz nevet a weboldaladnak:',
pictures: 'Képek',
plural_suffix: 'k',
powered_by_puter_js: 'A Puter.js hajtja',
-3
View File
@@ -92,7 +92,6 @@ const hy = {
desktop: 'Աշխատասեղան',
desktop_background_fit: 'Հարմարեցնել',
developers: 'Ծրագրավորողներ',
dir_published_as_website: '%strong% հրապարակվել է',
disable_2fa: 'Անջատել 2FA',
disable_2fa_confirm: 'Վստա՞հ եք, որ ցանկանում եք անջատել 2FA-ն:',
disable_2fa_instructions:
@@ -101,7 +100,6 @@ const hy = {
dont_allow: 'Թույլ չտալ',
download: 'Ներբեռնել',
download_file: 'Ներբեռնել ֆայլը',
downloading: 'Ներբեռնվում է',
email: 'Էլեկտրոնային հասցե',
email_change_confirmation_sent:
'Հաստատման նամակը ուղարկվել է ձեր նոր էլ. հասցեին։ Խնդրում ենք ստուգեք ձեր փոստարկղը և հետևեք ցուցումներին՝ գործընթացը ավարտելու համար:',
@@ -192,7 +190,6 @@ const hy = {
paste_into_folder: 'Տեղադրել պանակում',
path: 'Ուղի',
personalization: 'Անհատականացում',
pick_name_for_website: 'Ընտրեք անուն ձեր կայքի համար',
pictures: 'Նկարներ',
plural_suffix: 'ներ',
powered_by_puter_js: 'Աջակցվում է {{link=docs}}Puter.js{{/link}}-ի կողմից',
-3
View File
@@ -92,7 +92,6 @@ const id = {
desktop: 'Desktop',
desktop_background_fit: 'Cocokkan',
developers: 'Pengembang',
dir_published_as_website: '%strong% telah dipublikasikan di:',
disable_2fa: 'Nonaktifkan 2FA',
disable_2fa_confirm: 'Apakah Anda yakin ingin menonaktifkan 2FA?',
disable_2fa_instructions: 'Masukkan kata sandi Anda untuk menonaktifkan 2FA.',
@@ -100,7 +99,6 @@ const id = {
dont_allow: 'Jangan Izinkan',
download: 'Unduh',
download_file: 'Unduh File',
downloading: 'Mengunduh',
email: 'Email',
email_change_confirmation_sent:
'Email konfirmasi telah dikirim ke alamat email baru Anda. Silakan periksa kotak masuk Anda dan ikuti petunjuk untuk menyelesaikan prosesnya.',
@@ -188,7 +186,6 @@ const id = {
paste_into_folder: 'Tempel ke dalam Folder',
path: 'Lokasi',
personalization: 'Personalisasi',
pick_name_for_website: 'Pilih nama untuk situs web Anda:',
pictures: 'Gambar',
plural_suffix: 's',
powered_by_puter_js: 'Ditenagai oleh {{link=docs}}Puter.js{{/link}}',
-3
View File
@@ -93,7 +93,6 @@ const ig = {
desktop: 'desktọpụ',
desktop_background_fit: 'dabara',
developers: 'Ndị mme ya',
dir_published_as_website: '%strong% e bipụtara ya:',
disable_2fa: 'Gbanyụọ 2FA',
disable_2fa_confirm: "Ị ji n'aka na ịchọrọ gbanyụọ 2FA?",
disable_2fa_instructions: 'Tinye paswọọdụ gị i ji gbanyụọ 2FA.',
@@ -101,7 +100,6 @@ const ig = {
dont_allow: 'Ekwela',
download: 'Budata',
download_file: 'Budata faịlụ',
downloading: 'Nbudata',
email: 'Email',
email_change_confirmation_sent:
'ezipula email nkwenye na adreesị ozi-e ọhụrụ gị. Biko lelee igbe mbata gị ma soro ntuziaka ka ịmechaa usoro ahụ.',
@@ -190,7 +188,6 @@ const ig = {
paste_into_folder: "Tinye n'ime nchekwa",
path: 'ụzọ',
personalization: 'Nhazi onwe',
pick_name_for_website: 'Họrọ aha maka weebụsaịtị gị:',
pictures: 'Foto',
plural_suffix: 's',
powered_by_puter_js: 'Kwadoro site na {{link=docs}}Puter.js{{/link}}',
-3
View File
@@ -93,7 +93,6 @@ const it = {
desktop: 'Scrivania',
desktop_background_fit: 'Adatta',
developers: 'Sviluppatori',
dir_published_as_website: '%strong% è stato pubblicato su:',
disable_2fa: 'Disabilita 2FA',
disable_2fa_confirm: 'Sei sicuro di voler disabilitare la 2FA?',
disable_2fa_instructions:
@@ -102,7 +101,6 @@ const it = {
dont_allow: 'Non consentire',
download: 'Scarica',
download_file: 'Scarica file',
downloading: 'Download in corso',
email: 'Email',
email_change_confirmation_sent:
"Ti abbiamo inviato un'email di conferma. Controlla la tua casella di posta e segui le istruzioni per completare il processo.",
@@ -191,7 +189,6 @@ const it = {
paste_into_folder: 'Incolla nella cartella',
path: 'Percorso',
personalization: 'Personalizzazione',
pick_name_for_website: 'Scegli un nome per il tuo sito web:',
pictures: 'Immagini',
plural_suffix: 'i',
powered_by_puter_js: 'Realizzato con {{link=docs}}Puter.js{{/link}}',
-3
View File
@@ -85,7 +85,6 @@ const ja = {
desktop: 'デスクトップ',
desktop_background_fit: '画面背景をフィット',
developers: '開発者',
dir_published_as_website: '%strong% が公開されました:',
disable_2fa: '2FAを無効にする',
disable_2fa_confirm: '2FAを無効にしてよろしいですか?',
disable_2fa_instructions: 'パスワードを入力して2FAを無効にします。',
@@ -93,7 +92,6 @@ const ja = {
dont_allow: '許可しない',
download: 'ダウンロード',
download_file: 'ファイルをダウンロード',
downloading: 'ダウンロード中',
email: 'メール',
email_change_confirmation_sent: '確認メールが新しいメールアドレスに送信されました。受信トレイを確認し、指示に従って手続きを完了してください。',
email_invalid: 'メールアドレスが無効です。',
@@ -173,7 +171,6 @@ const ja = {
paste_into_folder: 'フォルダーに貼り付け',
path: 'パス',
personalization: 'パーソナライズ',
pick_name_for_website: 'ウェブサイトの名前を選んでください:',
pictures: '写真',
plural_suffix: '',
powered_by_puter_js: '{{link=docs}}Puter.js{{/link}} によって提供されています',
-3
View File
@@ -94,7 +94,6 @@ const ko = {
desktop: '바탕화면',
desktop_background_fit: '맞추기',
developers: '개발자',
dir_published_as_website: '%strong%에 게시되었습니다.',
disable_2fa: '2FA 비활성화',
disable_2fa_confirm: '정말 2FA를 비활성화하시겠습니까?',
disable_2fa_instructions: '2FA를 비활성화하려면 비밀번호를 입력해주세요.',
@@ -102,7 +101,6 @@ const ko = {
dont_allow: '허용하지 않음',
download: '다운로드',
download_file: '파일 다운로드',
downloading: '다운로드 중',
email: '이메일',
email_change_confirmation_sent:
'새 이메일 주소로 확인 메일이 전송되었습니다. 받은 편지함을 확인 후 안내에 따라 절차를 완료해주세요.',
@@ -193,7 +191,6 @@ const ko = {
paste_into_folder: '폴더에 붙여넣기',
path: '경로',
personalization: '개인 설정',
pick_name_for_website: '웹사이트 이름을 선택하세요.',
pictures: '사진',
plural_suffix: '',
powered_by_puter_js: '{{link=docs}}Puter.js{{/link}} 제공',
-3
View File
@@ -92,7 +92,6 @@ const ku = {
desktop: 'سەروروومیزی',
desktop_background_fit: 'ڕێکخستن',
developers: 'پەرەپێدانەران',
dir_published_as_website: '%strong% بەشکراوە بۆ:',
disable_2fa: 'ناچالاککردنی 2FA',
disable_2fa_confirm: 'دڵنیایت کە ئەتەوێت 2FA ناچالاک بکەیت؟',
disable_2fa_instructions: 'وشەی تێپەڕی نووسە بۆ ناچالاککردنی 2FA.',
@@ -100,7 +99,6 @@ const ku = {
dont_allow: 'ڕێگەندان',
download: 'داگرتن',
download_file: 'داگرتنی فایل',
downloading: 'داگرتن',
email: 'ئیمەیل',
email_change_confirmation_sent:
'پەیامی پشتڕاستکردنی ئیمەیل بۆ ناونیشانی ئیمەیلە نوێتەوە نێردرا. تکایە پەیامەکانت بپشکنە و ڕێنیشانەکان پەیڕەو بکە بۆ تەواوبوونەوەی ئەم کارە.',
@@ -189,7 +187,6 @@ const ku = {
paste_into_folder: 'دانان لە فۆڵدەرەکە',
path: 'ڕێکەوت',
personalization: 'بەشداربوون',
pick_name_for_website: 'ناوێک دیاربکە بۆ وێبسایتەکەت:',
pictures: 'وێنەکان',
plural_suffix: 'کان',
powered_by_puter_js: 'پێشکەشکراوە لە {{link=docs}}پوتەر.js{{/link}}',
-3
View File
@@ -86,7 +86,6 @@ const ml = {
desktop: 'ഡെസ്ക്ടോപ്പ്',
desktop_background_fit: 'ഫിറ്റ്',
developers: 'ഡെവലപ്പർമാർ',
dir_published_as_website: '%strong% എന്നതിലേക്ക് പ്രസിദ്ധീകരിച്ചിരിക്കുന്നു:',
disable_2fa: '2FA പ്രവർത്തനരഹിതമാക്കുക',
disable_2fa_confirm: '2FA പ്രവർത്തനരഹിതമാക്കണമെന്ന് തീർച്ചയാണോ?',
disable_2fa_instructions: '2FA പ്രവർത്തനരഹിതമാക്കാൻ നിങ്ങളുടെ പാസ്‌വേഡ് നൽകുക.',
@@ -94,7 +93,6 @@ const ml = {
dont_allow: 'അനുവദിക്കരുത്',
download: 'ഡൗൺലോഡ്',
download_file: 'ഫയൽ ഡൗൺലോഡ് ചെയ്യുക',
downloading: 'ഡൗൺലോഡ് ചെയ്യുന്നു',
email: 'ഇമെയിൽ',
email_change_confirmation_sent: 'നിങ്ങളുടെ പുതിയ ഇമെയിൽ വിലാസത്തിലേക്ക് ഒരു സ്ഥിരീകരണ ഇമെയിൽ അയച്ചിട്ടുണ്ട്. ദയവായി നിങ്ങളുടെ ഇൻബോ',
email_invalid: 'ഇമെയിൽ അസാധുവാണ്.',
@@ -174,7 +172,6 @@ const ml = {
paste_into_folder: 'ഫോൾഡറിലേക്ക് പേസ്റ്റ് ചെയ്യുക',
path: 'പാത',
personalization: 'വ്യക്തിഗതമാക്കൽ',
pick_name_for_website: 'നിങ്ങളുടെ വെബ്സൈറ്റിന് ഒരു പേര് തിരഞ്ഞെടുക്കുക:',
pictures: 'ചിത്രങ്ങൾ',
plural_suffix: 'കൾ',
powered_by_puter_js: '{{link=docs}}Puter.js{{/link}} ഉപയോഗിച്ച് നിർമ്മിച്ചത്',
-3
View File
@@ -105,7 +105,6 @@ const my = {
desktop: 'Desktop',
desktop_background_fit: 'Suaikan',
developers: 'Pembangun',
dir_published_as_website: '%strong% telah diterbitkan di:',
disable_2fa: 'Menyahaktifkan 2FA',
disable_2fa_confirm: 'Adakah anda yakin mahu menyahaktifkan 2FA?',
disable_2fa_instructions: 'Masukkan kata laluan anda untuk menyahaktifkan 2FA.',
@@ -113,7 +112,6 @@ const my = {
dont_allow: 'Tidak Izinkan',
download: 'Muat Naik',
download_file: 'Muat Naik Fail',
downloading: 'Memuat Naik',
email: 'Emel',
email_change_confirmation_sent:
'Emel pengesahan telah dihantar ke alamat emel baharu anda. Sila semak peti emel anda dan ikut arahan terbabit bagi menyelesaikan proses ini.',
@@ -196,7 +194,6 @@ const my = {
paste_into_folder: 'Tampal Ke Dalam Folder',
path: 'Laluan',
personalization: 'Pemperibadian',
pick_name_for_website: 'Pilih nama laman web anda:',
pictures: 'Gambar',
plural_suffix: 's',
powered_by_puter_js: 'Dikuasakan oleh {{link=docs}}Puter.js{{/link}}',
-3
View File
@@ -94,7 +94,6 @@ const nb = {
desktop: 'Skrivebord',
desktop_background_fit: 'Tilpass',
developers: 'Utviklere',
dir_published_as_website: '%strong% er publisert på:',
disable_2fa: 'Deaktiver 2FA',
disable_2fa_confirm: 'Er du sikker på at du vil deaktivere 2FA?',
disable_2fa_instructions: 'Skriv inn passordet ditt for å deaktivere 2FA.',
@@ -102,7 +101,6 @@ const nb = {
dont_allow: 'Ikke tillat',
download: 'Last ned',
download_file: 'Last ned fil',
downloading: 'Laster ned',
email: 'E-post',
email_change_confirmation_sent: 'En bekreftelses-e-post har blitt sendt til din nye e-postadresse. Sjekk innboksen din og følg instruksjonene for å fullføre prosessen.',
email_invalid: 'Ugyldig e-postadresse.',
@@ -182,7 +180,6 @@ const nb = {
paste_into_folder: 'Lim inn i mappe',
path: 'Sti',
personalization: 'Tilpasning',
pick_name_for_website: 'Velg et navn for nettstedet ditt:',
pictures: 'Bilder',
plural_suffix: '',
powered_by_puter_js: 'Drevet av {{link=docs}}Puter.js{{/link}}',
-3
View File
@@ -85,7 +85,6 @@ const nl = {
desktop: 'Bureaublad',
desktop_background_fit: 'Aanpassen',
developers: 'Ontwikkelaars',
dir_published_as_website: '%strong% is gepubliceerd op:',
disable_2fa: '2FA Uitschakelen',
disable_2fa_confirm: 'Weet u zeker dat u 2FA wilt uitschakelen?',
disable_2fa_instructions: 'Voer uw wachtwoord in om 2FA uit te schakelen.',
@@ -93,7 +92,6 @@ const nl = {
dont_allow: 'Niet Toestaan',
download: 'Downloaden',
download_file: 'Bestand Downloaden',
downloading: 'Downloaden',
email: 'E-mail',
email_change_confirmation_sent: 'Er is een bevestigingsmail gestuurd naar uw nieuwe e-mailadres. Controleer uw inbox en volg de instructies om het proces te voltooien.',
email_invalid: 'E-mail is ongeldig.',
@@ -174,7 +172,6 @@ const nl = {
paste_into_folder: 'Plakken in Map',
path: 'Pad',
personalization: 'Personalisatie',
pick_name_for_website: 'Kies een naam voor uw website',
pictures: 'Afbeeldingen',
plural_suffix: 'en',
powered_by_puter_js: 'Aangedreven door {{link=docs}}Puter.js{{/link}}',
-3
View File
@@ -49,9 +49,7 @@ const nn = {
deploy_as_app: 'Distribuer som app',
descending: 'Synkande',
desktop_background_fit: 'Tilpass',
dir_published_as_website: '%strong% er publisert på:',
download: 'Last ned',
downloading: 'Lastar ned',
email: 'E-post',
email_or_username: 'E-post eller brukarnamn',
empty_trash: 'Tøm papirkorg',
@@ -97,7 +95,6 @@ const nn = {
passwords_do_not_match: '`Nytt passord` og `Stadfest nytt passord` stemmer ikkje overeins.',
paste: 'Lim inn',
paste_into_folder: 'Lim inn i mappe',
pick_name_for_website: 'Vel eit namn for nettstaden din:',
powered_by_puter_js: 'Dreve av {{link=docs}}Puter.js{{/link}}',
preparing: 'Førebur…',
preparing_for_upload: 'Førebur opplasting…',
-3
View File
@@ -85,7 +85,6 @@ const pl = {
desktop: 'Pulpit',
desktop_background_fit: 'Dopasowanie',
developers: 'Dla deweloperów',
dir_published_as_website: '%strong% został opublikowany do:',
disable_2fa: 'Wyłącz uwierzytelnianie dwuskładnikowe',
disable_2fa_confirm: 'Czy jesteś pewien że chcesz wyłączyć uwierzytelnianie dwuskładnikowe?',
disable_2fa_instructions: 'Wprowadź swoje hasło aby wyłączyć uwierzytelnianie dwuskładnikowe.',
@@ -93,7 +92,6 @@ const pl = {
dont_allow: 'Nie pozwalaj',
download: 'Pobierz',
download_file: 'Pobierz plik',
downloading: 'Pobieranie',
email: 'Email',
email_change_confirmation_sent: 'Email z potwierdzeniem został wysłany na twój adres. Sprawdź swoją skrzynkę mailową i wykonaj przesłane instrukcje aby zakończyć proces.',
email_invalid: 'Email jest nieprawidłowy.',
@@ -173,7 +171,6 @@ const pl = {
paste_into_folder: 'Wklej do folderu',
path: 'Ścieżka',
personalization: 'Personalizacja',
pick_name_for_website: 'Wybierz nazwę dla swojej strony:',
pictures: 'Obrazy',
plural_suffix: '', //In polish there is a ton of plural suffixes, so I just left it empty
powered_by_puter_js: 'Zasilane za pomocą {{link=docs}}Puter.js{{/link}}',
-3
View File
@@ -86,7 +86,6 @@ const pt = {
desktop: 'Desktop',
desktop_background_fit: 'Ajustado',
developers: 'Desenvolvedores',
dir_published_as_website: '%strong% foi publicado em:',
disable_2fa: 'Disabilitar 2FA',
disable_2fa_confirm: 'Tem acerteza que quer desabilitar a 2FA?',
disable_2fa_instructions: 'Submetar a sua password para desabilitar 2FA.',
@@ -94,7 +93,6 @@ const pt = {
dont_allow: 'Não Permitir',
download: 'Descarregar',
download_file: 'Descarregar Ficheiro',
downloading: 'Fazendo a descarga',
email: 'Email',
email_change_confirmation_sent: 'Um email de confirmação foi enviado para o teu novo endereço de email. Por favor, verifique a sua caixa de entrada e siga as instruções para completar o processo.',
email_invalid: 'O Email que providenciou é invalido.',
@@ -174,7 +172,6 @@ const pt = {
paste_into_folder: 'Cola na Pasta',
path: 'Caminho',
personalization: 'Personalização',
pick_name_for_website: 'Escolha um nome para seu site:',
pictures: 'Imagens',
plural_suffix: 's',
powered_by_puter_js: 'Criado com {{link=docs}}Puter.js{{/link}}',
-3
View File
@@ -86,7 +86,6 @@ const ro = {
desktop: 'Desktop',
desktop_background_fit: 'Potrivește fundalul',
developers: 'Programatori',
dir_published_as_website: '%strong% a fost publicat către:',
disable_2fa: 'Dezactivați 2FA',
disable_2fa_confirm: 'Sunteți sigur(ă) că doriți să dezactivați 2FA?',
disable_2fa_instructions: 'Introduceți parola dumneavoastră pentru a dezactiva 2FA.',
@@ -94,7 +93,6 @@ const ro = {
dont_allow: 'Nu permiteți',
download: 'Descarcă',
download_file: 'Descarcă fișier',
downloading: 'Se descarcă',
email: 'E-mail',
email_change_confirmation_sent: 'Un e-mail de confirmare a fost trimis către noua dumneavoastră adresă de e-mail. Vă rugăm să verificați ©ăsuța și să urmați intrucțiunile pentru a finaliza procesul.',
email_invalid: 'E-mailul este invalid.',
@@ -174,7 +172,6 @@ const ro = {
paste_into_folder: 'Inserează in folder',
path: 'Cale',
personalization: 'Personalizare',
pick_name_for_website: 'Alegeți un nume pentru site-ul dvs:',
pictures: 'Imagini',
plural_suffix: 'e',
powered_by_puter_js: 'Creat de {{link=docs}}Puter.js{{/link}}',
-3
View File
@@ -94,7 +94,6 @@ const ru = {
desktop: 'Рабочий стол',
desktop_background_fit: 'Вместить',
developers: 'Разработчики',
dir_published_as_website: '%strong% опубликован в:',
disable_2fa: 'Отключить двойную аутентификацию',
disable_2fa_confirm:
'Вы уверены, что хотите отключить двойную аутентификацию?',
@@ -104,7 +103,6 @@ const ru = {
dont_allow: 'Доступ запрещён',
download: 'Загрузить',
download_file: 'Загрузить файл',
downloading: 'Загрузка',
email: 'Электронная почта',
email_change_confirmation_sent:
'На ваш новый адрес электронной почты было отправлено письмо с подтверждением. Пожалуйста, проверьте свой ящик электронной почты и следуйте инструкциям, чтобы завершить процесс.',
@@ -194,7 +192,6 @@ const ru = {
paste_into_folder: 'Вставить в папку',
path: 'Путь',
personalization: 'Персонализация',
pick_name_for_website: 'Выберите имя для вашего сайта:',
pictures: 'Изображения',
plural_suffix: 's', //does not exist in Russian language
powered_by_puter_js: 'Создано на {{link=docs}}Puter.js{{/link}}',
-3
View File
@@ -89,7 +89,6 @@ const sl = {
desktop: 'Namizje',
desktop_background_fit: 'Prilagodi',
developers: 'Razvijalci',
dir_published_as_website: '%strong% je bilo objavljeno na:',
disable_2fa: 'Onemogoči 2FA',
disable_2fa_confirm: 'Ali ste prepričani, da želite onemogočiti 2FA?',
disable_2fa_instructions: 'Za onemogočanje 2FA vnesite svoje geslo.',
@@ -97,7 +96,6 @@ const sl = {
dont_allow: 'Ne dovoli',
download: 'Prenos',
download_file: 'Prenesi datoteko',
downloading: 'Prenašanje',
email: 'E-pošta',
email_change_confirmation_sent: 'Potrditveno e-poštno sporočilo je bilo poslano na vaš novi e-poštni naslov. Preverite svoj poštni predal in sledite navodilom za dokončanje postopka.',
email_invalid: 'E-pošta ni veljavna.',
@@ -179,7 +177,6 @@ const sl = {
paste_into_folder: 'Prilepi v mapo',
path: 'Pot',
personalization: 'Prilagajanje',
pick_name_for_website: 'Izberite ime za svoje spletno mesto:',
pictures: 'Slike',
plural_suffix: '', // "i", "je" - masculine; "e", "i" - feminine; "a", "i" - neuter
powered_by_puter_js: 'Poganja {{link=docs}}Puter.js{{/link}}',
-3
View File
@@ -85,7 +85,6 @@ const sv = {
desktop: 'Skrivbord',
desktop_background_fit: 'Anpassa',
developers: 'Utvecklare',
dir_published_as_website: '%strong% är publicerat på:',
disable_2fa: 'Stäng av 2FA',
disable_2fa_confirm: 'Är det säkert att du vill stänga av 2FA?',
disable_2fa_instructions: 'Fyll i ditt lösenord för att stänga av 2FA.',
@@ -93,7 +92,6 @@ const sv = {
dont_allow: 'Tillåt inte',
download: 'Ladda ner',
download_file: 'Ladda ner fil',
downloading: 'Laddar ner',
email: 'E-post',
email_change_confirmation_sent: 'Ett bekräftelsemail har skickats till din nya e-postadress. Var god kontrollera inkorgen och följ instruktionerna för att fullfölja processen.',
email_invalid: 'Adressen är ogiltig.',
@@ -173,7 +171,6 @@ const sv = {
paste_into_folder: 'Klistra in i mapp',
path: 'Filväg',
personalization: 'Personalisering',
pick_name_for_website: 'Välj ett namn för din webbplats:',
pictures: 'Bilder',
plural_suffix: '', // neutrum has "", utrum has "or", "ar", "er"
powered_by_puter_js: 'Drivs av {{link=docs}}Puter.js{{/link}}',
-3
View File
@@ -95,7 +95,6 @@ const ta = {
desktop: 'டெஸ்க்டாப்',
desktop_background_fit: 'பொருத்தம்',
developers: 'டெவலப்பர்கள்',
dir_published_as_website: '%strong% வெளியிடப்பட்டது:',
disable_2fa: '2FA ஐ முடக்கு',
disable_2fa_confirm: '2FA ஐ நிச்சயமாக முடக்க விரும்புகிறீர்களா?',
disable_2fa_instructions: '2FA ஐ முடக்க உங்கள் கடவுச்சொல்லை உள்ளிடவும்.',
@@ -103,7 +102,6 @@ const ta = {
dont_allow: 'அனுமதிக்காதே',
download: 'பதிவிறக்கவும்',
download_file: 'பதிவிறக்க கோப்பு',
downloading: 'பதிவிறக்கிறது',
email: 'மின்னஞ்சல்',
email_change_confirmation_sent:
'உங்கள் புதிய மின்னஞ்சல் முகவரிக்கு உறுதிப்படுத்தல் மின்னஞ்சல் அனுப்பப்பட்டுள்ளது. செயல்முறையை முடிக்க உங்கள் இன்பாக்ஸைச் சரிபார்த்து, வழிமுறைகளைப் பின்பற்றவும்.',
@@ -194,7 +192,6 @@ const ta = {
paste_into_folder: 'கோப்புறையில் ஒட்டவும்',
path: 'பாதை',
personalization: 'தனிப்பயனாக்கம்',
pick_name_for_website: 'உங்கள் வலைத்தளத்திற்கு ஒரு பெயரைத் தேர்ந்தெடுக்கவும்:',
pictures: 'படங்கள்',
plural_suffix: 'கள்',
powered_by_puter_js: 'மூலம் இயக்கப்படுகிறது {{link=docs}}Puter.js{{/link}}',
-3
View File
@@ -84,7 +84,6 @@ const th = {
desktop: 'เดสก์ท็อป',
desktop_background_fit: 'พอดี',
developers: 'นักพัฒนา',
dir_published_as_website: '%strong% ได้รับการเผยแพร่ไปยัง:',
disable_2fa: 'ปิด 2FA',
disable_2fa_confirm: 'คุณแน่ใจว่าต้องการจะปิด 2FA?',
disable_2fa_instructions: 'กรอกรหัสผ่านเพื่อปิด 2FA.',
@@ -92,7 +91,6 @@ const th = {
dont_allow: 'ไม่อนุญาต',
download: 'ดาวน์โหลด',
download_file: 'ดาวน์โหลดไฟล์',
downloading: 'กำลังดาวน์โหลด',
email: 'อีเมล',
email_change_confirmation_sent: 'อีเมล์ยืนยันได้ถูกส่งไปที่อยู่อีเมลใหม่ของคุณแล้ว กรุณาตรวจสอบกล่องข้อความของคุณและทำตามขั้นตอนต่อจนเสร็จ',
email_invalid: 'อีเมล์ไม่ถูกต้อง.',
@@ -171,7 +169,6 @@ const th = {
paste: 'วาง',
paste_into_folder: 'วางลงในโฟลเดอร์',
personalization: 'ปรับเฉพาะบุคคล',
pick_name_for_website: 'เลือกชื่อสำหรับเว็บไซต์ของคุณ:',
pictures: 'รูปภาพ',
plural_suffix: '',
powered_by_puter_js: 'สนับสนุนโดย {{link=docs}}Puter.js{{/link}}',
-3
View File
@@ -84,7 +84,6 @@ const tr = {
desktop: 'Masaüstü',
desktop_background_fit: 'Sığdır',
developers: 'Geliştiriciler',
dir_published_as_website: '%strong% şu adrese yayınlandı:',
disable_2fa: 'İki faktörlü doğrulamayı kapat',
disable_2fa_confirm: 'İki faktörlü doğrulamayı kapatmak istediğinize emin misiniz?',
disable_2fa_instructions: 'İki faktörlü doğrulamayı kapatmak için parolanızı girin.',
@@ -92,7 +91,6 @@ const tr = {
dont_allow: 'İzin Verme',
download: 'İndir',
download_file: 'Dosyayı İndir',
downloading: 'İndiriliyor',
email: 'E-posta',
email_change_confirmation_sent: 'Yeni e-posta adresinize bir onay e-postası gönderilmiştir. Lütfen gelen kutunuzu kontrol edin ve işlemi tamamlamak için talimatları takip edin.',
email_invalid: 'Geçersiz E-Posta',
@@ -172,7 +170,6 @@ const tr = {
paste_into_folder: 'Klasöre Yapıştır',
path: 'Yol',
personalization: 'Kişiselleştirme',
pick_name_for_website: 'Web siteniz için bir ad seçin:',
pictures: 'Resimler',
plural_suffix: '',
powered_by_puter_js: '{{link=docs}}Puter.js{{/link}} tarafından güçlendirilmiştir',
-3
View File
@@ -86,7 +86,6 @@ const ua = {
desktop: 'Робочий стіл',
desktop_background_fit: 'Вмістити',
developers: 'Розробники',
dir_published_as_website: '%strong% опубліковано в:',
disable_2fa: 'Вимкнути двофакторну аутентифікацію',
disable_2fa_confirm: 'Ви впевнені, що хочете вимкнути двофакторну аутентифікацію?',
disable_2fa_instructions: 'Введіть ваш пароль для вимкнення двофакторної аутентифікації',
@@ -94,7 +93,6 @@ const ua = {
dont_allow: 'Не дозволяти',
download: 'Завантажити',
download_file: 'Завантажити Файл',
downloading: 'Завантажується',
email: 'Email',
email_change_confirmation_sent: 'На вашу нову електронну адресу було надіслано листа з підтвердженням. Будь ласка, перевірте свою поштову скриньку і дотримуйтесь інструкцій, щоб завершити процес.',
email_invalid: 'Електронна адреса недійсна.',
@@ -175,7 +173,6 @@ const ua = {
paste_into_folder: 'Вставити в Папку',
path: 'Шлях',
personalization: 'Персоналізація',
pick_name_for_website: "Виберіть ім'я для вашого сайту:",
pictures: 'Зображення',
plural_suffix: 's',
powered_by_puter_js: 'Створено на {{link=docs}}Puter.js{{/link}}',
-3
View File
@@ -88,7 +88,6 @@ const ur = {
desktop: 'ڈیسک ٹاپ',
desktop_background_fit: 'ڈیسک ٹاپ کا پس منظر مناسب ہو',
developers: 'ڈویلپرز',
dir_published_as_website: 'ڈائریکٹری کو ویب سائٹ کے طور پر شائع کیا گیا',
disable_2fa: '2FA کو غیر فعال کریں۔',
disable_2fa_confirm: 'کیا آپ واقعی 2FA کو غیر فعال کرنا چاہتے ہیں؟',
disable_2fa_instructions:
@@ -96,7 +95,6 @@ const ur = {
documents: 'دستاویزات',
download: 'ڈاؤن لوڈ کریں ',
download_file: 'فائل ڈاؤن لوڈ کریں',
downloading: 'ڈاؤن لوڈ ہو رہا ہے ',
email: 'ای میل',
email_change_confirmation_sent:
'ایک تصدیقی ای میل آپ کے نئے ای میل پتے پر بھیج دیا گیا ہے۔ براہ کرم اپنا ان باکس چیک کریں اور عمل کو مکمل کرنے کے لیے ہدایات پر عمل کریں۔',
@@ -176,7 +174,6 @@ const ur = {
paste: 'چسپاں کریں',
paste_into_folder: 'فولڈر میں چسپاں کریں',
personalization: 'متشکل',
pick_name_for_website: 'ویب سائٹ کے لئے نام منتخب کریں ',
pictures: 'تصویریں',
plural_suffix: 'س',
powered_by_puter_js:
-3
View File
@@ -85,7 +85,6 @@ const vi = {
desktop: 'Màn hình chính',
desktop_background_fit: 'Vừa khít',
developers: 'Nhà phát triển',
dir_published_as_website: '%strong% đã được xuất bản tại:',
disable_2fa: 'Tắt 2FA',
disable_2fa_confirm: 'Bạn có chắc chắn muốn tắt 2FA không?',
disable_2fa_instructions: 'Nhập mật khẩu của bạn để tắt 2FA.',
@@ -93,7 +92,6 @@ const vi = {
dont_allow: 'Không cho phép',
download: 'Tải xuống',
download_file: 'Tải xuống tệp',
downloading: 'Đang tải xuống',
email: 'Email',
email_change_confirmation_sent: 'Một email xác nhận đã được gửi đến địa chỉ email mới của bạn. Vui lòng kiểm tra hộp thư đến và làm theo hướng dẫn để hoàn tất quy trình.',
email_invalid: 'Email không hợp lệ.',
@@ -173,7 +171,6 @@ const vi = {
paste_into_folder: 'Dán vào thư mục',
path: 'Đường dẫn',
personalization: 'Cá nhân hóa',
pick_name_for_website: 'Chọn một tên cho trang web của bạn:',
pictures: 'Hình ảnh',
plural_suffix: '',
powered_by_puter_js: 'Được hỗ trợ bởi {{link=docs}}Puter.js{{/link}}',
-3
View File
@@ -85,7 +85,6 @@ const zh = {
desktop: '桌面',
desktop_background_fit: '适合',
developers: '开发人员',
dir_published_as_website: '%strong% 已发布到:',
disable_2fa: '关闭二次认证',
disable_2fa_confirm: '您确定要关闭二次认证?',
disable_2fa_instructions: '请输入您的密码以关闭二次认证',
@@ -93,7 +92,6 @@ const zh = {
dont_allow: '操作不允许!',
download: '下载',
download_file: '下载文件',
downloading: '下载',
email: '邮箱地址',
email_change_confirmation_sent: '确认邮件已发送到您的新邮箱,请按指令完成操作',
email_invalid: '邮箱地址未验证',
@@ -173,7 +171,6 @@ const zh = {
paste_into_folder: '粘贴到文件夹',
path: '路径',
personalization: '个性化',
pick_name_for_website: '为您的网站选择一个名称:',
pictures: '图片',
plural_suffix: '',
powered_by_puter_js: '由 {{link=docs}}Puter.js{{/link}} 提供支持',
-8
View File
@@ -103,7 +103,6 @@ const zhtw = {
desktop: '桌面',
desktop_background_fit: '適應',
developers: '開發者',
dir_published_as_website: '%strong% 已發布到:',
disable_2fa: '停用雙重驗證',
disable_2fa_confirm: '您確定要停用雙重驗證嗎?',
disable_2fa_instructions: '輸入您的密碼以停用雙重驗證。',
@@ -111,7 +110,6 @@ const zhtw = {
dont_allow: '不允許',
download: '下載',
download_file: '下載檔案',
downloading: '正在下載',
email: '電子郵件',
email_change_confirmation_sent: '確認電子郵件已發送到您的新電子郵件地址。請查看您的收件匣並按照指示完成此過程。',
email_invalid: '電子郵件無效。',
@@ -191,7 +189,6 @@ const zhtw = {
paste_into_folder: '貼上到資料夾',
path: '路徑',
personalization: '個人化',
pick_name_for_website: '為您的網站選擇一個名稱:',
pictures: '圖片',
plural_suffix: '個',
powered_by_puter_js: '由 {{link=docs}}Puter.js{{/link}} 提供支援',
@@ -421,20 +418,15 @@ const zhtw = {
'window_title_pictures': "圖片", // In English: "Pictures"
'window_title_puter': "Puter", // In English: "Puter"
'window_folder_empty': "此資料夾是空的", // In English: "This folder is empty"
'confirm_download_file_to_desktop': "您確定要將 %% 下載到您的桌面嗎?", // In English: "Are you sure you want to download %% to your Desktop?"
'downloading_file': "正在下載 %%", // In English: "Downloading %%"
'error_download_failed': "下載檔案失敗", // In English: "Failed to download file"
'Resources': "資源", // In English: "Resources"
'Storage': "儲存空間", // In English: "Storage"
'untar': "解壓縮 Tar", // In English: "Untar"
'untarring': "正在解壓縮 %strong%", // In English: "Untarring %strong%"
'uploading': "正在上傳", // In English: "Uploading"
'uploading_file': "正在上傳 %%", // In English: "Uploading %%"
'tar': "Tar 壓縮", // In English: "Tar"
'download_as_tar': "下載為 Tar", // In English: "Download as Tar"
'tarring': "正在建立 Tar 壓縮檔 %strong%", // In English: "Tarring %strong%"
'error_user_or_path_not_found': "找不到使用者或路徑。", // In English: "User or path not found."
'error_invalid_username': "無效的使用者名稱。", // In English: "Invalid username."
},
};
+19 -30
View File
@@ -121,6 +121,7 @@ import {
runsUserAppTokenExchange,
} from './util/popupAuth.js';
import { verifyOidcPopupReturn } from './util/popupOidcReturn.js';
import { bootsDashboard } from './helpers/bootsDashboard.js';
/**
* A share email names the account it went to. When another account is open,
@@ -135,6 +136,7 @@ const follow_share_recipient = async (action) => {
params: window.url_query_params,
action,
embedded: window.embedded_in_popup || window.is_embedded,
desktop: !window.is_dashboard_mode,
current_user: window.user,
logged_in_users: window.logged_in_users,
});
@@ -1262,14 +1264,10 @@ if (jQuery) {
// alias, and `/desktop` loads the desktop instead. Direct app landings (`/app/<name>`)
// open in the dashboard too: the app comes up maximized in-page with the dashboard
// route slotted underneath (see postAuthActions), so Back minimizes to the dashboard.
// To land the same app on the desktop instead, prefix the path:
// `/desktop/app/<name>` doesn't match the dashboard paths below, so it falls
// through to the desktop.
// URLs that carry a desktop-only flow keep booting the desktop: auth popups
// (`?embedded_in_popup=`), app deep links (`?app=`), direct downloads (`?download=`),
// fullpage mode (`?puter.fullpage=`), and iframe embeds. A share link (`?shared=`,
// (`?embedded_in_popup=`) and iframe embeds. A share link (`?shared=`,
// from an email) lands in the dashboard: Files, on Shared, with the items it names
// picked out; `/desktop?shared=` opens the item on the desktop instead. App metadata
// picked out; the desktop ignores it. App metadata
// like fullpage_on_landing does NOT opt a landing out of the dashboard; it only
// affects boots that still go through the desktop flow.
{
@@ -1288,16 +1286,11 @@ if (jQuery) {
};
// note: iframe detection is inlined because globals.js (window.is_embedded) loads after this module
const in_iframe = window.location !== window.parent.location;
const needs_desktop_at_root =
window.embedded_in_popup ||
in_iframe ||
search_params.has('puter.fullpage') ||
search_params.has('app') ||
search_params.has('download');
const is_dashboard_alias =
pathname === '/dashboard' || pathname === '/dashboard/';
const is_app_landing = /^\/app\/[^/]+\/?$/.test(pathname);
if (is_dashboard_alias || ((pathname === '/' || is_app_landing) && !needs_desktop_at_root)) {
if (bootsDashboard({
pathname,
inIframe: in_iframe,
embeddedInPopup: !!window.embedded_in_popup,
})) {
window.is_dashboard_mode = true;
window.dashboard_initial_route = parseDashboardRoute();
// A share link outranks the hash: Files, on Shared, with what it names
@@ -1533,10 +1526,6 @@ window.initgui = async function (options) {
const oidcChallenge = takeOidcLoginChallenge();
const url = new URL(window.location).href;
window.url = url;
// Route segments with a leading `/desktop` dropped, so the route checks
// downstream (app landings, actions) never have to know about the prefix:
// `/desktop/app/<name>` opens the app on the desktop the same way
// `/app/<name>` opens it in the dashboard.
const url_paths = parse_url_paths(window.location.pathname);
window.url_paths = url_paths;
@@ -1682,10 +1671,12 @@ window.initgui = async function (options) {
}
//--------------------------------------------------------------------------------------
// Determine if we are in full-page mode
// i.e. https://puter.com/app/<app_name>/?puter.fullpage=true
// Determine if we are in full-page mode. The dashboard always shows apps full
// page; a desktop boot (embeds, popups) can ask with ?puter.fullpage=true.
//--------------------------------------------------------------------------------------
if (
if (window.is_dashboard_mode) {
window.is_fullpage_mode = true;
} else if (
window.url_query_params.has('puter.fullpage') &&
(window.url_query_params.get('puter.fullpage') === 'false' ||
window.url_query_params.get('puter.fullpage') === '0')
@@ -1698,8 +1689,6 @@ window.initgui = async function (options) {
) {
// Puter is in fullpage mode.
window.is_fullpage_mode = true;
} else if (window.is_dashboard_mode) {
window.is_fullpage_mode = true;
}
// Launch services before any UI is rendered
@@ -2598,12 +2587,12 @@ window.initgui = async function (options) {
window.update_sites_cache();
}
}
// App landing pages (`/app/<name>`, incl. `/desktop/app/<name>`) require a
// real account even on a first visit — never a temp user. So does a share
// link: a share only ever reaches a real account, so a temporary one could
// never see what it points at.
// App landing pages (`/app/<name>`) require a real account even on a first
// visit — never a temp user. So does a share link on the dashboard: a share
// only ever reaches a real account, so a temporary one could never see what
// it points at.
const is_app_landing_page = window.url_paths[0] === 'app' && !!window.url_paths[1];
const is_share_link = window.url_query_params.has('shared');
const is_share_link = !!window.is_dashboard_mode && window.url_query_params.has('shared');
const needsSignIn =
!window.is_auth() &&
(!window.first_visit_ever || window.disable_temp_users || is_app_landing_page || is_share_link);
+20 -8
View File
@@ -317,7 +317,7 @@ describe('kv.get GUI boot cache', () => {
result: body.args.key.map((key) => `${key}-value`),
});
await expect(kv.get('sidebar_items')).resolves.toBe('sidebar_items-value');
await expect(kv.get('menubar_style')).resolves.toBe('menubar_style-value');
await expect(kv.get('window_sidebar_width')).resolves.toBe('window_sidebar_width-value');
expect(FakeXHR.requests).toHaveLength(1);
const body = lastBody();
expect(body.method).toBe('get');
@@ -325,6 +325,18 @@ describe('kv.get GUI boot cache', () => {
expect(body.args.key).toContain('sidebar_items');
});
it('leaves keys the GUI no longer reads out of the batch', async () => {
FakeXHR.respondWith = (body) => ({
success: true,
result: body.args.key.map(() => null),
});
await kv.get('sidebar_items');
expect(lastBody().args.key).not.toContain('desktop_icons_hidden');
expect(lastBody().args.key).not.toContain('menubar_style');
expect(lastBody().args.key).not.toContain('user_preferences.clock_visible');
expect(lastBody().args.key).not.toContain('has_set_default_app_user_permissions');
});
it('a failed batch makes get(bootKey) issue a normal call', async () => {
FakeXHR.respondWith = (body) =>
Array.isArray(body.args.key)
@@ -387,24 +399,24 @@ describe('kv.get GUI boot cache', () => {
['expireAt(key, timestamp)', () => kv.expireAt('sidebar_items', 1)],
['flush()', () => kv.flush()],
])('%s makes the next get(key) read the store', async (_label, write) => {
await expect(kv.get('menubar_style')).resolves.toBe('stale');
await expect(kv.get('window_sidebar_width')).resolves.toBe('stale');
await write();
await expect(kv.get('sidebar_items')).resolves.toBe('fresh');
expect(lastBody()).toMatchObject({ method: 'get', args: { key: 'sidebar_items' } });
});
it('keeps serving the boot keys that were not written', async () => {
await kv.get('menubar_style');
await kv.get('window_sidebar_width');
await kv.set('sidebar_items', 'v');
const requests = FakeXHR.requests.length;
await expect(kv.get('menubar_style')).resolves.toBe('stale');
await expect(kv.get('window_sidebar_width')).resolves.toBe('stale');
expect(FakeXHR.requests).toHaveLength(requests);
});
it('a write before the batch is fetched still bypasses it for that key', async () => {
await kv.set('sidebar_items', 'v');
await expect(kv.get('sidebar_items')).resolves.toBe('fresh');
await expect(kv.get('menubar_style')).resolves.toBe('stale');
await expect(kv.get('window_sidebar_width')).resolves.toBe('stale');
});
it('a write issued while the batch is in flight bypasses it', async () => {
@@ -415,7 +427,7 @@ describe('kv.get GUI boot cache', () => {
});
it('a rejected write still bypasses the batch', async () => {
await kv.get('menubar_style');
await kv.get('window_sidebar_width');
FakeXHR.respondWith = (body) => body.method === 'set'
? { success: false, error: { code: 'insufficient_funds' } }
: { success: true, result: 'fresh' };
@@ -434,11 +446,11 @@ describe('kv.get boot keys outside the GUI', () => {
FakeXHR.respondWith = (body) => ({ success: true, result: `${body.args.key}-value` });
await expect(kv.get('sidebar_items')).resolves.toBe('sidebar_items-value');
await expect(kv.get('sidebar_items')).resolves.toBe('sidebar_items-value');
await expect(kv.get('menubar_style')).resolves.toBe('menubar_style-value');
await expect(kv.get('window_sidebar_width')).resolves.toBe('window_sidebar_width-value');
expect(FakeXHR.requests.map((xhr) => JSON.parse(xhr.requestBody).args)).toEqual([
{ key: 'sidebar_items' },
{ key: 'sidebar_items' },
{ key: 'menubar_style' },
{ key: 'window_sidebar_width' },
]);
},
);
@@ -4,16 +4,12 @@ import { driverCallEnvelope } from '../../../lib/networkUtils.js';
// a single batched driver call fetching all of them, so the desktop doesn't
// issue a dozen round-trips while it initializes.
const GUI_CACHE_KEYS = [
'has_set_default_app_user_permissions',
'window_sidebar_width',
'sidebar_items',
'menubar_style',
'user_preferences.auto_arrange_desktop',
'user_preferences.show_hidden_files',
'user_preferences.language',
'user_preferences.clock_visible',
'desktop_item_positions',
'desktop_icons_hidden',
];
// How long the resolved batch keeps serving reads before the cache disables
+6 -6
View File
@@ -880,9 +880,9 @@ export default suite('kv', {
// The GUI batch-reads a fixed set of keys while it boots. No runner is
// the GUI, so here those names are ordinary keys.
'get of a GUI boot key returns the stored value': async (t) => {
await t.puter.kv.set('menubar_style', 'system');
t.assert.equal(await t.puter.kv.get('menubar_style'), 'system');
t.assert.equal(await t.puter.kv.get('menubar_style'), 'system');
await t.puter.kv.set('user_preferences.auto_arrange_desktop', 'true');
t.assert.equal(await t.puter.kv.get('user_preferences.auto_arrange_desktop'), 'true');
t.assert.equal(await t.puter.kv.get('user_preferences.auto_arrange_desktop'), 'true');
t.assert.equal(
await t.puter.kv.get('window_sidebar_width'),
null,
@@ -891,12 +891,12 @@ export default suite('kv', {
},
'writes to a GUI boot key are visible to the next get': async (t) => {
await t.puter.kv.set('desktop_icons_hidden', 'true');
await t.puter.kv.set('sidebar_items', '[]');
t.assert.equal(await t.puter.kv.get('desktop_item_positions'), null);
await t.puter.kv.set('desktop_item_positions', '{}');
t.assert.equal(await t.puter.kv.get('desktop_item_positions'), '{}');
await t.puter.kv.del('desktop_icons_hidden');
t.assert.equal(await t.puter.kv.get('desktop_icons_hidden'), null);
await t.puter.kv.del('sidebar_items');
t.assert.equal(await t.puter.kv.get('sidebar_items'), null);
},
'a GUI boot key written by another client reads fresh': async (t) => {