mirror of
https://github.com/HeyPuter/puter.git
synced 2026-11-13 20:12:57 -10:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a299872bcc | ||
|
|
d374cdb04f | ||
|
|
2d13b73b04 | ||
|
|
8518023b0b | ||
|
|
33988ac84c |
No files matched your search
+1
-1
@@ -40,7 +40,7 @@ The following have already been reviewed and determined **not to be vulnerabilit
|
||||
- **SSRF via `secureFetch`.** Production routes outbound requests through an isolated proxy that has no access to internal/SSRF-sensitive resources.
|
||||
- **Attacks that depend on guessing an `appInstanceID` or app UID.** These are random 128-bit secret values and are not considered guessable.
|
||||
- **Apps invoking drivers, creating workers, or using KV.** Applications are intended to do this; worker permissions are scoped to the owning app. This is by design.
|
||||
- **App metadata or app user-count "leaks".** This information is currently public by design. This covers **private apps** too: private apps are an unreleased, admin-only beta for paid listings in the Puter App Center. Their `index_url` and other metadata are not secret, the flag is not user-settable, and no production app relies on it. Reports that a private app's metadata is reachable through a sibling route (taskbar items, `/get-launch-apps`, `index_url` drift on the hosting gate, etc.) are normalization work, not vulnerabilities.
|
||||
- **App metadata or app user-count "leaks".** This information is currently public by design. This covers **private apps** too: private apps are an unreleased, admin-only beta for paid listings in the Puter App Center. Their `index_url` and other metadata are not secret, the flag is not user-settable, and no production app relies on it. Reports that a private app's metadata is reachable through a sibling route (`/get-launch-apps`, `index_url` drift on the hosting gate, etc.) are normalization work, not vulnerabilities.
|
||||
- **Worker hot-reload driven by a share recipient.** Sharing a worker's source file with write access is how you let a collaborator change that worker's code and have it deploy live. The redeploy running as the file owner, with the owner's worker credentials, is the intended behavior of that grant — the owner chose to hand out write access to executable code. If you do not want that, do not share the source file with write access.
|
||||
- **Re-claiming a site or worker name its owner deleted.** A hosted site or worker name that its owner deletes is treated as abandoned by that owner. If one of their apps still points at it, whoever claims the name next serves that app's `index_url` and receives what the app is launched with, and a new app on the same origin may share the old one's identity. Guarding app identities against being recycled this way is ongoing hardening, not a security boundary, so new ways to re-claim an abandoned name, or to inherit an app's tokens or data through one, are not eligible.
|
||||
- **Share-mode downgrades that do not cascade.** Demoting a delegate does not retroactively revoke the grants that delegate already issued downstream. Shares are independent grants once issued, not a live-evaluated chain. Revoking the downstream grant is a separate, supported action.
|
||||
|
||||
@@ -257,8 +257,21 @@ describe('whoami extension — handleWhoami', () => {
|
||||
expect(body.created_ts).toBeGreaterThan(0);
|
||||
// `directories` is only sent to user actors — confirm it’s present.
|
||||
expect(body.directories).toBeDefined();
|
||||
// taskbar_items is only sent to user actors.
|
||||
expect(body).toHaveProperty('taskbar_items');
|
||||
});
|
||||
|
||||
it('leaves out taskbar items saved on the account', async () => {
|
||||
const user = await seedUser();
|
||||
await server.stores.user.update(user.id as number, {
|
||||
taskbar_items: JSON.stringify([{ type: 'app', name: 'editor' }]),
|
||||
});
|
||||
const { res, captured } = makeRes();
|
||||
|
||||
await runWithContext(
|
||||
{ actor: { user: { uuid: user.uuid, id: user.id as number } } },
|
||||
() => handleWhoami(makeReq(), res),
|
||||
);
|
||||
|
||||
expect(captured.body).not.toHaveProperty('taskbar_items');
|
||||
});
|
||||
|
||||
it('leaves out a desktop background saved on the account', async () => {
|
||||
@@ -449,7 +462,7 @@ describe('whoami extension — handleWhoami', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot add taskbar_items for an app actor, which core omits', async () => {
|
||||
it('cannot add directories for an app actor, which core omits', async () => {
|
||||
const user = await seedUser();
|
||||
const body = await whoamiWithListener(
|
||||
makeActor({
|
||||
@@ -457,11 +470,11 @@ describe('whoami extension — handleWhoami', () => {
|
||||
app: { uid: 'app-test-actor' },
|
||||
}),
|
||||
(_key, event) => {
|
||||
event.details.taskbar_items = [{ name: 'injected' }];
|
||||
event.details.directories = { '/injected': 'uuid' };
|
||||
},
|
||||
);
|
||||
|
||||
expect(body).not.toHaveProperty('taskbar_items');
|
||||
expect(body).not.toHaveProperty('directories');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -542,7 +555,7 @@ describe('whoami extension — handleWhoami', () => {
|
||||
expect(body.otp_secret).toBeUndefined();
|
||||
expect(body.signup_ip).toBeUndefined();
|
||||
// Not just absent as a key — the values must not appear anywhere
|
||||
// in the payload (nested under metadata, taskbar items, …).
|
||||
// in the payload (nested under metadata, …).
|
||||
const serialized = JSON.stringify(body);
|
||||
expect(serialized).not.toContain('5551234567');
|
||||
expect(serialized).not.toContain('fp_ABC123');
|
||||
|
||||
+1
-11
@@ -1,6 +1,5 @@
|
||||
import { Context } from '@heyputer/backend/src/core';
|
||||
import { extension } from '@heyputer/backend/src/extensions';
|
||||
import { APP_ICON_SIZES } from '@heyputer/backend/src/util/appIcon.js';
|
||||
import {
|
||||
buildUserDetails,
|
||||
scrubSensitive,
|
||||
@@ -54,19 +53,10 @@ export const handleWhoami = async (
|
||||
return;
|
||||
}
|
||||
|
||||
const rawIconSize =
|
||||
typeof req.query?.icon_size === 'string'
|
||||
? Number(req.query.icon_size)
|
||||
: undefined;
|
||||
const iconSize =
|
||||
rawIconSize !== undefined && APP_ICON_SIZES.includes(rawIconSize)
|
||||
? rawIconSize
|
||||
: undefined;
|
||||
|
||||
const details = await buildUserDetails(
|
||||
user,
|
||||
{ config: extension.config, clients, stores, services },
|
||||
{ isUser, iconSize, noIcons: !iconSize },
|
||||
{ isUser },
|
||||
);
|
||||
details.human_readable_age = user.timestamp
|
||||
? timeago.format(new Date(user.timestamp as string))
|
||||
|
||||
@@ -52,8 +52,8 @@ const APP_READ_LIMIT = {
|
||||
/**
|
||||
* Unauthenticated icon serving; no actor to key on, so the bucket is the
|
||||
* address — and an address is a NAT, a campus or a carrier gateway that can
|
||||
* hold hundreds of desktops. Each desktop boot pulls the taskbar's icons at
|
||||
* once, so a single burst from one network is already thousands of requests.
|
||||
* hold hundreds of desktops. Each dashboard load pulls its app icons at once,
|
||||
* so a single burst from one network is already thousands of requests.
|
||||
* Responses are publicly cacheable and usually a redirect, so the ceiling is
|
||||
* not protecting bandwidth; it is there so a client looping on a broken icon
|
||||
* can't spin unbounded.
|
||||
@@ -523,7 +523,7 @@ export class AppController extends PuterController {
|
||||
};
|
||||
|
||||
// Icons are <img src> targets from the GUI (root) AND resolved via
|
||||
// api_base_url in taskbar payloads. Register on both so either origin
|
||||
// api_base_url in app summaries. Register on both so either origin
|
||||
// works without a cross-subdomain redirect.
|
||||
router.get(
|
||||
'/app-icon/:app_uid',
|
||||
|
||||
@@ -1271,7 +1271,7 @@ describe('AppController app-icon rate limit', () => {
|
||||
it('sizes the icon bucket for a whole network rather than one desktop', () => {
|
||||
// Icons are unauthenticated `<img src>` targets, so the only key is
|
||||
// the address — which one NAT shares across every desktop behind it,
|
||||
// and each boot pulls the taskbar's icons in a burst.
|
||||
// and each dashboard load pulls its app icons in a burst.
|
||||
for (const path of ['/app-icon/:app_uid', '/app-icon/:app_uid/:size']) {
|
||||
const route = router.routes.find(
|
||||
(r) => r.method === 'get' && r.path === path,
|
||||
|
||||
@@ -1416,7 +1416,7 @@ describe('AuthController.handleLogin', () => {
|
||||
is_user_token: true,
|
||||
});
|
||||
expect(body.user.directories).toBeTypeOf('object');
|
||||
expect(Array.isArray(body.user.taskbar_items)).toBe(true);
|
||||
expect(body.user).not.toHaveProperty('taskbar_items');
|
||||
});
|
||||
|
||||
// A deleted row still answering from cache reaches the session INSERT.
|
||||
|
||||
@@ -25,17 +25,13 @@ const ALLOWED_SORT_BY = ['name', 'size', 'modified', 'type'];
|
||||
const ALLOWED_SORT_ORDER = ['asc', 'desc'];
|
||||
|
||||
/**
|
||||
* Desktop/UI preference routes.
|
||||
*
|
||||
* Two categories:
|
||||
*
|
||||
* - User-level: taskbar items (UserStore)
|
||||
* - Folder-level: layout, sort_by/sort_order (fsentries table)
|
||||
* Folder view preference routes: layout and sort_by/sort_order, stored on the
|
||||
* folder's fsentries row.
|
||||
*/
|
||||
/**
|
||||
* Desktop preference writes — taskbar, layout, sort order. Each persists on
|
||||
* every call, and the GUI fires them on direct user action, so a per-minute
|
||||
* ceiling well above human speed is enough to catch a stuck client.
|
||||
* Desktop preference writes — layout, sort order. Each persists on every call,
|
||||
* and the GUI fires them on direct user action, so a per-minute ceiling well
|
||||
* above human speed is enough to catch a stuck client.
|
||||
*/
|
||||
const PREFERENCE_WRITE_LIMIT = {
|
||||
scope: 'desktop-preference',
|
||||
@@ -46,33 +42,6 @@ const PREFERENCE_WRITE_LIMIT = {
|
||||
|
||||
export class DesktopController extends PuterController {
|
||||
registerRoutes(router) {
|
||||
// -- Taskbar items -------------------------------------------
|
||||
|
||||
router.post(
|
||||
'/update-taskbar-items',
|
||||
{
|
||||
subdomain: 'api',
|
||||
requireUserActor: true,
|
||||
allowFullAccessToken: true,
|
||||
rateLimit: PREFERENCE_WRITE_LIMIT,
|
||||
},
|
||||
async (req, res) => {
|
||||
const { items } = req.body ?? {};
|
||||
if (!Array.isArray(items)) {
|
||||
throw new HttpError(
|
||||
400,
|
||||
'Missing or invalid `items` array',
|
||||
{ legacyCode: 'bad_request' },
|
||||
);
|
||||
}
|
||||
|
||||
await this.stores.user.update(req.actor.user.id, {
|
||||
taskbar_items: JSON.stringify(items),
|
||||
});
|
||||
res.json({});
|
||||
},
|
||||
);
|
||||
|
||||
// -- Folder layout -------------------------------------------
|
||||
|
||||
router.post(
|
||||
|
||||
@@ -136,50 +136,11 @@ describe('DesktopController routes', () => {
|
||||
router.routes.some((r) => r.path === '/set-desktop-bg'),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ── /update-taskbar-items ───────────────────────────────────────────
|
||||
|
||||
describe('DesktopController POST /update-taskbar-items', () => {
|
||||
it('persists items as a JSON-encoded string', async () => {
|
||||
const { actor, userId } = await makeUser();
|
||||
const items = [{ name: 'editor' }, { name: 'browser' }];
|
||||
const { res } = makeRes();
|
||||
await callRoute(
|
||||
'post',
|
||||
'/update-taskbar-items',
|
||||
makeReq({ body: { items }, actor }),
|
||||
res,
|
||||
);
|
||||
|
||||
const refreshed = await server.stores.user.getById(userId);
|
||||
expect(refreshed?.taskbar_items).toBe(JSON.stringify(items));
|
||||
});
|
||||
|
||||
it('throws 400 when items is missing', async () => {
|
||||
const { actor } = await makeUser();
|
||||
const { res } = makeRes();
|
||||
await expect(
|
||||
callRoute(
|
||||
'post',
|
||||
'/update-taskbar-items',
|
||||
makeReq({ body: {}, actor }),
|
||||
res,
|
||||
),
|
||||
).rejects.toMatchObject({ statusCode: 400 });
|
||||
});
|
||||
|
||||
it('throws 400 when items is not an array', async () => {
|
||||
const { actor } = await makeUser();
|
||||
const { res } = makeRes();
|
||||
await expect(
|
||||
callRoute(
|
||||
'post',
|
||||
'/update-taskbar-items',
|
||||
makeReq({ body: { items: 'oops' }, actor }),
|
||||
res,
|
||||
),
|
||||
).rejects.toMatchObject({ statusCode: 400 });
|
||||
it('has no taskbar items route', () => {
|
||||
expect(
|
||||
router.routes.some((r) => r.path === '/update-taskbar-items'),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -5111,9 +5111,8 @@ describe('LegacyFSController.updateFsentryThumbnail', () => {
|
||||
// ── GET /get-launch-apps ────────────────────────────────────────────
|
||||
//
|
||||
// `recent` is a launch-metadata producer: it returns `index_url` for
|
||||
// apps the user has opened before. The taskbar happens to launch these
|
||||
// by name (so AppDriver's hosted-backing guard applies), but the field
|
||||
// is served regardless, so the guard is applied here too.
|
||||
// apps the user has opened before. The field is served whoever reads it,
|
||||
// so AppDriver's hosted-backing guard is applied here too.
|
||||
|
||||
describe('LegacyFSController GET /get-launch-apps', () => {
|
||||
const hostedUrl = (sub: string) => `https://${sub}.site.puter.localhost/`;
|
||||
|
||||
@@ -401,11 +401,9 @@ export class LegacyFSController extends PuterController {
|
||||
.filter(Boolean);
|
||||
|
||||
// Don't hand out an index_url whose puter-hosted backing is
|
||||
// gone or reclaimed. The taskbar launches recents by name (so
|
||||
// AppDriver's guard applies), but this list is a
|
||||
// launch-metadata producer like any other — a future consumer
|
||||
// reading index_url straight off it shouldn't inherit a stale
|
||||
// origin.
|
||||
// gone or reclaimed. This list is a launch-metadata producer
|
||||
// like any other — a consumer reading index_url straight off
|
||||
// it shouldn't inherit a stale origin.
|
||||
let backingGoneFlags: boolean[];
|
||||
try {
|
||||
backingGoneFlags =
|
||||
@@ -950,9 +948,7 @@ export class LegacyFSController extends PuterController {
|
||||
// Trash, and `null`/`{}` when restoring. See
|
||||
// `src/gui/src/helpers.js` → `window.move_items`.
|
||||
newMetadata: (body.new_metadata ?? undefined) as
|
||||
| Record<string, unknown>
|
||||
| null
|
||||
| undefined,
|
||||
Record<string, unknown> | null | undefined,
|
||||
});
|
||||
const overwrittenEntry = replacedByMove(overwriteTarget, moved, name);
|
||||
const oldPath = source.path;
|
||||
@@ -1426,8 +1422,7 @@ export class LegacyFSController extends PuterController {
|
||||
}
|
||||
|
||||
type SignedOrEmpty =
|
||||
| (SignedFile & { path?: string })
|
||||
| Record<string, never>;
|
||||
(SignedFile & { path?: string }) | Record<string, never>;
|
||||
const result: { signatures: SignedOrEmpty[]; token?: string } = {
|
||||
signatures: [],
|
||||
};
|
||||
@@ -2054,10 +2049,7 @@ export class LegacyFSController extends PuterController {
|
||||
const subjectRef = body.subject;
|
||||
const appRef = body.app;
|
||||
const mode = (getString(body, 'mode') ?? 'read') as
|
||||
| 'see'
|
||||
| 'list'
|
||||
| 'read'
|
||||
| 'write';
|
||||
'see' | 'list' | 'read' | 'write';
|
||||
if (!subjectRef || !appRef)
|
||||
throw new HttpError(400, '`subject` and `app` are required', {
|
||||
legacyCode: 'bad_request',
|
||||
|
||||
@@ -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,23 +222,56 @@ describe('HomepageController shell routes', () => {
|
||||
expect(String(captured.body)).toMatch(/<!DOCTYPE html>/i);
|
||||
});
|
||||
|
||||
it('serves the shell on /settings, /dashboard, /desktop, /action, /@:username', async () => {
|
||||
for (const path of [
|
||||
'/settings',
|
||||
'/settings/*splat',
|
||||
'/dashboard',
|
||||
'/dashboard/',
|
||||
'/desktop',
|
||||
'/desktop/',
|
||||
'/action/*splat',
|
||||
'/@:username',
|
||||
]) {
|
||||
it('serves the shell on /dashboard and /action', async () => {
|
||||
for (const path of ['/dashboard', '/dashboard/', '/action/*splat']) {
|
||||
const { res, captured } = makeRes();
|
||||
await callRoute('get', path, makeReq({ path }), res);
|
||||
expect(typeof captured.body).toBe('string');
|
||||
expect(String(captured.body)).toMatch(/<!DOCTYPE html>/i);
|
||||
}
|
||||
});
|
||||
|
||||
it('redirects /desktop to the root, keeping the query', async () => {
|
||||
for (const path of ['/desktop', '/desktop/']) {
|
||||
const { res, captured } = makeRes();
|
||||
await callRoute(
|
||||
'get',
|
||||
path,
|
||||
makeReq({ path, originalUrl: `${path}?ref=abc&x=1` }),
|
||||
res,
|
||||
);
|
||||
expect(captured.statusCode).toBe(302);
|
||||
expect(captured.location).toBe('/?ref=abc&x=1');
|
||||
expect(captured.body).toBeUndefined();
|
||||
}
|
||||
|
||||
const { res, captured } = makeRes();
|
||||
await callRoute('get', '/desktop', makeReq({ path: '/desktop' }), res);
|
||||
expect(captured.location).toBe('/');
|
||||
});
|
||||
|
||||
it('redirects /settings to the dashboard Home tab', async () => {
|
||||
for (const path of ['/settings', '/settings/*splat']) {
|
||||
const { res, captured } = makeRes();
|
||||
await callRoute('get', path, makeReq({ path }), res);
|
||||
expect(captured.statusCode).toBe(302);
|
||||
expect(captured.location).toBe('/dashboard#home');
|
||||
expect(captured.body).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
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 +401,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 +449,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',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -263,23 +263,42 @@ export class HomepageController extends PuterController {
|
||||
});
|
||||
}
|
||||
|
||||
router.get('/settings', {}, (req, res) => sendShell(req, res));
|
||||
router.get('/settings/*splat', {}, (req, res) => sendShell(req, res));
|
||||
// Old settings links land on the dashboard's Home tab.
|
||||
router.get('/settings', {}, (_req, res) =>
|
||||
res.redirect('/dashboard#home'),
|
||||
);
|
||||
router.get('/settings/*splat', {}, (_req, res) =>
|
||||
res.redirect('/dashboard#home'),
|
||||
);
|
||||
|
||||
router.get('/dashboard', {}, (req, res) => sendShell(req, res));
|
||||
router.get('/dashboard/', {}, (req, res) => sendShell(req, res));
|
||||
|
||||
router.get('/desktop', {}, (req, res) => sendShell(req, res));
|
||||
router.get('/desktop/', {}, (req, res) => sendShell(req, res));
|
||||
const queryOf = (req: express.Request) => {
|
||||
const separator = req.originalUrl.indexOf('?');
|
||||
return separator === -1 ? '' : req.originalUrl.slice(separator);
|
||||
};
|
||||
|
||||
// Old desktop links land on the dashboard, query intact.
|
||||
router.get('/desktop', {}, (req, res) =>
|
||||
res.redirect(`/${queryOf(req)}`),
|
||||
);
|
||||
router.get('/desktop/', {}, (req, res) =>
|
||||
res.redirect(`/${queryOf(req)}`),
|
||||
);
|
||||
|
||||
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 name = encodeURIComponent(String(req.params.name ?? ''));
|
||||
res.redirect(`/app/${name}${queryOf(req)}`);
|
||||
});
|
||||
|
||||
// -- /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 +353,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,8 @@ describe('OIDCController GET /auth/oidc/:provider/start', () => {
|
||||
'//evil.test',
|
||||
'/settings',
|
||||
`/app/${'a'.repeat(101)}`,
|
||||
'/desktop',
|
||||
'/desktop/app/my-App_2',
|
||||
'/desktop/app/evil/extra',
|
||||
'/desktop/app/',
|
||||
'/dashboard/app/name',
|
||||
@@ -476,11 +458,11 @@ describe('OIDCController GET /auth/oidc/:provider/start', () => {
|
||||
sharedPath('b.txt'),
|
||||
sharedPath('a.txt'),
|
||||
],
|
||||
'/desktop',
|
||||
'/dashboard',
|
||||
),
|
||||
);
|
||||
const url = new URL(uri);
|
||||
expect(url.origin + url.pathname).toBe(`${TEST_ORIGIN}/desktop`);
|
||||
expect(url.origin + url.pathname).toBe(`${TEST_ORIGIN}/dashboard`);
|
||||
expect(url.searchParams.getAll('shared')).toEqual([
|
||||
sharedPath('a.txt'),
|
||||
sharedPath('b.txt'),
|
||||
|
||||
@@ -102,16 +102,14 @@ 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).
|
||||
// /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)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -438,9 +436,8 @@ export class OIDCController extends PuterController {
|
||||
|
||||
let appRedirectUri = flowRedirects[flow] ?? (origin || '/');
|
||||
|
||||
// Optional GUI return path so login started from /desktop,
|
||||
// /dashboard, an /app/<name> landing, or a share link lands
|
||||
// back there.
|
||||
// Optional GUI return path so login started from /dashboard,
|
||||
// an /app/<name> landing, or a share link lands back there.
|
||||
const rawReturnTo = Array.isArray(req.query.return_to)
|
||||
? req.query.return_to[0]
|
||||
: req.query.return_to;
|
||||
|
||||
@@ -314,8 +314,8 @@ describe('OIDC account second-factor login', () => {
|
||||
|
||||
it('preserves ordinary OIDC login for accounts without 2FA', async () => {
|
||||
await account(false);
|
||||
const response = await callback('login', { return_to: '/desktop' });
|
||||
expect(response.headers.get('location')).toBe(`${env.origin}/desktop`);
|
||||
const response = await callback('login', { return_to: '/dashboard' });
|
||||
expect(response.headers.get('location')).toBe(`${env.origin}/dashboard`);
|
||||
expect(
|
||||
response.headers.get('set-cookie')?.includes('puter_auth_token='),
|
||||
).toBe(true);
|
||||
|
||||
@@ -159,8 +159,8 @@ export interface AppSummaryOptions {
|
||||
|
||||
/**
|
||||
* The summary the desktop launches from directly, without re-reading the app
|
||||
* (suggested, recommended, recent and taskbar apps), so every launch-relevant
|
||||
* flag belongs here.
|
||||
* (suggested, recommended and recent apps), so every launch-relevant flag
|
||||
* belongs here.
|
||||
*/
|
||||
export function toAppSummary(
|
||||
app: AppRow,
|
||||
|
||||
@@ -1,271 +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 { v4 as uuidv4 } from 'uuid';
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
|
||||
import { PuterServer } from '../server.ts';
|
||||
import { setupTestServer } from '../testUtil.ts';
|
||||
import { getTaskbarItems } from './taskbarItems.ts';
|
||||
|
||||
describe('getTaskbarItems', () => {
|
||||
let server: PuterServer;
|
||||
let deps: Parameters<typeof getTaskbarItems>[1];
|
||||
|
||||
beforeAll(async () => {
|
||||
server = await setupTestServer();
|
||||
deps = {
|
||||
clients: server.clients,
|
||||
stores: server.stores,
|
||||
apiBaseUrl: 'https://api.puter.com',
|
||||
config: { static_hosting_domain: 'puter.site' },
|
||||
} as unknown as Parameters<typeof getTaskbarItems>[1];
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await server?.shutdown();
|
||||
});
|
||||
|
||||
const makeUser = async (taskbarItems?: unknown) => {
|
||||
const username = `tb-${Math.random().toString(36).slice(2, 10)}`;
|
||||
const created = await server.stores.user.create({
|
||||
username,
|
||||
uuid: uuidv4(),
|
||||
password: null,
|
||||
email: `${username}@test.local`,
|
||||
} as never);
|
||||
if (taskbarItems !== undefined) {
|
||||
await server.stores.user.update(created.id, {
|
||||
taskbar_items:
|
||||
typeof taskbarItems === 'string'
|
||||
? taskbarItems
|
||||
: JSON.stringify(taskbarItems),
|
||||
});
|
||||
}
|
||||
return (await server.stores.user.getById(created.id))!;
|
||||
};
|
||||
|
||||
const makeApp = async (ownerUserId: number, overrides = {}) => {
|
||||
const name = `tb-app-${Math.random().toString(36).slice(2, 10)}`;
|
||||
return server.stores.app.create(
|
||||
{
|
||||
name,
|
||||
title: `Title ${name}`,
|
||||
index_url: `https://${name}.example.com/`,
|
||||
description: 'a test app',
|
||||
...overrides,
|
||||
},
|
||||
{ ownerUserId },
|
||||
);
|
||||
};
|
||||
|
||||
it('seeds the default taskbar for a user that has none, and persists it', async () => {
|
||||
const user = await makeUser();
|
||||
expect(user.taskbar_items).toBeFalsy();
|
||||
|
||||
await getTaskbarItems(user as never, deps);
|
||||
|
||||
const [row] = await server.clients.db.read(
|
||||
'SELECT `taskbar_items` FROM `user` WHERE `id` = ?',
|
||||
[user.id],
|
||||
);
|
||||
const stored = JSON.parse(String(row.taskbar_items));
|
||||
expect(stored.map((e: { name: string }) => e.name)).toContain(
|
||||
'app-center',
|
||||
);
|
||||
// The cached row must not keep serving the pre-seed value.
|
||||
expect(
|
||||
(await server.stores.user.getById(user.id))?.taskbar_items,
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('resolves entries by name, uid and id', async () => {
|
||||
const owner = await makeUser([]);
|
||||
const byName = await makeApp(owner.id);
|
||||
const byUid = await makeApp(owner.id);
|
||||
const byId = await makeApp(owner.id);
|
||||
|
||||
const user = await makeUser([
|
||||
{ name: byName.name, type: 'app' },
|
||||
{ uid: byUid.uid, type: 'app' },
|
||||
{ id: byId.id, type: 'app' },
|
||||
]);
|
||||
|
||||
const items = await getTaskbarItems(user as never, deps);
|
||||
expect(items.map((i) => i.uid)).toEqual([
|
||||
byName.uid,
|
||||
byUid.uid,
|
||||
byId.uid,
|
||||
]);
|
||||
expect(items[0]).toMatchObject({
|
||||
uuid: byName.uid,
|
||||
name: byName.name,
|
||||
title: byName.title,
|
||||
godmode: false,
|
||||
maximize_on_start: false,
|
||||
description: 'a test app',
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves the whole taskbar with one batched lookup per key, not one per entry', async () => {
|
||||
const owner = await makeUser([]);
|
||||
const apps = await Promise.all([0, 1, 2].map(() => makeApp(owner.id)));
|
||||
const byUid = await makeApp(owner.id);
|
||||
const user = await makeUser([
|
||||
...apps.map((app) => ({ name: app.name, type: 'app' })),
|
||||
{ uid: byUid.uid, type: 'app' },
|
||||
]);
|
||||
const appStore = server.stores.app;
|
||||
const spies = {
|
||||
getByName: vi.spyOn(appStore, 'getByName'),
|
||||
getByUid: vi.spyOn(appStore, 'getByUid'),
|
||||
getById: vi.spyOn(appStore, 'getById'),
|
||||
getByNames: vi.spyOn(appStore, 'getByNames'),
|
||||
getByUids: vi.spyOn(appStore, 'getByUids'),
|
||||
};
|
||||
try {
|
||||
const items = await getTaskbarItems(user as never, deps);
|
||||
|
||||
expect(items.map((i) => i.uid)).toEqual([
|
||||
...apps.map((app) => app.uid),
|
||||
byUid.uid,
|
||||
]);
|
||||
expect(spies.getByName).not.toHaveBeenCalled();
|
||||
expect(spies.getByUid).not.toHaveBeenCalled();
|
||||
expect(spies.getById).not.toHaveBeenCalled();
|
||||
expect(spies.getByNames).toHaveBeenCalledTimes(1);
|
||||
expect(spies.getByUids).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
for (const spy of Object.values(spies)) spy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('skips non-app entries, the explorer pin, and unresolvable apps', async () => {
|
||||
const owner = await makeUser([]);
|
||||
const app = await makeApp(owner.id);
|
||||
const user = await makeUser([
|
||||
{ name: 'explorer', type: 'app' },
|
||||
{ name: 'nothing-here', type: 'app' },
|
||||
{ name: app.name, type: 'separator' },
|
||||
{ type: 'app' },
|
||||
{ name: app.name, type: 'app' },
|
||||
]);
|
||||
|
||||
const items = await getTaskbarItems(user as never, deps);
|
||||
expect(items).toHaveLength(1);
|
||||
expect(items[0].name).toBe(app.name);
|
||||
});
|
||||
|
||||
it('falls back to an empty taskbar when the stored JSON is corrupt', async () => {
|
||||
const user = await makeUser('{not json');
|
||||
expect(await getTaskbarItems(user as never, deps)).toEqual([]);
|
||||
});
|
||||
|
||||
it('accepts an already-parsed taskbar array on the user row', async () => {
|
||||
const owner = await makeUser([]);
|
||||
const app = await makeApp(owner.id);
|
||||
const user = await makeUser([]);
|
||||
|
||||
const items = await getTaskbarItems(
|
||||
{ ...user, taskbar_items: [{ name: app.name, type: 'app' }] },
|
||||
deps,
|
||||
);
|
||||
expect(items.map((i) => i.name)).toEqual([app.name]);
|
||||
});
|
||||
|
||||
it('builds a sized icon URL by default and drops the icon on request', async () => {
|
||||
const owner = await makeUser([]);
|
||||
const app = await makeApp(owner.id);
|
||||
const user = await makeUser([{ name: app.name, type: 'app' }]);
|
||||
|
||||
const withIcon = await getTaskbarItems(user as never, deps, {
|
||||
iconSize: 64,
|
||||
});
|
||||
expect(withIcon[0].icon).toBe(
|
||||
`https://api.puter.com/app-icon/${app.uid}/64`,
|
||||
);
|
||||
|
||||
const withoutIcon = await getTaskbarItems(user as never, deps, {
|
||||
noIcons: true,
|
||||
});
|
||||
expect('icon' in withoutIcon[0]).toBe(false);
|
||||
});
|
||||
|
||||
it('pairs the icon URL with the direct subdomain URL when one exists', async () => {
|
||||
const owner = await makeUser([]);
|
||||
// An http(s) icon column is what the resize pipeline leaves behind, so
|
||||
// the sized PNG is on the subdomain.
|
||||
const app = await makeApp(owner.id, {
|
||||
icon: 'https://api.puter.com/app-icon/x',
|
||||
});
|
||||
const user = await makeUser([{ name: app.name, type: 'app' }]);
|
||||
|
||||
const items = await getTaskbarItems(user as never, deps, {
|
||||
iconSize: 64,
|
||||
});
|
||||
expect(items[0].icon).toBe(
|
||||
`https://api.puter.com/app-icon/${app.uid}/64`,
|
||||
);
|
||||
expect(items[0].iconCdnUrl).toBe(
|
||||
`https://puter-app-icons.puter.site/${app.uid}-64.png`,
|
||||
);
|
||||
});
|
||||
|
||||
it('reports no subdomain URL for an app whose icon was never generated', async () => {
|
||||
const owner = await makeUser([]);
|
||||
const app = await makeApp(owner.id);
|
||||
const user = await makeUser([{ name: app.name, type: 'app' }]);
|
||||
|
||||
const items = await getTaskbarItems(user as never, deps, {
|
||||
iconSize: 64,
|
||||
});
|
||||
expect(items[0].iconCdnUrl).toBeNull();
|
||||
});
|
||||
|
||||
it('falls back to the raw icon column when no API base URL is configured', async () => {
|
||||
const owner = await makeUser([]);
|
||||
const app = await makeApp(owner.id, {
|
||||
icon: 'https://cdn.example.com/i.png',
|
||||
});
|
||||
const user = await makeUser([{ name: app.name, type: 'app' }]);
|
||||
|
||||
const items = await getTaskbarItems(
|
||||
user as never,
|
||||
{
|
||||
...deps,
|
||||
apiBaseUrl: undefined,
|
||||
} as never,
|
||||
);
|
||||
expect(items[0].icon).toBe('https://cdn.example.com/i.png');
|
||||
});
|
||||
|
||||
it('reports null for an app with no icon and no API base URL', async () => {
|
||||
const owner = await makeUser([]);
|
||||
const app = await makeApp(owner.id);
|
||||
const user = await makeUser([{ name: app.name, type: 'app' }]);
|
||||
|
||||
const items = await getTaskbarItems(
|
||||
user as never,
|
||||
{
|
||||
...deps,
|
||||
apiBaseUrl: undefined,
|
||||
} as never,
|
||||
);
|
||||
expect(items[0].icon).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,134 +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 type { AppIconHostConfig } from './appIcon.js';
|
||||
import { toAppSummary, type AppRow } from './appView.js';
|
||||
|
||||
interface TaskbarEntry {
|
||||
name?: string;
|
||||
id?: number;
|
||||
uid?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
interface TaskbarOptions {
|
||||
iconSize?: number;
|
||||
noIcons?: boolean;
|
||||
}
|
||||
|
||||
interface TaskbarDeps {
|
||||
apiBaseUrl?: string;
|
||||
config?: AppIconHostConfig;
|
||||
stores: {
|
||||
app: {
|
||||
getByNames: (names: string[]) => Promise<Map<string, AppRow>>;
|
||||
getByUids: (uids: string[]) => Promise<Map<string, AppRow>>;
|
||||
getByIds: (ids: number[]) => Promise<Map<number, AppRow>>;
|
||||
};
|
||||
user: {
|
||||
update: (
|
||||
id: number,
|
||||
patch: Record<string, unknown>,
|
||||
) => Promise<unknown>;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const DEFAULT_TASKBAR_ITEMS: TaskbarEntry[] = [
|
||||
{ name: 'app-center', type: 'app' },
|
||||
{ name: 'builder', type: 'app' },
|
||||
{ name: 'dev-center', type: 'app' },
|
||||
{ name: 'editor', type: 'app' },
|
||||
{ name: 'code', type: 'app' },
|
||||
{ name: 'camera', type: 'app' },
|
||||
{ name: 'recorder', type: 'app' },
|
||||
{ name: 'browser', type: 'app' },
|
||||
];
|
||||
|
||||
export async function getTaskbarItems(
|
||||
user: Record<string, unknown>,
|
||||
deps: TaskbarDeps,
|
||||
options: TaskbarOptions = {},
|
||||
): Promise<Array<Record<string, unknown>>> {
|
||||
let raw: TaskbarEntry[];
|
||||
|
||||
if (!user.taskbar_items) {
|
||||
raw = DEFAULT_TASKBAR_ITEMS;
|
||||
await deps.stores.user.update(user.id as number, {
|
||||
taskbar_items: JSON.stringify(raw),
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
raw =
|
||||
typeof user.taskbar_items === 'string'
|
||||
? JSON.parse(user.taskbar_items as string)
|
||||
: (user.taskbar_items as TaskbarEntry[]);
|
||||
} catch {
|
||||
raw = [];
|
||||
}
|
||||
}
|
||||
|
||||
const entries = raw.filter(
|
||||
(entry) => entry.type === 'app' && entry.name !== 'explorer',
|
||||
);
|
||||
const [byName, byUid, byId] = await Promise.all([
|
||||
deps.stores.app.getByNames(
|
||||
entries.flatMap((entry) => (entry.name ? [entry.name] : [])),
|
||||
),
|
||||
deps.stores.app.getByUids(
|
||||
entries.flatMap((entry) =>
|
||||
!entry.name && entry.uid ? [entry.uid] : [],
|
||||
),
|
||||
),
|
||||
deps.stores.app.getByIds(
|
||||
entries.flatMap((entry) =>
|
||||
!entry.name && !entry.uid && entry.id ? [entry.id] : [],
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
const items: Array<Record<string, unknown>> = [];
|
||||
for (const entry of entries) {
|
||||
const app = entry.name
|
||||
? byName.get(entry.name)
|
||||
: entry.uid
|
||||
? byUid.get(entry.uid)
|
||||
: entry.id
|
||||
? byId.get(entry.id)
|
||||
: undefined;
|
||||
if (!app) continue;
|
||||
|
||||
const item: Record<string, unknown> = {
|
||||
uid: app.uid,
|
||||
...toAppSummary(app, {
|
||||
apiBaseUrl: deps.apiBaseUrl,
|
||||
config: deps.config,
|
||||
iconSize: options.iconSize,
|
||||
}),
|
||||
description: app.description,
|
||||
};
|
||||
if (options.noIcons) {
|
||||
delete item.icon;
|
||||
delete item.iconCdnUrl;
|
||||
}
|
||||
items.push(item);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
@@ -85,7 +85,17 @@ describe('buildUserDetails', () => {
|
||||
[`/${user.username}/Desktop`]: user.desktop_uuid,
|
||||
[`/${user.username}/Trash`]: user.trash_uuid,
|
||||
});
|
||||
expect(Array.isArray(details.taskbar_items)).toBe(true);
|
||||
});
|
||||
|
||||
it('leaves out taskbar items saved on the account', async () => {
|
||||
const user = {
|
||||
...(await makeUser()),
|
||||
taskbar_items: JSON.stringify([{ type: 'app', name: 'editor' }]),
|
||||
};
|
||||
const details = await buildUserDetails(user, deps(), {
|
||||
isUser: true,
|
||||
});
|
||||
expect(details).not.toHaveProperty('taskbar_items');
|
||||
});
|
||||
|
||||
it('leaves the desktop-only parts out for an app actor', async () => {
|
||||
@@ -93,7 +103,6 @@ describe('buildUserDetails', () => {
|
||||
const details = await buildUserDetails(user, deps(), {
|
||||
isUser: false,
|
||||
});
|
||||
expect(details).not.toHaveProperty('taskbar_items');
|
||||
expect(details).not.toHaveProperty('directories');
|
||||
expect(details.card_fallback_available).toBe(false);
|
||||
});
|
||||
@@ -124,7 +133,7 @@ describe('buildUserDetails lookups', () => {
|
||||
return keys.slice(keys.indexOf('hasDevAccountAccess') + 1);
|
||||
};
|
||||
|
||||
it('reads the linked provider, taskbar and seat together, listed in a fixed order', async () => {
|
||||
it('reads the linked provider and seat together, listed in a fixed order', async () => {
|
||||
const user = {
|
||||
...(await makeUser()),
|
||||
password: null,
|
||||
@@ -162,7 +171,6 @@ describe('buildUserDetails lookups', () => {
|
||||
);
|
||||
expect(tailKeys(details)).toEqual([
|
||||
'oidc_revalidate_url',
|
||||
'taskbar_items',
|
||||
'directories',
|
||||
'team',
|
||||
'last_activity_ts',
|
||||
@@ -177,7 +185,6 @@ describe('buildUserDetails lookups', () => {
|
||||
const user = {
|
||||
...(await makeUser()),
|
||||
password: null,
|
||||
taskbar_items: JSON.stringify([{ type: 'app', name: 'editor' }]),
|
||||
last_activity_ts: '2026-01-02 03:04:05',
|
||||
};
|
||||
const events: string[] = [];
|
||||
@@ -199,15 +206,6 @@ describe('buildUserDetails lookups', () => {
|
||||
},
|
||||
},
|
||||
stores: Object.assign(Object.create(d.stores), {
|
||||
app: {
|
||||
getByNames: () =>
|
||||
timed(events, 'taskbar', 20, () => {
|
||||
throw new Error('taskbar down');
|
||||
}),
|
||||
getByUids: async () => new Map(),
|
||||
getByIds: async () => new Map(),
|
||||
},
|
||||
// Fails first, but is reported after the taskbar.
|
||||
team: {
|
||||
getOrgSeat: () =>
|
||||
timed(events, 'seat', 5, () => {
|
||||
@@ -219,18 +217,12 @@ describe('buildUserDetails lookups', () => {
|
||||
{ isUser: true },
|
||||
);
|
||||
expect(warn.mock.calls).toEqual([
|
||||
['[user-details] taskbar_items failed:', expect.any(Error)],
|
||||
['[user-details] team lookup failed:', expect.any(Error)],
|
||||
]);
|
||||
} finally {
|
||||
warn.mockRestore();
|
||||
}
|
||||
expect(tailKeys(details)).toEqual([
|
||||
'taskbar_items',
|
||||
'directories',
|
||||
'last_activity_ts',
|
||||
]);
|
||||
expect(details.taskbar_items).toEqual([]);
|
||||
expect(tailKeys(details)).toEqual(['directories', 'last_activity_ts']);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -27,18 +27,12 @@ import {
|
||||
isCardFallbackEligible,
|
||||
type CardFallbackClients,
|
||||
} from './cardFallback.js';
|
||||
import { getTaskbarItems } from './taskbarItems.js';
|
||||
import { isEmailGateLast } from './verificationOrder.js';
|
||||
|
||||
type TaskbarDeps = Parameters<typeof getTaskbarItems>[1];
|
||||
|
||||
export interface UserDetailsDeps {
|
||||
config: IConfig;
|
||||
// `db.write` backs the taskbar's first-read default row.
|
||||
clients: CardFallbackClients & {
|
||||
db: { write: (query: string, params?: unknown[]) => Promise<unknown> };
|
||||
};
|
||||
stores: TaskbarDeps['stores'] & {
|
||||
clients: CardFallbackClients;
|
||||
stores: {
|
||||
kv: Pick<SystemKVStore, 'get'>;
|
||||
team?: Pick<TeamStoreContract, 'getOrgSeat'>;
|
||||
};
|
||||
@@ -78,8 +72,8 @@ const SENSITIVE_KEYS: ReadonlySet<string> = new Set([
|
||||
]);
|
||||
|
||||
/**
|
||||
* Delete every sensitive key at any depth (`metadata` and `taskbar_items` are
|
||||
* nested). Depth-limited and cycle-safe. Runs last over a whole payload.
|
||||
* Delete every sensitive key at any depth (`metadata` is nested). Depth-limited
|
||||
* and cycle-safe. Runs last over a whole payload.
|
||||
*/
|
||||
export const scrubSensitive = (
|
||||
value: unknown,
|
||||
@@ -124,7 +118,7 @@ export const toUnixSeconds = (value: unknown): number | undefined => {
|
||||
export async function buildUserDetails(
|
||||
user: UserRow,
|
||||
deps: UserDetailsDeps,
|
||||
opts: { isUser: boolean; iconSize?: number; noIcons?: boolean },
|
||||
opts: { isUser: boolean },
|
||||
): Promise<Record<string, unknown>> {
|
||||
const { config, clients, stores, services } = deps;
|
||||
const { isUser } = opts;
|
||||
@@ -182,14 +176,6 @@ export async function buildUserDetails(
|
||||
hasDevAccountAccess: !!user.metadata?.hasDevAccountAccess,
|
||||
};
|
||||
|
||||
// Not an inline literal, so a `TaskbarDeps` without `clients` still
|
||||
// accepts it.
|
||||
const taskbarDeps = {
|
||||
clients,
|
||||
stores,
|
||||
apiBaseUrl: String(config.api_base_url ?? ''),
|
||||
config,
|
||||
};
|
||||
const linkedProvider = async () => {
|
||||
if (!oidcOnly) return null;
|
||||
try {
|
||||
@@ -199,19 +185,6 @@ export async function buildUserDetails(
|
||||
return null;
|
||||
}
|
||||
};
|
||||
const taskbarItems = async () => {
|
||||
if (!isUser) return null;
|
||||
try {
|
||||
return {
|
||||
items: await getTaskbarItems(user, taskbarDeps, {
|
||||
iconSize: opts.iconSize,
|
||||
noIcons: opts.noIcons,
|
||||
}),
|
||||
};
|
||||
} catch (error) {
|
||||
return { error };
|
||||
}
|
||||
};
|
||||
// The team that pays for the account; every seat restriction keys on it.
|
||||
const orgSeat = async () => {
|
||||
if (!isUser || config.teams_enabled !== true) return null;
|
||||
@@ -223,26 +196,14 @@ export async function buildUserDetails(
|
||||
};
|
||||
// Independent and each best-effort, so read together; the results land
|
||||
// below in the order the payload has always listed them.
|
||||
const [provider, taskbar, team] = await Promise.all([
|
||||
linkedProvider(),
|
||||
taskbarItems(),
|
||||
orgSeat(),
|
||||
]);
|
||||
const [provider, team] = await Promise.all([linkedProvider(), orgSeat()]);
|
||||
|
||||
if (provider) {
|
||||
const origin = (config.origin ?? '').replace(/\/$/, '');
|
||||
details.oidc_revalidate_url = `${origin}/auth/oidc/${provider}/start?flow=revalidate&user_uuid=${encodeURIComponent(user.uuid)}`;
|
||||
}
|
||||
|
||||
if (taskbar) {
|
||||
// Best-effort: the account still works without them.
|
||||
if ('error' in taskbar) {
|
||||
console.warn('[user-details] taskbar_items failed:', taskbar.error);
|
||||
details.taskbar_items = [];
|
||||
} else {
|
||||
details.taskbar_items = taskbar.items;
|
||||
}
|
||||
|
||||
if (isUser) {
|
||||
const directories: Record<string, unknown> = {};
|
||||
const nameToProp: Record<string, string> = {
|
||||
desktop_uuid: `/${user.username}/Desktop`,
|
||||
|
||||
@@ -27,7 +27,7 @@ A set of key/value pairs that configure the window.
|
||||
| `is_resizable` | `Boolean` | If set to `true`, the user can resize the window. |
|
||||
| `has_head` | `Boolean` | If set to `true`, the window has a head which contains the icon and the close, minimize, and maximize buttons. |
|
||||
| `disable_parent_window` | `Boolean` | If set to `true`, the parent window is blocked until the current window is closed. |
|
||||
| `show_in_taskbar` | `Boolean` | If set to `true`, the window is represented in the taskbar. |
|
||||
| `show_in_taskbar` | `Boolean` | Deprecated. It has no effect. |
|
||||
|
||||
## Return value
|
||||
A `Promise` that resolves to a window handle object with an `id` (String) property identifying the created window. This `id` can be passed as the `window_id` argument to the `setWindow*` methods.
|
||||
@@ -50,7 +50,6 @@ A `Promise` that resolves to a window handle object with an `id` (String) proper
|
||||
is_resizable: false,
|
||||
has_head: true,
|
||||
center: true,
|
||||
show_in_taskbar: false,
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -36,12 +36,11 @@ Without `background`, Puter creates and shows the window before the app's own co
|
||||
runs, so a service app cannot avoid briefly appearing on screen.
|
||||
|
||||
A background instance stays private to your app for as long as it is hidden: it has no
|
||||
taskbar item and no running mark on its icon, and opening the app from the
|
||||
taskbar or from Puter's app list starts a separate, ordinary instance for the
|
||||
user rather than handing them the one you are talking to. It can show itself at
|
||||
any time with [`puter.ui.showWindow()`](/UI/showWindow), and from that moment it
|
||||
is an ordinary window. It takes its place in the taskbar, and the user can
|
||||
return to it, hide it, or close it like any other.
|
||||
running mark on its icon, and opening the app from Puter's app list starts a
|
||||
separate, ordinary instance for the user rather than handing them the one you
|
||||
are talking to. It can show itself at any time with
|
||||
[`puter.ui.showWindow()`](/UI/showWindow), and from that moment it is an
|
||||
ordinary window that the user can return to, hide, or close like any other.
|
||||
|
||||
A background app closes when the app that launched it closes: it was launched to
|
||||
serve that app, and the user never saw it. Once it has shown itself it keeps
|
||||
|
||||
@@ -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 | |
|
||||
|
||||
@@ -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', '/dashboard'], (req, res) => {
|
||||
res.send(generateDevHtml({
|
||||
env: env,
|
||||
api_origin: apiOrigin,
|
||||
|
||||
+4
-5
@@ -484,7 +484,6 @@ const ipc_listener = async (event, handled) => {
|
||||
is_resizable: event.data.options.is_resizable,
|
||||
has_head: event.data.options.has_head,
|
||||
center: event.data.options.center,
|
||||
show_in_taskbar: event.data.options.show_in_taskbar,
|
||||
iframe_srcdoc: event.data.options.content,
|
||||
parent_uuid: event.data.appInstanceID,
|
||||
});
|
||||
@@ -1149,9 +1148,9 @@ const ipc_listener = async (event, handled) => {
|
||||
if ( event.data.y !== undefined ) {
|
||||
event.data.y = parseFloat(event.data.y);
|
||||
// we don't want the window to go off the top edge of the screen
|
||||
if ( event.data.y < window.taskbar_height )
|
||||
if ( event.data.y < 0 )
|
||||
{
|
||||
event.data.y = window.taskbar_height;
|
||||
event.data.y = 0;
|
||||
}
|
||||
// we don't want the window to go off the bottom edge of the screen
|
||||
if ( event.data.y > window.innerHeight - 100 )
|
||||
@@ -1217,9 +1216,9 @@ const ipc_listener = async (event, handled) => {
|
||||
if ( event.data.y !== undefined ) {
|
||||
event.data.y = parseFloat(event.data.y);
|
||||
// we don't want the window to go off the top edge of the screen
|
||||
if ( event.data.y < window.taskbar_height )
|
||||
if ( event.data.y < 0 )
|
||||
{
|
||||
event.data.y = window.taskbar_height;
|
||||
event.data.y = 0;
|
||||
}
|
||||
// we don't want the window to go off the bottom edge of the screen
|
||||
if ( event.data.y > window.innerHeight - 100 )
|
||||
|
||||
@@ -424,15 +424,14 @@ export const authHeadHtml = (flow, title, { openerNotice = true } = {}) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* On a cover page, the logo is a way out: desktop users stay on the desktop,
|
||||
* everyone else goes to the root dashboard.
|
||||
* On a cover page, the logo is a way out, to the root dashboard.
|
||||
*
|
||||
* @param {AuthFlow} flow
|
||||
* @returns {string} Empty where the logo leads nowhere.
|
||||
*/
|
||||
const logoHref = (flow) => {
|
||||
if ( ! flow.page || window.embedded_in_popup ) return '';
|
||||
return window.location.pathname === '/desktop' ? '/desktop' : '/';
|
||||
return '/';
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -75,8 +75,7 @@ async function UIDashboard (options) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
options = options ?? {};
|
||||
|
||||
// Mark dashboard mode on <body> so window chrome can adapt — e.g. app
|
||||
// windows keep their minimize button even though fullpage-mode hides it.
|
||||
// Mark dashboard mode on <body> so window chrome and notifications can adapt.
|
||||
$('body').addClass('dashboard-mode');
|
||||
|
||||
// Create mutable tabs array from built-in tabs
|
||||
|
||||
@@ -79,7 +79,6 @@ async function UIWindowDisable2FA (options) {
|
||||
width: 350,
|
||||
height: 'auto',
|
||||
dominant: true,
|
||||
show_in_taskbar: false,
|
||||
on_before_exit: async () => {
|
||||
if ( ! disabled_successfully ) {
|
||||
promise.resolve(false);
|
||||
|
||||
@@ -136,7 +136,6 @@ function UIAlert (options) {
|
||||
selectable_body: false,
|
||||
draggable_body: options.draggable_body ?? true,
|
||||
allow_context_menu: false,
|
||||
show_in_taskbar: false,
|
||||
window_class: 'window-alert',
|
||||
dominant: true,
|
||||
body_content: h,
|
||||
|
||||
@@ -568,9 +568,9 @@ function UIContextMenu (options) {
|
||||
|
||||
// Y position
|
||||
// is the menu going to go out of the window from the bottom?
|
||||
if ( (start_y + menu_height) > (window.innerHeight - window.taskbar_height - 10) )
|
||||
if ( (start_y + menu_height) > (window.innerHeight - 10) )
|
||||
{
|
||||
y_pos = window.innerHeight - menu_height - window.taskbar_height - 10;
|
||||
y_pos = window.innerHeight - menu_height - 10;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -801,9 +801,6 @@ function UIContextMenu (options) {
|
||||
$(options.parent_element).css('overflow', 'scroll');
|
||||
|
||||
$(options.parent_element).removeClass('has-open-contextmenu');
|
||||
if ( $(options.parent_element).hasClass('taskbar-item') ) {
|
||||
window.make_taskbar_sortable();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+11
-849
@@ -19,24 +19,16 @@
|
||||
|
||||
import path from '../lib/path.js';
|
||||
|
||||
import UIContextMenu from './UIContextMenu.js';
|
||||
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 UITaskbar from './UITaskbar.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';
|
||||
import truncate_filename from '../helpers/truncateFilename.js';
|
||||
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,
|
||||
@@ -63,7 +55,7 @@ import apply_item_added_to_containers from '../helpers/applyItemAddedToContainer
|
||||
import parse_item_metadata from '../helpers/parseItemMetadata.js';
|
||||
import { resolve_default_app } from '../helpers/defaultApp.js';
|
||||
|
||||
async function UIDesktop (options) {
|
||||
async function UIDesktop () {
|
||||
// start a transaction if we're not in embedded or fullpage mode
|
||||
let transaction;
|
||||
if ( !window.is_embedded && !window.is_fullpage_mode ) {
|
||||
@@ -73,58 +65,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: {
|
||||
@@ -714,16 +654,8 @@ async function UIDesktop (options) {
|
||||
|
||||
h += '<div class="window-container"></div>';
|
||||
|
||||
// Desktop
|
||||
// If desktop is not in fullpage/embedded mode, we hide it until files and directories are loaded and then fade in the UI
|
||||
// This gives a calm and smooth experience for the user
|
||||
h += `<div class="desktop item-container disable-user-select"
|
||||
data-uid="${options.desktop_fsentry?.uid ?? ''}"
|
||||
data-sort_by="${!options.desktop_fsentry?.sort_by ? 'name' : options.desktop_fsentry.sort_by}"
|
||||
data-sort_order="${!options.desktop_fsentry?.sort_order ? 'asc' : options.desktop_fsentry.sort_order}"
|
||||
data-path="${html_encode(window.desktop_path)}"
|
||||
>`;
|
||||
h += '</div>';
|
||||
// The surface windows open on. It holds no items.
|
||||
h += '<div class="desktop disable-user-select"></div>';
|
||||
|
||||
// Get window sidebar width
|
||||
puter.kv.get('window_sidebar_width').catch(() => null).then(async (val) => {
|
||||
@@ -759,424 +691,22 @@ async function UIDesktop (options) {
|
||||
// Append to <body>
|
||||
$('body').append(h);
|
||||
|
||||
// Set desktop height based on taskbar height
|
||||
$('.desktop').css('height', `calc(100vh - ${window.taskbar_height + window.toolbar_height}px)`);
|
||||
|
||||
// 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));
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Taskbar
|
||||
// In fullpage mode the desktop only hosts a single app, so there's
|
||||
// nothing for the taskbar to do; skip building it entirely.
|
||||
// ---------------------------------------------------------------
|
||||
if ( !window.is_fullpage_mode ) {
|
||||
UITaskbar();
|
||||
}
|
||||
|
||||
// Update desktop dimensions after taskbar is initialized with position
|
||||
window.update_desktop_dimensions_for_taskbar();
|
||||
|
||||
const el_desktop = document.querySelector('.desktop');
|
||||
|
||||
window.active_element = el_desktop;
|
||||
window.active_item_container = el_desktop;
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Dragster
|
||||
// Allow dragging of local files onto desktop.
|
||||
// --------------------------------------------------------
|
||||
$(el_desktop).dragster({
|
||||
enter: function (dragsterEvent, event) {
|
||||
$('.context-menu').remove();
|
||||
},
|
||||
leave: function (dragsterEvent, event) {
|
||||
},
|
||||
drop: async function (dragsterEvent, event) {
|
||||
const e = event.originalEvent;
|
||||
// no drop on item
|
||||
if ( $(event.target).hasClass('item') || $(event.target).parent('.item').length > 0 )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// recursively create directories and upload files
|
||||
if ( e.dataTransfer?.items?.length > 0 ) {
|
||||
window.upload_items(e.dataTransfer.items, window.desktop_path);
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
return false;
|
||||
},
|
||||
// The desktop has no menu of its own, and the browser's doesn't belong here.
|
||||
$(el_desktop).on('contextmenu', function (event) {
|
||||
if ( event.target === el_desktop ) event.preventDefault();
|
||||
});
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Droppable
|
||||
// --------------------------------------------------------
|
||||
$(el_desktop).droppable({
|
||||
accept: '.item',
|
||||
tolerance: 'intersect',
|
||||
drop: function (event, ui) {
|
||||
// Check if item was actually dropped on desktop and not a window
|
||||
if ( window.mouseover_window !== undefined )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Can't drop anything but UIItems on desktop
|
||||
if ( ! $(ui.draggable).hasClass('item') )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't move an item to its current directory
|
||||
if ( path.dirname($(ui.draggable).attr('data-path')) === window.desktop_path && !event.ctrlKey )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If ctrl is pressed and source is Trashed, cancel whole operation
|
||||
if ( event.ctrlKey && path.dirname($(ui.draggable).attr('data-path')) === window.trash_path )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Unselect previously selected items
|
||||
$(el_desktop).children('.item-selected').removeClass('item-selected');
|
||||
|
||||
const items_to_move = [];
|
||||
// first item
|
||||
items_to_move.push(ui.draggable);
|
||||
|
||||
// all subsequent items
|
||||
const cloned_items = document.getElementsByClassName('item-selected-clone');
|
||||
for ( let i = 0; i < cloned_items.length; i++ ) {
|
||||
const source_item = document.getElementById(`item-${ $(cloned_items[i]).attr('data-id')}`);
|
||||
if ( source_item !== null )
|
||||
{
|
||||
items_to_move.push(source_item);
|
||||
}
|
||||
}
|
||||
|
||||
// if ctrl key is down, copy items
|
||||
if ( event.ctrlKey ) {
|
||||
// unless source is Trash
|
||||
if ( path.dirname($(ui.draggable).attr('data-path')) === window.trash_path )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
window.copy_items(items_to_move, window.desktop_path);
|
||||
}
|
||||
// otherwise, move items
|
||||
else {
|
||||
window.move_items(items_to_move, window.desktop_path);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
//--------------------------------------------------
|
||||
// ContextMenu
|
||||
//--------------------------------------------------
|
||||
$(el_desktop).bind('contextmenu taphold', function (event) {
|
||||
// dismiss taphold on regular devices
|
||||
if ( event.type === 'taphold' && !isMobile.phone && !isMobile.tablet )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const $target = $(event.target);
|
||||
|
||||
// elements that should retain native ctxmenu
|
||||
if ( $target.is('input') || $target.is('textarea') )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// custom ctxmenu for all other elements
|
||||
if ( event.target === el_desktop ) {
|
||||
event.preventDefault();
|
||||
UIContextMenu({
|
||||
position: event.type === 'taphold' ? undefined : { left: event.pageX, top: event.pageY },
|
||||
items: [
|
||||
// -------------------------------------------
|
||||
// Sort by
|
||||
// -------------------------------------------
|
||||
{
|
||||
html: i18n('sort_by'),
|
||||
items: [
|
||||
{
|
||||
html: i18n('auto_arrange'),
|
||||
icon: window.is_auto_arrange_enabled ? '✓' : '',
|
||||
onClick: async function () {
|
||||
window.is_auto_arrange_enabled = !window.is_auto_arrange_enabled;
|
||||
window.store_auto_arrange_preference(window.is_auto_arrange_enabled);
|
||||
if ( window.is_auto_arrange_enabled ) {
|
||||
window.sort_items(el_desktop, $(el_desktop).attr('data-sort_by'), $(el_desktop).attr('data-sort_order'));
|
||||
window.set_sort_by(options.desktop_fsentry.uid, $(el_desktop).attr('data-sort_by'), $(el_desktop).attr('data-sort_order'));
|
||||
window.clear_desktop_item_positions(el_desktop);
|
||||
} else {
|
||||
window.set_desktop_item_positions(el_desktop);
|
||||
}
|
||||
},
|
||||
},
|
||||
// -------------------------------------------
|
||||
// -
|
||||
// -------------------------------------------
|
||||
'-',
|
||||
{
|
||||
html: i18n('name'),
|
||||
disabled: !window.is_auto_arrange_enabled,
|
||||
icon: $(el_desktop).attr('data-sort_by') === 'name' ? '✓' : '',
|
||||
onClick: async function () {
|
||||
window.sort_items(el_desktop, 'name', $(el_desktop).attr('data-sort_order'));
|
||||
window.set_sort_by(options.desktop_fsentry.uid, 'name', $(el_desktop).attr('data-sort_order'));
|
||||
},
|
||||
},
|
||||
{
|
||||
html: i18n('date_modified'),
|
||||
disabled: !window.is_auto_arrange_enabled,
|
||||
icon: $(el_desktop).attr('data-sort_by') === 'modified' ? '✓' : '',
|
||||
onClick: async function () {
|
||||
window.sort_items(el_desktop, 'modified', $(el_desktop).attr('data-sort_order'));
|
||||
window.set_sort_by(options.desktop_fsentry.uid, 'modified', $(el_desktop).attr('data-sort_order'));
|
||||
},
|
||||
},
|
||||
{
|
||||
html: i18n('type'),
|
||||
disabled: !window.is_auto_arrange_enabled,
|
||||
icon: $(el_desktop).attr('data-sort_by') === 'type' ? '✓' : '',
|
||||
onClick: async function () {
|
||||
window.sort_items(el_desktop, 'type', $(el_desktop).attr('data-sort_order'));
|
||||
window.set_sort_by(options.desktop_fsentry.uid, 'type', $(el_desktop).attr('data-sort_order'));
|
||||
},
|
||||
},
|
||||
{
|
||||
html: i18n('size'),
|
||||
disabled: !window.is_auto_arrange_enabled,
|
||||
icon: $(el_desktop).attr('data-sort_by') === 'size' ? '✓' : '',
|
||||
onClick: async function () {
|
||||
window.sort_items(el_desktop, 'size', $(el_desktop).attr('data-sort_order'));
|
||||
window.set_sort_by(options.desktop_fsentry.uid, 'size', $(el_desktop).attr('data-sort_order'));
|
||||
},
|
||||
},
|
||||
// -------------------------------------------
|
||||
// -
|
||||
// -------------------------------------------
|
||||
'-',
|
||||
{
|
||||
html: i18n('ascending'),
|
||||
disabled: !window.is_auto_arrange_enabled,
|
||||
icon: $(el_desktop).attr('data-sort_order') === 'asc' ? '✓' : '',
|
||||
onClick: async function () {
|
||||
const sort_by = $(el_desktop).attr('data-sort_by');
|
||||
window.sort_items(el_desktop, sort_by, 'asc');
|
||||
window.set_sort_by(options.desktop_fsentry.uid, sort_by, 'asc');
|
||||
},
|
||||
},
|
||||
{
|
||||
html: i18n('descending'),
|
||||
disabled: !window.is_auto_arrange_enabled,
|
||||
icon: $(el_desktop).attr('data-sort_order') === 'desc' ? '✓' : '',
|
||||
onClick: async function () {
|
||||
const sort_by = $(el_desktop).attr('data-sort_by');
|
||||
window.sort_items(el_desktop, sort_by, 'desc');
|
||||
window.set_sort_by(options.desktop_fsentry.uid, sort_by, 'desc');
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// -------------------------------------------
|
||||
// Refresh
|
||||
// -------------------------------------------
|
||||
{
|
||||
html: i18n('refresh'),
|
||||
onClick: function () {
|
||||
refresh_item_container(el_desktop, { consistency: 'strong' });
|
||||
},
|
||||
},
|
||||
// -------------------------------------------
|
||||
// Show/Hide hidden files
|
||||
// -------------------------------------------
|
||||
{
|
||||
html: i18n('show_hidden'),
|
||||
icon: window.user_preferences.show_hidden_files ? '✓' : '',
|
||||
onClick: function () {
|
||||
window.mutate_user_preferences({
|
||||
show_hidden_files: !window.user_preferences.show_hidden_files,
|
||||
});
|
||||
window.show_or_hide_files(document.querySelectorAll('.item-container'));
|
||||
},
|
||||
},
|
||||
// -------------------------------------------
|
||||
// Hide Desktop Icons
|
||||
// -------------------------------------------
|
||||
{
|
||||
html: window.desktop_icons_hidden ? i18n('Show desktop icons') : i18n('Hide desktop icons'),
|
||||
onClick: function () {
|
||||
toggleDesktopIcons();
|
||||
},
|
||||
},
|
||||
// -------------------------------------------
|
||||
// -
|
||||
// -------------------------------------------
|
||||
'-',
|
||||
// -------------------------------------------
|
||||
// New File
|
||||
// -------------------------------------------
|
||||
new_context_menu_item(window.desktop_path, el_desktop),
|
||||
// -------------------------------------------
|
||||
// -
|
||||
// -------------------------------------------
|
||||
'-',
|
||||
// -------------------------------------------
|
||||
// Paste
|
||||
// -------------------------------------------
|
||||
{
|
||||
html: i18n('paste'),
|
||||
disabled: window.clipboard.length > 0 ? false : true,
|
||||
onClick: function () {
|
||||
if ( window.clipboard_op === 'copy' )
|
||||
{
|
||||
window.copy_clipboard_items(window.desktop_path, el_desktop);
|
||||
}
|
||||
else if ( window.clipboard_op === 'move' )
|
||||
{
|
||||
window.move_clipboard_items(el_desktop);
|
||||
}
|
||||
},
|
||||
},
|
||||
// -------------------------------------------
|
||||
// Undo
|
||||
// -------------------------------------------
|
||||
{
|
||||
html: i18n('undo'),
|
||||
disabled: window.actions_history.length > 0 ? false : true,
|
||||
onClick: function () {
|
||||
window.undo_last_action();
|
||||
},
|
||||
},
|
||||
// -------------------------------------------
|
||||
// Upload Here
|
||||
// -------------------------------------------
|
||||
{
|
||||
html: i18n('upload_here'),
|
||||
onClick: function () {
|
||||
window.init_upload_using_dialog(el_desktop);
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
//-------------------------------------------
|
||||
// Desktop Files/Folders
|
||||
// we don't need to get the desktop items if we're in embedded or fullpage mode
|
||||
// because the items aren't visible anyway and we don't need to waste bandwidth/server resources
|
||||
//-------------------------------------------
|
||||
if ( !window.is_embedded && !window.is_fullpage_mode ) {
|
||||
refresh_item_container(el_desktop, {
|
||||
fadeInItems: true,
|
||||
onComplete: () => {
|
||||
// End transaction when desktop is fully ready for user interaction
|
||||
transaction.end();
|
||||
},
|
||||
});
|
||||
transaction.end();
|
||||
|
||||
// perform readdirs for caching purposes
|
||||
|
||||
// home directory
|
||||
// warm the home directory listing for the first Explorer window
|
||||
puter.fs.readdir({ path: window.home_path, consistency: 'strong' });
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Selectable
|
||||
// Only for desktop
|
||||
// -------------------------------------------
|
||||
if ( !isMobile.phone && !isMobile.tablet ) {
|
||||
let selected_ctrl_items = [];
|
||||
const selection = new SelectionArea({
|
||||
selectionContainerClass: '.selection-area-container',
|
||||
container: '.desktop',
|
||||
selectables: ['.desktop.item-container > .item'],
|
||||
startareas: ['.desktop'],
|
||||
boundaries: ['.desktop'],
|
||||
behaviour: {
|
||||
overlap: 'drop',
|
||||
intersect: 'touch',
|
||||
startThreshold: 10,
|
||||
scrolling: {
|
||||
speedDivider: 10,
|
||||
manualSpeed: 750,
|
||||
startScrollMargins: { x: 0, y: 0 },
|
||||
},
|
||||
},
|
||||
features: {
|
||||
touch: true,
|
||||
range: true,
|
||||
singleTap: {
|
||||
allow: true,
|
||||
intersect: 'native',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
selection.on('beforestart', ({ event }) => {
|
||||
selected_ctrl_items = [];
|
||||
// Returning false prevents a selection
|
||||
return $(event.target).hasClass('item-container');
|
||||
})
|
||||
.on('beforedrag', evt => {
|
||||
})
|
||||
.on('start', ({ store, event }) => {
|
||||
if ( !event.ctrlKey && !event.metaKey ) {
|
||||
for ( const el of store.stored ) {
|
||||
el.classList.remove('item-selected');
|
||||
}
|
||||
|
||||
selection.clearSelection();
|
||||
}
|
||||
|
||||
// mark desktop as selectable active
|
||||
$('.desktop').addClass('desktop-selectable-active');
|
||||
})
|
||||
.on('move', ({ store: { changed: { added, removed } }, event }) => {
|
||||
window.desktop_selectable_is_active = true;
|
||||
|
||||
for ( const el of added ) {
|
||||
// if ctrl or meta key is pressed and the item is already selected, then unselect it
|
||||
if ( (event.ctrlKey || event.metaKey) && $(el).hasClass('item-selected') ) {
|
||||
el.classList.remove('item-selected');
|
||||
selected_ctrl_items.push(el);
|
||||
}
|
||||
// otherwise select it
|
||||
else {
|
||||
el.classList.add('item-selected');
|
||||
}
|
||||
}
|
||||
|
||||
for ( const el of removed ) {
|
||||
el.classList.remove('item-selected');
|
||||
// in case this item was selected by ctrl+click before, then reselect it again
|
||||
if ( selected_ctrl_items.includes(el) )
|
||||
{
|
||||
$(el).not('.item-disabled').addClass('item-selected');
|
||||
}
|
||||
}
|
||||
})
|
||||
.on('stop', evt => {
|
||||
window.desktop_selectable_is_active = false;
|
||||
$('.desktop').removeClass('desktop-selectable-active');
|
||||
});
|
||||
}
|
||||
// notification container
|
||||
$('body').append(`<div class="notification-container"><div class="notifications-close-all">${i18n('close_all')}</div></div>`);
|
||||
|
||||
@@ -1216,41 +746,10 @@ async function UIDesktop (options) {
|
||||
}
|
||||
}
|
||||
}
|
||||
//--------------------------------------------------------------------------------------
|
||||
// /settings redirects to /dashboard
|
||||
//--------------------------------------------------------------------------------------
|
||||
else if ( window.url_paths[0]?.toLocaleLowerCase() === 'settings' ) {
|
||||
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 ) {
|
||||
@@ -1286,10 +785,10 @@ async function UIDesktop (options) {
|
||||
return;
|
||||
}
|
||||
|
||||
// disable pointer-events for all app iframes, this is to make sure selectable works
|
||||
$('.window-app-iframe').css('pointer-events', 'none');
|
||||
$('.window').find('.item-selected').addClass('item-blurred');
|
||||
$('.desktop').find('.item-blurred').removeClass('item-blurred');
|
||||
// Keyboard shortcuts act on the active container, and a click here leaves every window's.
|
||||
window.active_item_container = null;
|
||||
});
|
||||
|
||||
$(el_desktop).on('click', function (e) {
|
||||
@@ -1299,43 +798,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.
|
||||
@@ -1390,7 +852,6 @@ async function UIDesktop (options) {
|
||||
file_path: item_path,
|
||||
app_obj: suggested_apps[0],
|
||||
window_title: path.basename(item_path),
|
||||
maximized: options.maximized,
|
||||
file_signature: open_item_meta.signature,
|
||||
custom_path: window.location.pathname,
|
||||
});
|
||||
@@ -1428,272 +889,9 @@ 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();
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
$(document).on('contextmenu taphold', '.taskbar', function (event) {
|
||||
// dismiss taphold on regular devices
|
||||
if ( event.type === 'taphold' && !isMobile.phone && !isMobile.tablet )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// Get current taskbar position
|
||||
const currentPosition = window.taskbar_position || 'bottom';
|
||||
|
||||
// Create base menu items
|
||||
let menuItems = [];
|
||||
|
||||
// Only show position submenu on desktop devices
|
||||
if ( !isMobile.phone && !isMobile.tablet ) {
|
||||
menuItems.push({
|
||||
html: i18n('desktop_position'),
|
||||
items: [
|
||||
{
|
||||
html: i18n('desktop_position_left'),
|
||||
checked: currentPosition === 'left',
|
||||
onClick: function () {
|
||||
window.update_taskbar_position('left');
|
||||
},
|
||||
},
|
||||
{
|
||||
html: i18n('desktop_position_bottom'),
|
||||
checked: currentPosition === 'bottom',
|
||||
onClick: function () {
|
||||
window.update_taskbar_position('bottom');
|
||||
},
|
||||
},
|
||||
{
|
||||
html: i18n('desktop_position_right'),
|
||||
checked: currentPosition === 'right',
|
||||
onClick: function () {
|
||||
window.update_taskbar_position('right');
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
menuItems.push('-'); // divider
|
||||
}
|
||||
|
||||
// Add the "Show open windows" option for all devices
|
||||
menuItems.push({
|
||||
html: i18n('desktop_show_open_windows'),
|
||||
onClick: function () {
|
||||
$('.window').showWindow();
|
||||
},
|
||||
});
|
||||
|
||||
// Add the "Show the desktop" option for all devices
|
||||
menuItems.push({
|
||||
html: i18n('desktop_show_desktop'),
|
||||
onClick: function () {
|
||||
$('.window').hideWindow();
|
||||
},
|
||||
});
|
||||
|
||||
UIContextMenu({
|
||||
parent_element: $('.taskbar'),
|
||||
items: menuItems,
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
window.update_taskbar = function () {
|
||||
let items = [];
|
||||
$('.taskbar-item-sortable[data-keep-in-taskbar="true"]').each(function (index) {
|
||||
items.push({
|
||||
name: $(this).attr('data-app'),
|
||||
type: 'app',
|
||||
});
|
||||
});
|
||||
|
||||
// update taskbar in the server-side
|
||||
$.ajax({
|
||||
url: `${window.api_origin }/update-taskbar-items`,
|
||||
type: 'POST',
|
||||
data: JSON.stringify({
|
||||
items: items,
|
||||
}),
|
||||
async: true,
|
||||
contentType: 'application/json',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${ window.auth_token}`,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
window.remove_taskbar_item = function (item) {
|
||||
$(item).find('*').fadeOut(100, function () {
|
||||
|
||||
});
|
||||
|
||||
$(item).animate({ width: 0 }, 200, function () {
|
||||
$(item).remove();
|
||||
|
||||
// Adjust taskbar item sizes after removing an item
|
||||
if ( window.adjust_taskbar_item_sizes ) {
|
||||
setTimeout(() => {
|
||||
window.adjust_taskbar_item_sizes();
|
||||
}, 10);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.enter_fullpage_mode = (el_window) => {
|
||||
$('.taskbar').hide();
|
||||
$(el_window).find('.window-head').hide();
|
||||
$('body').addClass('fullpage-mode');
|
||||
$(el_window).css({
|
||||
@@ -1707,23 +905,11 @@ window.enter_fullpage_mode = (el_window) => {
|
||||
|
||||
window.exit_fullpage_mode = (el_window) => {
|
||||
$('body').removeClass('fullpage-mode');
|
||||
window.taskbar_height = window.default_taskbar_height;
|
||||
// In fullpage mode the taskbar is never built, so create it on exit; otherwise just restore it.
|
||||
if ( $('.taskbar').length === 0 ) {
|
||||
UITaskbar();
|
||||
} else {
|
||||
$('.taskbar').css('height', window.taskbar_height);
|
||||
$('.taskbar').show();
|
||||
}
|
||||
refresh_item_container($('.desktop.item-container'), { fadeInItems: true });
|
||||
$(el_window).removeAttr('data-is_fullpage');
|
||||
if ( el_window ) {
|
||||
window.reset_window_size_and_position(el_window);
|
||||
$(el_window).find('.window-head').show();
|
||||
}
|
||||
|
||||
// reset dektop height to take into account the taskbar height
|
||||
$('.desktop').css('height', `calc(100vh - ${window.taskbar_height + window.toolbar_height}px)`);
|
||||
};
|
||||
|
||||
window.reset_window_size_and_position = (el_window) => {
|
||||
@@ -1736,28 +922,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;
|
||||
@@ -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';
|
||||
@@ -235,17 +234,6 @@ async function UIItem (options) {
|
||||
window.update_explorer_footer_item_count(el_window);
|
||||
}
|
||||
|
||||
// manual positioning
|
||||
if ( !window.is_auto_arrange_enabled &&
|
||||
options.position &&
|
||||
// item is on the desktop (must be desktop itself and not a window, hence the '.desktop' class check)
|
||||
$(el_item).closest('.item-container.desktop').attr('data-path') === window.desktop_path
|
||||
) {
|
||||
el_item.style.position = 'absolute';
|
||||
el_item.style.left = `${options.position.left }px`;
|
||||
el_item.style.top = `${options.position.top }px`;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Dragster
|
||||
// allow dragging of local files on this window, if it's is_dir
|
||||
@@ -410,21 +398,6 @@ async function UIItem (options) {
|
||||
}
|
||||
},
|
||||
stop: function (event, ui) {
|
||||
// Allow rearranging only if item is on desktop, not trash container, auto arrange is disabled and item is not dropped into another item
|
||||
// the desktop itself, not a window showing the Desktop folder
|
||||
if ( $(el_item).closest('.item-container').hasClass('desktop') &&
|
||||
!window.is_auto_arrange_enabled && $(el_item).attr('data-path') !== window.trash_path && !ui.helper.data('dropped') &&
|
||||
// Item must be dropped on the Desktop and not on the taskbar
|
||||
window.mouseover_window === undefined && ui.position.top <= window.desktop_height - window.taskbar_height - 15 ) {
|
||||
|
||||
el_item.style.position = 'absolute';
|
||||
el_item.style.left = `${ui.position.left }px`;
|
||||
el_item.style.top = `${ui.position.top }px`;
|
||||
$('.ui-draggable-dragging').remove();
|
||||
window.desktop_item_positions[$(el_item).attr('data-uid')] = ui.position;
|
||||
window.save_desktop_item_positions();
|
||||
}
|
||||
|
||||
$('.item-selected-clone').remove();
|
||||
$('.draggable-count-badge').remove();
|
||||
// re-enable all droppable UIItems that are not a dir
|
||||
@@ -547,11 +520,6 @@ async function UIItem (options) {
|
||||
else if ( options.is_dir ) {
|
||||
// a selected folder can be the drop target of its own selection; it stays where it is
|
||||
const items_to_drop = items_to_move.filter(item => $(item).attr('data-uid') !== $(el_item).attr('data-uid'));
|
||||
if ( $(el_item).closest('.item-container').attr('data-path') === window.desktop_path ) {
|
||||
// the items leaving the desktop lose their saved positions, not the folder they land in
|
||||
items_to_drop.forEach(item => delete window.desktop_item_positions[$(item).attr('data-uid')]);
|
||||
window.save_desktop_item_positions();
|
||||
}
|
||||
window.move_items(items_to_drop, $(el_item).attr('data-shortcut_to_path') !== '' ? $(el_item).attr('data-shortcut_to_path') : $(el_item).attr('data-path'));
|
||||
}
|
||||
}
|
||||
@@ -1242,23 +1210,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 ) {
|
||||
@@ -1554,7 +1505,7 @@ async function UIItem (options) {
|
||||
left = left > (window.innerWidth - window_width) ? (window.innerWidth - window_width) : left;
|
||||
|
||||
let top = $(el_item).position().top + $(el_item).height();
|
||||
top = top > (window.innerHeight - (window_height + window.taskbar_height + window.toolbar_height)) ? (window.innerHeight - (window_height + window.taskbar_height + window.toolbar_height)) : top;
|
||||
top = top > (window.innerHeight - (window_height + window.toolbar_height)) ? (window.innerHeight - (window_height + window.toolbar_height)) : top;
|
||||
|
||||
UIWindowItemProperties(
|
||||
$(el_item).attr('data-name'),
|
||||
@@ -1571,7 +1522,7 @@ async function UIItem (options) {
|
||||
|
||||
// Create ContextMenu
|
||||
UIContextMenu({
|
||||
parent_element: ($(options.appendTo).hasClass('desktop') ? undefined : options.appendTo),
|
||||
parent_element: options.appendTo,
|
||||
position: menu_position,
|
||||
items: menu_items,
|
||||
});
|
||||
|
||||
@@ -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() }));
|
||||
|
||||
@@ -175,3 +174,24 @@ describe('UIItem Publish as Worker', () => {
|
||||
expect(labels).not.toContain('publish_as_serverless_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');
|
||||
});
|
||||
});
|
||||
@@ -62,47 +62,17 @@ function UIPopover (options) {
|
||||
function position_popover () {
|
||||
// X position
|
||||
const popover_width = options.width ?? $(el_popover).width();
|
||||
if ( options.center_horizontally ) {
|
||||
// Check taskbar position to determine popover positioning
|
||||
const taskbar_position = window.taskbar_position || 'bottom';
|
||||
|
||||
if ( taskbar_position === 'left' ) {
|
||||
// Position in top-left corner for left taskbar
|
||||
x_pos = window.taskbar_height + 10; // Just to the right of the taskbar
|
||||
} else if ( taskbar_position === 'right' ) {
|
||||
// Position in top-right corner for right taskbar
|
||||
x_pos = window.innerWidth - popover_width - window.taskbar_height - 40; // Just to the left of the taskbar
|
||||
} else {
|
||||
// Default bottom taskbar behavior - center horizontally
|
||||
x_pos = window.innerWidth / 2 - popover_width / 2 - 15;
|
||||
}
|
||||
} else {
|
||||
if ( options.position === 'bottom' || options.position === 'top' )
|
||||
{
|
||||
x_pos = options.left ?? ($(options.snapToElement).offset().left - (popover_width / 2) + 10);
|
||||
}
|
||||
else
|
||||
{
|
||||
x_pos = options.left ?? ($(options.snapToElement).offset().left + 5);
|
||||
}
|
||||
if ( options.position === 'bottom' || options.position === 'top' )
|
||||
{
|
||||
x_pos = options.left ?? ($(options.snapToElement).offset().left - (popover_width / 2) + 10);
|
||||
}
|
||||
else
|
||||
{
|
||||
x_pos = options.left ?? ($(options.snapToElement).offset().left + 5);
|
||||
}
|
||||
|
||||
// Y position
|
||||
const popover_height = options.height ?? $(el_popover).height();
|
||||
if ( options.center_horizontally ) {
|
||||
// Check taskbar position to determine popover positioning
|
||||
const taskbar_position = window.taskbar_position || 'bottom';
|
||||
|
||||
if ( taskbar_position === 'left' || taskbar_position === 'right' ) {
|
||||
// Position at top for left/right taskbars
|
||||
y_pos = window.toolbar_height + 10; // Just below the toolbar
|
||||
} else {
|
||||
// Default bottom taskbar behavior - position above taskbar
|
||||
y_pos = options.top ?? (window.innerHeight - (window.taskbar_height + popover_height + 10));
|
||||
}
|
||||
} else {
|
||||
y_pos = options.top ?? ($(options.snapToElement).offset().top + $(options.snapToElement).height() + 5);
|
||||
}
|
||||
y_pos = options.top ?? ($(options.snapToElement).offset().top + $(options.snapToElement).height() + 5);
|
||||
|
||||
$(el_popover).css({
|
||||
left: `${x_pos }px`,
|
||||
|
||||
@@ -71,7 +71,6 @@ function UIPrompt (options) {
|
||||
selectable_body: false,
|
||||
draggable_body: true,
|
||||
allow_context_menu: false,
|
||||
show_in_taskbar: false,
|
||||
window_class: 'window-alert',
|
||||
dominant: true,
|
||||
body_content: h,
|
||||
|
||||
@@ -1,468 +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 UITaskbarItem from './UITaskbarItem.js';
|
||||
import launch_app from '../helpers/launchApp.js';
|
||||
import { appIconSrc } from '../helpers/appIcon.js';
|
||||
|
||||
const TASKBAR_POSITIONS = ['left', 'bottom', 'right'];
|
||||
|
||||
async function UITaskbar (options) {
|
||||
window.global_element_id++;
|
||||
|
||||
options = options ?? {};
|
||||
options.content = options.content ?? '';
|
||||
|
||||
let taskbar_position;
|
||||
|
||||
// if first visit ever, set taskbar position to left
|
||||
if ( window.first_visit_ever ) {
|
||||
puter.kv.set('taskbar_position', 'left').catch(err => console.warn('Could not save taskbar_position:', err));
|
||||
taskbar_position = 'left';
|
||||
} else {
|
||||
taskbar_position = await puter.kv.get('taskbar_position').catch(() => null);
|
||||
// if this is not first visit, set taskbar position to bottom since it's from a user that
|
||||
// used puter before customizing taskbar position was added and the taskbar position was set to bottom
|
||||
// Unknown values are discarded too: this ends up in markup and stored values may be poisoned.
|
||||
if ( ! TASKBAR_POSITIONS.includes(taskbar_position) ) {
|
||||
taskbar_position = 'bottom'; // default position
|
||||
puter.kv.set('taskbar_position', taskbar_position).catch(err => console.warn('Could not save taskbar_position:', err));
|
||||
}
|
||||
}
|
||||
|
||||
// Force bottom position on mobile devices
|
||||
if ( isMobile.phone || isMobile.tablet ) {
|
||||
taskbar_position = 'bottom';
|
||||
}
|
||||
|
||||
// Set global taskbar position
|
||||
window.taskbar_position = taskbar_position;
|
||||
|
||||
let h = '';
|
||||
h += `<div id="ui-taskbar_${window.global_element_id}" class="taskbar taskbar-position-${taskbar_position}" style="height:${window.taskbar_height}px;">`;
|
||||
h += '<div class="taskbar-sortable" style="display: flex; justify-content: center; z-index: 99999;"></div>';
|
||||
h += '</div>';
|
||||
|
||||
if ( taskbar_position === 'left' || taskbar_position === 'right' ) {
|
||||
$('.desktop').addClass(`desktop-taskbar-position-${taskbar_position}`);
|
||||
}
|
||||
|
||||
$('.desktop').append(h);
|
||||
|
||||
//---------------------------------------------
|
||||
// add `Explorer` to the taskbar
|
||||
//---------------------------------------------
|
||||
UITaskbarItem({
|
||||
icon: window.icons['folders.svg'],
|
||||
app: 'explorer',
|
||||
name: 'Explorer',
|
||||
sortable: false,
|
||||
keep_in_taskbar: true,
|
||||
lock_keep_in_taskbar: true,
|
||||
onClick: function () {
|
||||
let open_window_count = parseInt($('.taskbar-item[data-app="explorer"]').attr('data-open-windows'));
|
||||
if ( open_window_count === 0 ) {
|
||||
launch_app({ name: 'explorer', path: window.home_path });
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
//---------------------------------------------
|
||||
// add separator before trash
|
||||
//---------------------------------------------
|
||||
UITaskbarItem({
|
||||
icon: '', // No icon for separator
|
||||
name: 'separator',
|
||||
app: 'separator',
|
||||
sortable: false,
|
||||
keep_in_taskbar: true,
|
||||
lock_keep_in_taskbar: true,
|
||||
disable_context_menu: true,
|
||||
style: 'pointer-events: none;', // Make it non-interactive
|
||||
onClick: function () {
|
||||
// Separator is non-interactive
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
//---------------------------------------------
|
||||
// Add other useful apps to the taskbar
|
||||
//---------------------------------------------
|
||||
if ( window.user.taskbar_items && window.user.taskbar_items.length > 0 ) {
|
||||
for ( let index = 0; index < window.user.taskbar_items.length; index++ ) {
|
||||
const app_info = window.user.taskbar_items[index];
|
||||
const icon = appIconSrc(app_info);
|
||||
// add taskbar item for each app
|
||||
UITaskbarItem({
|
||||
icon: icon.src,
|
||||
iconFallback: icon.fallback,
|
||||
app: app_info.name,
|
||||
name: app_info.title,
|
||||
keep_in_taskbar: true,
|
||||
onClick: function () {
|
||||
let open_window_count = parseInt($(`.taskbar-item[data-app="${app_info.name}"]`).attr('data-open-windows'));
|
||||
if ( open_window_count === 0 ) {
|
||||
launch_app({
|
||||
name: app_info.name,
|
||||
});
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//---------------------------------------------
|
||||
// add `Trash` to the taskbar
|
||||
//---------------------------------------------
|
||||
const trash = await puter.fs.stat({ path: window.trash_path, consistency: 'eventual' });
|
||||
if ( window.socket ) {
|
||||
window.socket.emit('trash.is_empty', { is_empty: trash.is_empty });
|
||||
}
|
||||
|
||||
UITaskbarItem({
|
||||
icon: trash.is_empty ? window.icons['trash.svg'] : window.icons['trash-full.svg'],
|
||||
app: 'trash',
|
||||
name: `${i18n('trash')}`,
|
||||
sortable: false,
|
||||
keep_in_taskbar: true,
|
||||
lock_keep_in_taskbar: true,
|
||||
onClick: function () {
|
||||
let open_windows = $(`.window[data-path="${html_encode(window.trash_path)}"]`);
|
||||
if ( open_windows.length === 0 ) {
|
||||
launch_app({ name: 'explorer', path: window.trash_path });
|
||||
} else {
|
||||
open_windows.focusWindow();
|
||||
}
|
||||
},
|
||||
onItemsDrop: function (items) {
|
||||
window.move_items(items, window.trash_path);
|
||||
},
|
||||
});
|
||||
|
||||
//---------------------------------------------
|
||||
// add separator before trash
|
||||
//---------------------------------------------
|
||||
UITaskbarItem({
|
||||
icon: '', // No icon for separator
|
||||
name: 'separator',
|
||||
app: 'separator',
|
||||
sortable: false,
|
||||
keep_in_taskbar: true,
|
||||
lock_keep_in_taskbar: true,
|
||||
disable_context_menu: true,
|
||||
style: 'pointer-events: none;', // Make it non-interactive
|
||||
onClick: function () {
|
||||
// Separator is non-interactive
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
window.make_taskbar_sortable();
|
||||
}
|
||||
|
||||
//-------------------------------------------
|
||||
// Taskbar is sortable
|
||||
//-------------------------------------------
|
||||
window.make_taskbar_sortable = function () {
|
||||
const position = window.taskbar_position || 'bottom';
|
||||
const axis = position === 'bottom' ? 'x' : 'y';
|
||||
|
||||
$('.taskbar-sortable').sortable({
|
||||
axis: axis,
|
||||
items: '.taskbar-item-sortable:not(.has-open-contextmenu):not([data-app="separator"])',
|
||||
cancel: '.has-open-contextmenu',
|
||||
placeholder: 'taskbar-item-sortable-placeholder',
|
||||
helper: 'clone',
|
||||
distance: 5,
|
||||
revert: 10,
|
||||
update: function (event, ui) {
|
||||
// only update the DB if the sorted item is pinned
|
||||
if ( $(ui.item).attr('data-keep-in-taskbar') === 'true' ) {
|
||||
window.update_taskbar();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Function to update taskbar position
|
||||
window.update_taskbar_position = async function (new_position) {
|
||||
// Prevent position changes on mobile devices - always keep bottom
|
||||
if ( isMobile.phone || isMobile.tablet ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! TASKBAR_POSITIONS.includes(new_position) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the new position
|
||||
puter.kv.set('taskbar_position', new_position).catch(err => console.warn('Could not save taskbar_position:', err));
|
||||
window.taskbar_position = new_position;
|
||||
|
||||
// Remove old position classes and add new one
|
||||
$('.taskbar').removeClass('taskbar-position-left taskbar-position-bottom taskbar-position-right');
|
||||
$('.taskbar').addClass(`taskbar-position-${new_position}`);
|
||||
|
||||
// update desktop class, if left or right, add `desktop-taskbar-position-left` or `desktop-taskbar-position-right`
|
||||
$('.desktop').removeClass('desktop-taskbar-position-left');
|
||||
$('.desktop').removeClass('desktop-taskbar-position-right');
|
||||
$('.desktop').addClass(`desktop-taskbar-position-${new_position}`);
|
||||
|
||||
// Update desktop height/width calculations based on new position
|
||||
window.update_desktop_dimensions_for_taskbar();
|
||||
|
||||
// Update window positions if needed (for maximized windows)
|
||||
$('.window[data-is_maximized="1"]').each(function () {
|
||||
const el_window = this;
|
||||
window.update_maximized_window_for_taskbar(el_window);
|
||||
});
|
||||
|
||||
// Re-initialize sortable with correct axis
|
||||
$('.taskbar-sortable').sortable('destroy');
|
||||
window.make_taskbar_sortable();
|
||||
|
||||
// Adjust taskbar item sizes for the new position
|
||||
setTimeout(() => {
|
||||
window.adjust_taskbar_item_sizes();
|
||||
}, 10);
|
||||
|
||||
// Reinitialize all taskbar item tooltips with new position
|
||||
$('.taskbar-item').each(function () {
|
||||
const $item = $(this);
|
||||
// Destroy existing tooltip
|
||||
if ( $item.data('ui-tooltip') ) {
|
||||
$item.tooltip('destroy');
|
||||
}
|
||||
|
||||
// Helper function to get tooltip position based on taskbar position
|
||||
function getTooltipPosition () {
|
||||
const taskbarPosition = window.taskbar_position || 'bottom';
|
||||
|
||||
if ( taskbarPosition === 'bottom' ) {
|
||||
return {
|
||||
my: 'center bottom-20',
|
||||
at: 'center top',
|
||||
};
|
||||
} else if ( taskbarPosition === 'top' ) {
|
||||
return {
|
||||
my: 'center top+20',
|
||||
at: 'center bottom',
|
||||
};
|
||||
} else if ( taskbarPosition === 'left' ) {
|
||||
return {
|
||||
my: 'left+20 center',
|
||||
at: 'right center',
|
||||
};
|
||||
} else if ( taskbarPosition === 'right' ) {
|
||||
return {
|
||||
my: 'right-20 center',
|
||||
at: 'left center',
|
||||
};
|
||||
}
|
||||
return {
|
||||
my: 'center bottom-20',
|
||||
at: 'center top',
|
||||
}; // fallback
|
||||
}
|
||||
|
||||
const tooltipPosition = getTooltipPosition();
|
||||
|
||||
// Reinitialize tooltip with new position
|
||||
$item.tooltip({
|
||||
items: ".taskbar:not(.children-have-open-contextmenu) .taskbar-item:not([data-app='separator'])",
|
||||
position: {
|
||||
my: tooltipPosition.my,
|
||||
at: tooltipPosition.at,
|
||||
using: function ( position, feedback ) {
|
||||
$(this).css( position);
|
||||
$('<div>')
|
||||
.addClass( 'arrow')
|
||||
.addClass( feedback.vertical)
|
||||
.addClass( feedback.horizontal)
|
||||
.appendTo( this);
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Function to update desktop dimensions based on taskbar position
|
||||
window.update_desktop_dimensions_for_taskbar = function () {
|
||||
const position = window.taskbar_position || 'bottom';
|
||||
|
||||
if ( position === 'bottom' ) {
|
||||
$('.desktop').css({
|
||||
'height': `calc(100vh - ${window.taskbar_height + window.toolbar_height}px)`,
|
||||
'width': '100%',
|
||||
'left': '0',
|
||||
'top': `${window.toolbar_height}px`,
|
||||
});
|
||||
} else if ( position === 'left' ) {
|
||||
$('.desktop').css({
|
||||
'height': `calc(100vh - ${window.toolbar_height}px)`,
|
||||
'width': `calc(100% - ${window.taskbar_height}px)`,
|
||||
'left': `${window.taskbar_height}px`,
|
||||
'top': `${window.toolbar_height}px`,
|
||||
});
|
||||
} else if ( position === 'right' ) {
|
||||
$('.desktop').css({
|
||||
'height': `calc(100vh - ${window.toolbar_height}px)`,
|
||||
'width': `calc(100% - ${window.taskbar_height}px)`,
|
||||
'left': '0',
|
||||
'top': `${window.toolbar_height}px`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//-------------------------------------------
|
||||
// Dynamic taskbar item resizing for left/right positions
|
||||
//-------------------------------------------
|
||||
window.adjust_taskbar_item_sizes = function () {
|
||||
const position = window.taskbar_position || 'bottom';
|
||||
|
||||
// Only apply to left and right positions
|
||||
if ( position !== 'left' && position !== 'right' ) {
|
||||
// Reset to default sizes for bottom position
|
||||
$('.taskbar .taskbar-item').css({
|
||||
'width': '40px',
|
||||
'height': '40px',
|
||||
'min-width': '40px',
|
||||
'min-height': '40px',
|
||||
});
|
||||
$('.taskbar-icon').css('height', '40px');
|
||||
return;
|
||||
}
|
||||
|
||||
const taskbar = $('.taskbar')[0];
|
||||
const taskbarItems = $('.taskbar .taskbar-item:visible');
|
||||
|
||||
if ( !taskbar || taskbarItems.length === 0 ) return;
|
||||
|
||||
// Get available height (minus padding)
|
||||
const totalItemsNeeded = taskbarItems.length;
|
||||
const taskbarHeight = taskbar.clientHeight;
|
||||
const paddingTop = 20; // from CSS
|
||||
const paddingBottom = 20; // from CSS
|
||||
const availableHeight = taskbarHeight - paddingTop - paddingBottom - 180;
|
||||
|
||||
// Calculate space needed with default sizes
|
||||
const defaultItemSize = 40;
|
||||
const defaultMargin = 5;
|
||||
const spaceNeededDefault = (totalItemsNeeded * defaultItemSize) + ((totalItemsNeeded - 1) * defaultMargin);
|
||||
|
||||
if ( spaceNeededDefault <= availableHeight ) {
|
||||
// No overflow, use default sizes
|
||||
taskbarItems.css({
|
||||
'width': '40px',
|
||||
'height': '40px',
|
||||
'min-width': '40px',
|
||||
'min-height': '40px',
|
||||
'padding': '6px 5px 10px 5px', // default padding
|
||||
});
|
||||
$('.taskbar-icon').css('height', `${defaultItemSize }px`);
|
||||
$('.taskbar-icon').css('width', '40px');
|
||||
$('.taskbar-icon > img').css('width', 'auto');
|
||||
$('.taskbar-icon > img').css('margin', 'auto');
|
||||
$('.taskbar-icon > img').css('display', 'block');
|
||||
|
||||
// Reset margins to default
|
||||
taskbarItems.css('margin-bottom', '5px');
|
||||
taskbarItems.last().css('margin-bottom', '0px');
|
||||
} else {
|
||||
// Overflow detected, calculate smaller sizes
|
||||
// Reserve some margin space (minimum 2px between items)
|
||||
const minMargin = 2;
|
||||
const marginSpace = (totalItemsNeeded - 1) * minMargin;
|
||||
const availableForItems = availableHeight - marginSpace;
|
||||
const newItemSize = Math.floor(availableForItems / totalItemsNeeded);
|
||||
|
||||
// Ensure minimum size of 20px
|
||||
const finalItemSize = Math.max(20, newItemSize);
|
||||
|
||||
// Calculate proportional padding based on size ratio
|
||||
const sizeRatio = finalItemSize / defaultItemSize;
|
||||
const paddingTop = Math.max(1, Math.floor(6 * sizeRatio));
|
||||
const paddingRight = Math.max(1, Math.floor(5 * sizeRatio));
|
||||
const paddingBottom = Math.max(1, Math.floor(10 * sizeRatio));
|
||||
const paddingLeft = Math.max(1, Math.floor(5 * sizeRatio));
|
||||
|
||||
// Apply new sizes and padding
|
||||
taskbarItems.css({
|
||||
'width': '40px',
|
||||
'height': `${finalItemSize }px`,
|
||||
'min-width': '40px',
|
||||
'min-height': `${finalItemSize }px`,
|
||||
'padding': `${paddingTop}px ${paddingRight}px ${paddingBottom}px ${paddingLeft}px`,
|
||||
});
|
||||
$('.taskbar-icon').css('height', `${finalItemSize }px`);
|
||||
$('.taskbar-icon').css('width', '40px');
|
||||
$('.taskbar-icon > img').css('width', 'auto');
|
||||
$('.taskbar-icon > img').css('margin', 'auto');
|
||||
$('.taskbar-icon > img').css('display', 'block');
|
||||
// Adjust margins
|
||||
taskbarItems.css('margin-bottom', `${minMargin }px`);
|
||||
taskbarItems.last().css('margin-bottom', '0px');
|
||||
}
|
||||
};
|
||||
|
||||
// Hook into existing taskbar functionality
|
||||
$(document).ready(function () {
|
||||
// Watch for taskbar item changes
|
||||
const observer = new MutationObserver(function (mutations) {
|
||||
mutations.forEach(function (mutation) {
|
||||
if ( mutation.type === 'childList' || mutation.type === 'attributes' ) {
|
||||
// Delay to ensure DOM updates are complete
|
||||
setTimeout(() => {
|
||||
window.adjust_taskbar_item_sizes();
|
||||
}, 10);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Start observing when taskbar is available
|
||||
const checkTaskbar = setInterval(() => {
|
||||
const taskbar = document.querySelector('.taskbar-sortable');
|
||||
if ( taskbar ) {
|
||||
observer.observe(taskbar, {
|
||||
childList: true,
|
||||
attributes: true,
|
||||
subtree: true,
|
||||
});
|
||||
clearInterval(checkTaskbar);
|
||||
|
||||
// Initial call
|
||||
setTimeout(() => {
|
||||
window.adjust_taskbar_item_sizes();
|
||||
}, 100);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// Also watch for window resize events
|
||||
window.addEventListener('resize', () => {
|
||||
setTimeout(() => {
|
||||
window.adjust_taskbar_item_sizes();
|
||||
}, 10);
|
||||
});
|
||||
});
|
||||
|
||||
export default UITaskbar;
|
||||
@@ -1,75 +0,0 @@
|
||||
// @vitest-environment jsdom
|
||||
/*
|
||||
* 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 { afterEach, beforeEach, expect, it, vi } from 'vitest';
|
||||
import jQuery from '../lib/jquery-3.6.1/jquery-3.6.1.min.js';
|
||||
|
||||
vi.mock('../helpers/launchApp.js', () => ({ default: vi.fn() }));
|
||||
|
||||
globalThis.$ = globalThis.jQuery = jQuery;
|
||||
globalThis.i18n = (key) => key;
|
||||
globalThis.html_encode = (value) => String(value ?? '');
|
||||
globalThis.isMobile = { phone: false, tablet: false };
|
||||
await import('../lib/jquery-ui-1.13.2/jquery-ui.js');
|
||||
const { default: UITaskbar } = await import('./UITaskbar.js');
|
||||
|
||||
let requests;
|
||||
|
||||
afterEach(() => {
|
||||
$('.taskbar').remove();
|
||||
$(document).off();
|
||||
$(window).off();
|
||||
document.body.innerHTML = '';
|
||||
vi.unstubAllGlobals();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
document.body.innerHTML = '<div class="desktop"></div>';
|
||||
Object.assign(window, {
|
||||
global_element_id: 0,
|
||||
taskbar_height: 50,
|
||||
toolbar_height: 0,
|
||||
api_origin: 'http://api.test',
|
||||
auth_token: 'test-token',
|
||||
icons: {},
|
||||
user: { taskbar_items: [{ name: 'notes', title: 'Notes' }] },
|
||||
trash_path: '/test/Trash',
|
||||
});
|
||||
vi.stubGlobal('puter', {
|
||||
kv: { get: async () => 'bottom' },
|
||||
fs: { stat: async () => ({ is_empty: true }) },
|
||||
});
|
||||
requests = [];
|
||||
$.ajaxTransport('+*', (options) => {
|
||||
requests.push(options.url);
|
||||
return { send () {}, abort () {} };
|
||||
});
|
||||
await UITaskbar();
|
||||
});
|
||||
|
||||
it('starts the taskbar at Explorer, followed by pinned apps and Trash', () => {
|
||||
const apps = $('.taskbar .taskbar-item').map((_, el) => el.dataset.app).get();
|
||||
expect(apps).toEqual(['explorer', 'separator', 'notes', 'separator', 'trash']);
|
||||
});
|
||||
|
||||
it('does not fetch launch apps when the taskbar loads', () => {
|
||||
expect(requests.filter(url => url.includes('/get-launch-apps'))).toEqual([]);
|
||||
});
|
||||
@@ -1,531 +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 UIContextMenu from './UIContextMenu.js';
|
||||
import path from '../lib/path.js';
|
||||
import launch_app from '../helpers/launchApp.js';
|
||||
import { user_facing_windows } from '../helpers/windowVisibility.js';
|
||||
import { appIconFallbackAttr } from '../helpers/appIcon.js';
|
||||
|
||||
let tray_item_id = 1;
|
||||
|
||||
function UITaskbarItem (options) {
|
||||
let h = '';
|
||||
tray_item_id++;
|
||||
options.sortable = options.sortable ?? true;
|
||||
options.open_windows_count = options.open_windows_count ?? 0;
|
||||
options.lock_keep_in_taskbar = options.lock_keep_in_taskbar ?? false;
|
||||
options.before_trash = options.before_trash ?? false;
|
||||
|
||||
// The windows this item stands for: the user's own. An instance another
|
||||
// app launched in the background is not counted in data-open-windows and
|
||||
// has no item of its own (see UIWindow's taskbar block) — it must not be
|
||||
// shown, hidden, or closed through the item its app's OTHER windows own.
|
||||
const item_windows = () => $(user_facing_windows($(`.window[data-app="${options.app}"]`)));
|
||||
|
||||
const element_id = window.global_element_id++;
|
||||
|
||||
h += `<div class = "taskbar-item ${options.sortable ? 'taskbar-item-sortable' : ''} disable-user-select"
|
||||
id = "taskbar-item-${tray_item_id}"
|
||||
data-taskbar-item-id = "${tray_item_id}"
|
||||
data-element-id = "${html_encode(element_id)}"
|
||||
data-name = "${html_encode(options.name)}"
|
||||
data-app = "${html_encode(options.app)}"
|
||||
data-keep-in-taskbar = "${html_encode(options.keep_in_taskbar ?? 'false')}"
|
||||
data-open-windows="${(options.open_windows_count)}"
|
||||
title = "${html_encode(options.name)}"
|
||||
style= "${options.style ? html_encode(options.style) : ''}"
|
||||
>`;
|
||||
let icon = options.icon ? options.icon : window.icons['app.svg'];
|
||||
// App icons carry a retry URL (see helpers/appIcon.js); the bundled icons
|
||||
// below never need one.
|
||||
let icon_fallback = options.iconFallback ?? '';
|
||||
if ( options.app === 'explorer' )
|
||||
{
|
||||
icon = window.icons['folders.svg'];
|
||||
icon_fallback = '';
|
||||
}
|
||||
|
||||
// taskbar icon
|
||||
h += '<div class="taskbar-icon">';
|
||||
// Don't add img tag for separator
|
||||
if ( options.app !== 'separator' ) {
|
||||
h += `<img src="${html_encode(icon)}"${appIconFallbackAttr(icon_fallback)} style="${options.group === 'apps' ? 'filter:none;' : ''}">`;
|
||||
}
|
||||
h += '</div>';
|
||||
|
||||
// active indicator
|
||||
if ( options.app !== 'apps' )
|
||||
{
|
||||
h += '<span class="active-taskbar-indicator"></span>';
|
||||
}
|
||||
h += '</div>';
|
||||
|
||||
if ( options.before_trash ) {
|
||||
$('.taskbar-sortable').append(h);
|
||||
} else {
|
||||
if ( options.sortable )
|
||||
{
|
||||
$('.taskbar-sortable').append(h);
|
||||
}
|
||||
else {
|
||||
// if taskbar-sortable is empty then append before it
|
||||
if ( $('.taskbar-sortable').children().length === 0 )
|
||||
{
|
||||
$('.taskbar').find('.taskbar-sortable').before(h);
|
||||
}
|
||||
else
|
||||
{
|
||||
$('.taskbar').find('.taskbar-sortable').after(h);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const el_taskbar_item = document.querySelector(`#taskbar-item-${tray_item_id}`);
|
||||
|
||||
// fade in the taskbar item
|
||||
$(el_taskbar_item).show(50);
|
||||
|
||||
// Adjust taskbar item sizes after adding new item
|
||||
if ( window.adjust_taskbar_item_sizes ) {
|
||||
setTimeout(() => {
|
||||
window.adjust_taskbar_item_sizes();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
$(el_taskbar_item).on('click', function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Don't handle clicks for separators
|
||||
if ( options.app === 'separator' ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If this item has an open context menu, don't do anything
|
||||
if ( $(el_taskbar_item).hasClass('has-open-contextmenu') )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if ( options.onClick === undefined || options.onClick(el_taskbar_item) === false ) {
|
||||
// re-show each window in this app group
|
||||
item_windows().showWindow();
|
||||
}
|
||||
});
|
||||
|
||||
$(el_taskbar_item).on('contextmenu taphold', function (e) {
|
||||
// seems like the only way to stop sortable is to destroy it
|
||||
if ( options.sortable ) {
|
||||
$('.taskbar-sortable').sortable('destroy');
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Don't show context menu for separators
|
||||
if ( options.app === 'separator' ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If context menu is disabled on this item, return
|
||||
if ( options.disable_context_menu )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// don't allow context menu to open if it's already open
|
||||
if ( $(el_taskbar_item).hasClass('has-open-contextmenu') )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const menu_items = [];
|
||||
const open_windows = parseInt($(el_taskbar_item).attr('data-open-windows'));
|
||||
// -------------------------------------------
|
||||
// List of open windows belonging to this app
|
||||
// -------------------------------------------
|
||||
item_windows().each(function () {
|
||||
menu_items.push({
|
||||
html: $(this).find('.window-head-title').html(),
|
||||
val: $(this).attr('data-id'),
|
||||
onClick: function (e) {
|
||||
$(`.window[data-id="${e.value}"]`).showWindow();
|
||||
},
|
||||
});
|
||||
});
|
||||
// -------------------------------------------
|
||||
// divider
|
||||
// -------------------------------------------
|
||||
if ( menu_items.length > 0 )
|
||||
{
|
||||
menu_items.push('-');
|
||||
}
|
||||
//------------------------------------------
|
||||
// New Window
|
||||
//------------------------------------------
|
||||
if ( options.app && options.app !== 'trash' ) {
|
||||
menu_items.push({
|
||||
html: i18n('new_window'),
|
||||
val: $(this).attr('data-id'),
|
||||
onClick: function () {
|
||||
// is trash?
|
||||
launch_app({
|
||||
name: options.app,
|
||||
maximized: (isMobile.phone || isMobile.tablet),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
//------------------------------------------
|
||||
// Open Trash
|
||||
//------------------------------------------
|
||||
else if ( options.app && options.app === 'trash' ) {
|
||||
menu_items.push({
|
||||
html: i18n('open_trash'),
|
||||
val: $(this).attr('data-id'),
|
||||
onClick: function () {
|
||||
launch_app({
|
||||
name: options.app,
|
||||
path: window.trash_path,
|
||||
maximized: (isMobile.phone || isMobile.tablet),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
//------------------------------------------
|
||||
// Empty Trash
|
||||
//------------------------------------------
|
||||
if ( options.app && options.app === 'trash' ) {
|
||||
// divider
|
||||
menu_items.push('-');
|
||||
|
||||
// Empty Trash menu item
|
||||
menu_items.push({
|
||||
html: i18n('empty_trash'),
|
||||
val: $(this).attr('data-id'),
|
||||
onClick: async function () {
|
||||
window.empty_trash();
|
||||
},
|
||||
});
|
||||
}
|
||||
//------------------------------------------
|
||||
// Remove from Taskbar
|
||||
//------------------------------------------
|
||||
if ( options.keep_in_taskbar && !options.lock_keep_in_taskbar ) {
|
||||
menu_items.push({
|
||||
html: i18n('remove_from_taskbar'),
|
||||
val: $(this).attr('data-id'),
|
||||
onClick: function () {
|
||||
$(el_taskbar_item).attr('data-keep-in-taskbar', 'false');
|
||||
if ( $(el_taskbar_item).attr('data-open-windows') === '0' ) {
|
||||
window.remove_taskbar_item(el_taskbar_item);
|
||||
}
|
||||
window.update_taskbar();
|
||||
options.keep_in_taskbar = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
//------------------------------------------
|
||||
// Keep in Taskbar
|
||||
//------------------------------------------
|
||||
else if ( ! options.keep_in_taskbar ) {
|
||||
menu_items.push({
|
||||
html: i18n('keep_in_taskbar'),
|
||||
val: $(this).attr('data-id'),
|
||||
onClick: function () {
|
||||
$(el_taskbar_item).attr('data-keep-in-taskbar', 'true');
|
||||
window.update_taskbar();
|
||||
options.keep_in_taskbar = true;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if ( open_windows > 0 ) {
|
||||
// -------------------------------------------
|
||||
// divider
|
||||
// -------------------------------------------
|
||||
menu_items.push('-');
|
||||
// -------------------------------------------
|
||||
// Show All Windows
|
||||
// -------------------------------------------
|
||||
menu_items.push({
|
||||
html: i18n('show_all_windows'),
|
||||
onClick: function () {
|
||||
item_windows().showWindow();
|
||||
},
|
||||
});
|
||||
// -------------------------------------------
|
||||
// Hide All Windows
|
||||
// -------------------------------------------
|
||||
menu_items.push({
|
||||
html: i18n('hide_all_windows'),
|
||||
onClick: function () {
|
||||
if ( open_windows > 0 )
|
||||
{
|
||||
item_windows().hideWindow();
|
||||
}
|
||||
},
|
||||
});
|
||||
// -------------------------------------------
|
||||
// Close All Windows
|
||||
// -------------------------------------------
|
||||
menu_items.push({
|
||||
html: i18n('close_all_windows'),
|
||||
onClick: function () {
|
||||
// "All" means the windows this item lists — a background
|
||||
// helper belongs to the app running it, and goes when
|
||||
// that app does (UIWindow's close path).
|
||||
item_windows().close();
|
||||
},
|
||||
});
|
||||
}
|
||||
const pos = el_taskbar_item.getBoundingClientRect();
|
||||
UIContextMenu({
|
||||
parent_element: el_taskbar_item,
|
||||
position: getContextMenuPosition(pos),
|
||||
items: menu_items,
|
||||
});
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
// Helper function to get tooltip position based on taskbar position
|
||||
function getTooltipPosition () {
|
||||
const taskbarPosition = window.taskbar_position || 'bottom';
|
||||
|
||||
if ( taskbarPosition === 'bottom' ) {
|
||||
return {
|
||||
my: 'center bottom-20',
|
||||
at: 'center top',
|
||||
};
|
||||
} else if ( taskbarPosition === 'top' ) {
|
||||
return {
|
||||
my: 'center top+20',
|
||||
at: 'center bottom',
|
||||
};
|
||||
} else if ( taskbarPosition === 'left' ) {
|
||||
return {
|
||||
my: 'left+20 center',
|
||||
at: 'right center',
|
||||
};
|
||||
} else if ( taskbarPosition === 'right' ) {
|
||||
return {
|
||||
my: 'right-20 center',
|
||||
at: 'left center',
|
||||
};
|
||||
}
|
||||
return {
|
||||
my: 'center bottom-20',
|
||||
at: 'center top',
|
||||
}; // fallback
|
||||
}
|
||||
|
||||
// Helper function to get context menu position based on taskbar position
|
||||
function getContextMenuPosition (pos) {
|
||||
const taskbarPosition = window.taskbar_position || 'bottom';
|
||||
|
||||
if ( taskbarPosition === 'bottom' ) {
|
||||
return {
|
||||
top: pos.top - 15,
|
||||
left: pos.left + 5,
|
||||
};
|
||||
} else if ( taskbarPosition === 'top' ) {
|
||||
return {
|
||||
top: pos.bottom + 15,
|
||||
left: pos.left + 5,
|
||||
};
|
||||
} else if ( taskbarPosition === 'left' ) {
|
||||
return {
|
||||
top: pos.top + 5,
|
||||
left: pos.right + 5,
|
||||
};
|
||||
} else if ( taskbarPosition === 'right' ) {
|
||||
return {
|
||||
top: pos.top + 5,
|
||||
left: pos.left - 20,
|
||||
};
|
||||
}
|
||||
return {
|
||||
top: pos.top - 15,
|
||||
left: pos.left + 5,
|
||||
}; // fallback
|
||||
}
|
||||
|
||||
const tooltipPosition = getTooltipPosition();
|
||||
|
||||
$(el_taskbar_item).tooltip({
|
||||
// only show tooltip if desktop is not selectable active
|
||||
items: ".desktop:not(.desktop-selectable-active) .taskbar:not(.children-have-open-contextmenu) .taskbar-item:not([data-app='separator'])",
|
||||
position: {
|
||||
my: tooltipPosition.my,
|
||||
at: tooltipPosition.at,
|
||||
using: function ( position, feedback ) {
|
||||
$(this).css( position);
|
||||
$('<div>')
|
||||
.addClass( 'arrow')
|
||||
.addClass( feedback.vertical)
|
||||
.addClass( feedback.horizontal)
|
||||
.appendTo( this);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Droppable
|
||||
// --------------------------------------------------------
|
||||
// Don't make separators droppable
|
||||
if ( options.app !== 'separator' ) {
|
||||
$(el_taskbar_item).droppable({
|
||||
accept: '.item',
|
||||
// 'pointer' is very important because of active window tracking is based on the position of cursor.
|
||||
tolerance: 'pointer',
|
||||
drop: async function ( event, ui ) {
|
||||
// Check if hovering over an item that is VISIBILE
|
||||
if ( $(event.target).closest('.window').attr('data-id') !== $(window.mouseover_window).attr('data-id') )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If ctrl is pressed and source is Trashed, cancel whole operation
|
||||
if ( event.ctrlKey && path.dirname($(ui.draggable).attr('data-path')) === window.trash_path )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const items_to_move = [];
|
||||
|
||||
// First item
|
||||
items_to_move.push(ui.draggable);
|
||||
|
||||
// All subsequent items
|
||||
const cloned_items = document.getElementsByClassName('item-selected-clone');
|
||||
for ( let i = 0; i < cloned_items.length; i++ ) {
|
||||
const source_item = document.getElementById(`item-${ $(cloned_items[i]).attr('data-id')}`);
|
||||
if ( source_item !== null )
|
||||
{
|
||||
items_to_move.push(source_item);
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// If `options.onItemsDrop` is set, call it with the items to move
|
||||
//--------------------------------------------------------
|
||||
if ( options.onItemsDrop && typeof options.onItemsDrop === 'function' ) {
|
||||
options.onItemsDrop(items_to_move);
|
||||
return;
|
||||
}
|
||||
// --------------------------------------------------------
|
||||
// If dropped on an app, open the app with the dropped item as an argument
|
||||
//--------------------------------------------------------
|
||||
else if ( options.app ) {
|
||||
// an array that hold the items to sign
|
||||
const items_to_sign = [];
|
||||
|
||||
// prepare items to sign
|
||||
for ( let i = 0; i < items_to_move.length; i++ ) {
|
||||
items_to_sign.push({
|
||||
name: $(items_to_move[i]).attr('data-name'),
|
||||
uid: $(items_to_move[i]).attr('data-uid'),
|
||||
action: 'write',
|
||||
path: $(items_to_move[i]).attr('data-path'),
|
||||
});
|
||||
}
|
||||
|
||||
// open each item
|
||||
for ( let i = 0; i < items_to_sign.length; i++ ) {
|
||||
const item = items_to_sign[i];
|
||||
launch_app({
|
||||
name: options.app,
|
||||
file_path: item.path,
|
||||
// app_obj: open_item_meta.suggested_apps[0],
|
||||
window_title: item.name,
|
||||
file_uid: item.uid,
|
||||
// file_signature: item,
|
||||
});
|
||||
}
|
||||
|
||||
// deselect dragged item
|
||||
for ( let i = 0; i < items_to_move.length; i++ )
|
||||
{
|
||||
$(items_to_move[i]).removeClass('item-selected');
|
||||
}
|
||||
}
|
||||
|
||||
// Unselect directory/app if item is dropped
|
||||
if ( options.is_dir || options.app ) {
|
||||
$(el_taskbar_item).removeClass('active');
|
||||
$(el_taskbar_item).tooltip('close');
|
||||
$('.ui-draggable-dragging .item-name, .item-selected-clone .item-name').css('opacity', 'initial');
|
||||
$('.item-container').removeClass('item-container-transparent-border');
|
||||
}
|
||||
|
||||
// Re-enable droppable on all item-container
|
||||
$('.item-container').droppable('enable');
|
||||
|
||||
return false;
|
||||
},
|
||||
over: function (event, ui) {
|
||||
// Check hovering over an item that is VISIBILE
|
||||
const $event_parent_win = $(event.target).closest('.window');
|
||||
if ( $event_parent_win.length > 0 && $event_parent_win.attr('data-id') !== $(window.mouseover_window).attr('data-id') )
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Don't do anything if the dragged item is NOT a UIItem
|
||||
if ( ! $(ui.draggable).hasClass('item') )
|
||||
{
|
||||
return;
|
||||
}
|
||||
// If this is a directory or an app, and an item was dragged over it, highlight it.
|
||||
if ( options.is_dir || options.app ) {
|
||||
$(el_taskbar_item).addClass('active');
|
||||
// show tooltip of this item
|
||||
$(el_taskbar_item).tooltip().mouseover();
|
||||
// make item name partially transparent
|
||||
$('.ui-draggable-dragging .item-name, .item-selected-clone .item-name').css('opacity', 0.1);
|
||||
// remove all item-container active borders
|
||||
$('.item-container').addClass('item-container-transparent-border');
|
||||
}
|
||||
// Disable all window bodies
|
||||
$('.item-container').droppable( 'disable');
|
||||
},
|
||||
out: function (event, ui) {
|
||||
// Don't do anything if the dragged item is NOT a UIItem
|
||||
if ( ! $(ui.draggable).hasClass('item') )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Unselect directory/app if item is dragged out
|
||||
if ( options.is_dir || options.app ) {
|
||||
$(el_taskbar_item).removeClass('active');
|
||||
$(el_taskbar_item).tooltip('close');
|
||||
$('.ui-draggable-dragging .item-name, .item-selected-clone .item-name').css('opacity', 'initial');
|
||||
$('.item-container').removeClass('item-container-transparent-border');
|
||||
}
|
||||
$('.item-container').droppable( 'enable');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return el_taskbar_item;
|
||||
}
|
||||
|
||||
export default UITaskbarItem;
|
||||
@@ -0,0 +1,123 @@
|
||||
// @vitest-environment jsdom
|
||||
/*
|
||||
* 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 { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import jQuery from '../lib/jquery-3.6.1/jquery-3.6.1.min.js';
|
||||
|
||||
vi.mock('../helpers/launchApp.js', () => ({ default: vi.fn() }));
|
||||
|
||||
globalThis.$ = globalThis.jQuery = jQuery;
|
||||
globalThis.i18n = (key) => key;
|
||||
globalThis.html_encode = (value) => String(value ?? '');
|
||||
globalThis.isMobile = { phone: false, tablet: false };
|
||||
globalThis.def = globalThis.def ?? ((value) => value);
|
||||
await import('../lib/jquery-ui-1.13.2/jquery-ui.js');
|
||||
// The page loads this plugin from a vendored script; nothing here drops files.
|
||||
$.fn.dragster = function () {
|
||||
return this;
|
||||
};
|
||||
const { default: UIWindow } = await import('./UIWindow.js');
|
||||
|
||||
const openWindow = (options = {}) => UIWindow({
|
||||
title: 'Notes',
|
||||
app: 'notes',
|
||||
body_content: '<p>hello</p>',
|
||||
width: 400,
|
||||
height: 300,
|
||||
...options,
|
||||
});
|
||||
|
||||
/** Opens the window head's context menu and returns its item labels. */
|
||||
const headMenuLabels = (el_window) => {
|
||||
$(el_window).find('.window-head').trigger('contextmenu');
|
||||
return $('.context-menu .contextmenu-label').map((_, el) => $(el).text().toLowerCase()).get();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = '<div class="desktop"></div>';
|
||||
Object.assign(window, {
|
||||
global_element_id: 0,
|
||||
window_counter: 0,
|
||||
last_window_zindex: 1,
|
||||
window_stack: [],
|
||||
toolbar_height: 0,
|
||||
icons: {},
|
||||
uuidv4: () => crypto.randomUUID(),
|
||||
is_dashboard_mode: false,
|
||||
is_fullpage_mode: false,
|
||||
is_embedded: false,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
$(document).off();
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
describe('minimize', () => {
|
||||
it('is not offered on the desktop, where nothing could restore the window', async () => {
|
||||
const el_window = await openWindow();
|
||||
|
||||
expect($(el_window).find('.window-minimize-btn').length).toBe(0);
|
||||
|
||||
const labels = headMenuLabels(el_window);
|
||||
expect(labels.some((label) => label === 'maximize')).toBe(true);
|
||||
expect(labels.some((label) => label === 'minimize')).toBe(false);
|
||||
});
|
||||
|
||||
it('is offered on the dashboard, which restores the window from its tile', async () => {
|
||||
window.is_dashboard_mode = true;
|
||||
const el_window = await openWindow();
|
||||
|
||||
expect($(el_window).find('.window-minimize-btn').length).toBe(1);
|
||||
expect(headMenuLabels(el_window).some((label) => label === 'minimize')).toBe(true);
|
||||
});
|
||||
|
||||
it('hides the window in place and brings it back', async () => {
|
||||
window.is_dashboard_mode = true;
|
||||
const el_window = await openWindow();
|
||||
const before = { top: el_window.style.top, left: el_window.style.left };
|
||||
|
||||
await $(el_window).hideWindow();
|
||||
expect($(el_window).attr('data-is_minimized')).toBe('true');
|
||||
expect($(el_window).attr('data-minimized_in_place')).toBe('1');
|
||||
|
||||
await $(el_window).showWindow({ no_history: true });
|
||||
expect($(el_window).attr('data-is_minimized')).toBe('false');
|
||||
expect({ top: el_window.style.top, left: el_window.style.left }).toEqual(before);
|
||||
});
|
||||
});
|
||||
|
||||
describe('backdrop', () => {
|
||||
const backdropZ = (el_window) => Number($(el_window).closest('.window-backdrop').css('z-index'));
|
||||
|
||||
it('stays above ordinary windows on the desktop', async () => {
|
||||
const el_window = await openWindow({ backdrop: true });
|
||||
|
||||
expect(backdropZ(el_window)).toBeGreaterThan(window.last_window_zindex + 1000);
|
||||
});
|
||||
|
||||
it('stacks at the window counter on the dashboard', async () => {
|
||||
window.is_dashboard_mode = true;
|
||||
const el_window = await openWindow({ backdrop: true });
|
||||
|
||||
expect(backdropZ(el_window)).toBeLessThanOrEqual(window.last_window_zindex);
|
||||
});
|
||||
});
|
||||
+251
-532
File diff suppressed because it is too large.
Load diff
@@ -782,7 +782,6 @@ const UIWindow2FASetup = async function UIWindow2FASetup () {
|
||||
width: Math.min(480, window.innerWidth - 24),
|
||||
height: 'auto',
|
||||
dominant: true,
|
||||
show_in_taskbar: false,
|
||||
is_draggable: false,
|
||||
backdrop: true,
|
||||
on_before_exit: async () => {
|
||||
|
||||
@@ -215,7 +215,6 @@ async function UIWindowAuthMe (options = {}) {
|
||||
width: 400,
|
||||
height: 'auto',
|
||||
dominant: true,
|
||||
show_in_taskbar: false,
|
||||
window_class: 'window-authme',
|
||||
body_css: {
|
||||
width: 'initial',
|
||||
|
||||
@@ -87,7 +87,6 @@ async function UIWindowBlockedSenders (options) {
|
||||
width: 420,
|
||||
height: 'auto',
|
||||
dominant: true,
|
||||
show_in_taskbar: false,
|
||||
onAppend: function (this_window) {
|
||||
$(this_window).find('.blocked-username').get(0)?.focus({ preventScroll: true });
|
||||
},
|
||||
|
||||
@@ -68,7 +68,6 @@ async function UIWindowChangeUsername (options) {
|
||||
width: 350,
|
||||
height: 'auto',
|
||||
dominant: true,
|
||||
show_in_taskbar: false,
|
||||
onAppend: function (this_window) {
|
||||
$(this_window).find('.new-username').get(0)?.focus({ preventScroll: true });
|
||||
const oidc_only = !!(window.user && window.user.oidc_only);
|
||||
|
||||
@@ -114,7 +114,6 @@ function UIWindowCopyToken (options = {}) {
|
||||
width: 450,
|
||||
height: 'auto',
|
||||
dominant: true,
|
||||
show_in_taskbar: false,
|
||||
window_class: 'window-copy-auth-token',
|
||||
body_css: {
|
||||
width: 'initial',
|
||||
|
||||
@@ -57,7 +57,6 @@ async function UIWindowLoginInProgress (options) {
|
||||
width: 350,
|
||||
height: 'auto',
|
||||
dominant: true,
|
||||
show_in_taskbar: false,
|
||||
backdrop: true,
|
||||
stay_on_top: true,
|
||||
window_class: 'window-login-progress',
|
||||
|
||||
@@ -100,7 +100,6 @@ async function UIWindowNewPassword (options) {
|
||||
width: 350,
|
||||
height: 'auto',
|
||||
dominant: true,
|
||||
show_in_taskbar: false,
|
||||
onAppend: function (this_window) {
|
||||
$(this_window).find('.new-password').get(0)?.focus({ preventScroll: true });
|
||||
},
|
||||
|
||||
@@ -71,7 +71,6 @@ function UIWindowPasswordChangeRequired (options) {
|
||||
width: 390,
|
||||
height: 'auto',
|
||||
dominant: true,
|
||||
show_in_taskbar: false,
|
||||
onAppend: function (this_window) {
|
||||
$(this_window).find('.pcr-current').get(0)?.focus({ preventScroll: true });
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
@@ -91,7 +91,6 @@ async function UIWindowSaveAccount (options) {
|
||||
allow_user_select: true,
|
||||
width: 350,
|
||||
dominant: true,
|
||||
show_in_taskbar: false,
|
||||
...options.window_options,
|
||||
onAppend: function (this_window) {
|
||||
if ( options.default_username )
|
||||
|
||||
@@ -8691,13 +8691,6 @@ body.myapps-reordering .myapps-tile {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Dashboard mode runs fullpage (which normally hides the minimize button
|
||||
because there's no taskbar to restore from), but here a minimized app can
|
||||
be brought back from its Apps-tab tile — so keep the button available. */
|
||||
body.dashboard-mode .window-minimize-btn {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* Fullpage mode sizes .window-body-app to 100% because fullpage windows have
|
||||
no titlebar — but dashboard-mode app windows WITH a head still lose 29px to
|
||||
it, so compensate for those. Headless dashboard app windows
|
||||
|
||||
+7
-595
@@ -104,12 +104,6 @@ pre {
|
||||
--window-sidebar-alpha: calc(min(1, 0.11 + var(--primary-alpha)));
|
||||
--window-sidebar-color: var(--primary-color);
|
||||
|
||||
--taskbar-hue: var(--primary-hue);
|
||||
--taskbar-saturation: var(--primary-saturation);
|
||||
--taskbar-lightness: var(--primary-lightness);
|
||||
--taskbar-alpha: calc(0.73 * var(--primary-alpha));
|
||||
--taskbar-color: var(--primary-color);
|
||||
|
||||
--select-hue: 213.05;
|
||||
--select-saturation: 74.22%;
|
||||
--select-lightness: 55.88%;
|
||||
@@ -367,46 +361,15 @@ input[type=text]:focus, input[type=password]:focus, input[type=email]:focus, sel
|
||||
}
|
||||
|
||||
.desktop {
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
height: calc(100vh - 60px) !important;
|
||||
height: 100vh !important;
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-rows: repeat(auto-fill, 109px);
|
||||
grid-auto-flow: column;
|
||||
grid-template-columns: repeat(auto-fill, 120px);
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.device-desktop .desktop {
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
.desktop.desktop-taskbar-position-left {
|
||||
margin-left: 50px;
|
||||
padding-right: 0;
|
||||
padding-bottom: 0;
|
||||
height: calc(100vh) !important;
|
||||
}
|
||||
|
||||
.desktop.desktop-taskbar-position-right {
|
||||
margin-right: 50px;
|
||||
padding-left: 0;
|
||||
padding-bottom: 0;
|
||||
height: calc(100vh) !important;
|
||||
}
|
||||
|
||||
.fullpage-mode .window-minimize-btn {
|
||||
display: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.device-phone .desktop {
|
||||
height: calc(100vh - 90px) !important;
|
||||
height: calc(100dvh - 90px) !important;
|
||||
/* Ensure no left/right padding on mobile, regardless of taskbar position classes */
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
height: 100vh !important;
|
||||
height: 100dvh !important;
|
||||
}
|
||||
|
||||
.item-container-list {
|
||||
@@ -908,18 +871,6 @@ span.header-sort-icon img {
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
/* The desktop is a plain page color, so its labels need text that reads on it. */
|
||||
.desktop .item .item-name {
|
||||
color: rgb(73, 73, 73);
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.desktop .item .item-name {
|
||||
color: #e6e6e6;
|
||||
}
|
||||
}
|
||||
|
||||
.device-phone .item-container-list .item .item-name {
|
||||
line-height: 42px;
|
||||
border-bottom: 1px solid #e3e3e3;
|
||||
@@ -2060,7 +2011,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;
|
||||
@@ -2154,34 +2105,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;
|
||||
@@ -2518,253 +2441,6 @@ label {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/*****************************************************
|
||||
* Taskbar
|
||||
*****************************************************/
|
||||
|
||||
.taskbar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background-color: hsla(var(--taskbar-hue),
|
||||
var(--taskbar-saturation),
|
||||
var(--taskbar-lightness),
|
||||
calc(0.5 + 0.5*var(--taskbar-alpha)));
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
z-index: 99999;
|
||||
overflow: hidden !important;
|
||||
|
||||
height: 50px;
|
||||
border-radius: 10px;
|
||||
bottom: calc(5px + env(safe-area-inset-bottom, 0px));
|
||||
padding-left: 7px;
|
||||
padding-right: 7px;
|
||||
width: auto;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
|
||||
/* that sweet sweet subtle shadow */
|
||||
box-shadow:
|
||||
inset 0 0 0 0.5px rgba(255, 255, 255, 0.2),
|
||||
0 0 0 0.5px rgba(0, 0, 0, 0.2),
|
||||
0 4px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Bottom positioned taskbar (default) */
|
||||
.taskbar.taskbar-position-bottom {
|
||||
bottom: 5px;
|
||||
left: 50%;
|
||||
right: auto;
|
||||
top: auto;
|
||||
width: auto;
|
||||
height: 50px;
|
||||
transform: translateX(-50%);
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
writing-mode: initial;
|
||||
}
|
||||
|
||||
/* Left positioned taskbar */
|
||||
.taskbar.taskbar-position-left {
|
||||
left: 0 !important;
|
||||
top: 0;
|
||||
width: 50px;
|
||||
transform: none;
|
||||
height: 100% !important;
|
||||
flex-direction: column;
|
||||
justify-content: normal;
|
||||
writing-mode: initial;
|
||||
padding-top: 7px;
|
||||
padding-bottom: 7px;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* Right positioned taskbar */
|
||||
.taskbar.taskbar-position-right {
|
||||
right: 0 !important;
|
||||
top: 0;
|
||||
left: auto;
|
||||
bottom: auto;
|
||||
width: 50px;
|
||||
height: 100% !important;
|
||||
transform: none;
|
||||
flex-direction: column;
|
||||
justify-content: normal;
|
||||
writing-mode: initial;
|
||||
padding-top: 7px;
|
||||
padding-bottom: 7px;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.taskbar.taskbar-position-left .taskbar-sortable,
|
||||
.taskbar.taskbar-position-right .taskbar-sortable {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* Taskbar items for left/right positioning */
|
||||
.taskbar.taskbar-position-left .taskbar-item,
|
||||
.taskbar.taskbar-position-right .taskbar-item {
|
||||
margin-bottom: 5px;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.taskbar.taskbar-position-left .taskbar-item:last-child,
|
||||
.taskbar.taskbar-position-right .taskbar-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.taskbar .taskbar-item {
|
||||
float: left;
|
||||
position: relative;
|
||||
overflow: hidden !important;
|
||||
transition: background-color 0.2s;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.taskbar .taskbar-item-sortable-placeholder, .taskbar .taskbar-item {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 6px 5px 10px 5px;
|
||||
}
|
||||
|
||||
.taskbar .taskbar-item .taskbar-icon {
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.taskbar .taskbar-item, .taskbar .taskbar-item * {
|
||||
-webkit-user-drag: none;
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
.taskbar-item.ui-sortable-helper {
|
||||
margin-left: 25px;
|
||||
z-index: 999999999 !important;
|
||||
}
|
||||
|
||||
.desktop:not(.desktop-selectable-active) .taskbar .taskbar-item:hover .taskbar-icon {
|
||||
background-color: rgb(255 255 255 / 40%);
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.taskbar .taskbar-item:active .taskbar-icon,
|
||||
.taskbar .taskbar-item:focus-within .taskbar-icon,
|
||||
.taskbar .taskbar-item:focus-visible .taskbar-icon,
|
||||
.taskbar .taskbar-item:focus .taskbar-icon,
|
||||
.taskbar-item.has-open-contextmenu .taskbar-icon,
|
||||
.taskbar-item.has-open-popover .taskbar-icon,
|
||||
.taskbar .taskbar-item.active .taskbar-icon,
|
||||
.taskbar-item.ui-sortable-helper .taskbar-icon {
|
||||
background-color: rgb(255 255 255 / 80%) !important;
|
||||
transition: background-color 0.2s;
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.active-taskbar-indicator {
|
||||
font-size: 18px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
-webkit-transform: translateX(-50%);
|
||||
transform: translateX(-50%);
|
||||
bottom: -6px;
|
||||
display: none;
|
||||
width: 9px;
|
||||
height: 3px;
|
||||
background-color: #686868;
|
||||
bottom: 8px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.device-phone .active-taskbar-indicator {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.taskbar .taskbar-icon img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
filter: drop-shadow(0px 0px 0.2px rgb(51, 51, 51));
|
||||
padding: 5px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.taskbar-icon {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
/* Taskbar separator styling */
|
||||
.taskbar-item[data-app="separator"] {
|
||||
pointer-events: none !important;
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.taskbar-item[data-app="separator"] .taskbar-icon {
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
||||
/* Vertical separator for bottom taskbar */
|
||||
.taskbar.taskbar-position-bottom .taskbar-item[data-app="separator"] .taskbar-icon::after {
|
||||
content: '';
|
||||
width: 1px;
|
||||
height: 35px;
|
||||
max-height: 35px;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 0.5px;
|
||||
}
|
||||
|
||||
/* Horizontal separator for left/right taskbar */
|
||||
.taskbar.taskbar-position-left .taskbar-item[data-app="separator"] .taskbar-icon::after,
|
||||
.taskbar.taskbar-position-right .taskbar-item[data-app="separator"] .taskbar-icon::after {
|
||||
content: '';
|
||||
width: 35px;
|
||||
height: 1px;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 0.5px;
|
||||
}
|
||||
|
||||
/* Hide separator on mobile devices */
|
||||
.device-phone .taskbar-item[data-app="separator"],
|
||||
.device-tablet .taskbar-item[data-app="separator"] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.taskbar.taskbar-position-bottom .taskbar-item[data-app="separator"]{
|
||||
max-width: 10px;
|
||||
min-width: 10px !important;
|
||||
}
|
||||
|
||||
.taskbar.taskbar-position-bottom .taskbar-item[data-app="separator"] .taskbar-icon{
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.taskbar.taskbar-position-left .taskbar-item[data-app="separator"],
|
||||
.taskbar.taskbar-position-right .taskbar-item[data-app="separator"]{
|
||||
max-height: 10px;
|
||||
min-height: 10px !important;
|
||||
padding: 5px 3px 5px 7px !important;
|
||||
}
|
||||
.taskbar.taskbar-position-left .taskbar-item[data-app="separator"] .taskbar-icon,
|
||||
.taskbar.taskbar-position-right .taskbar-item[data-app="separator"] .taskbar-icon{
|
||||
max-height: 10px;
|
||||
min-height: 10px !important;
|
||||
padding-bottom: 5px !important;
|
||||
}
|
||||
|
||||
/*****************************************************
|
||||
* Captcha
|
||||
*****************************************************/
|
||||
@@ -2892,20 +2568,6 @@ label {
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@supports ((backdrop-filter: blur())) {
|
||||
.taskbar {
|
||||
background-color: hsla(var(--taskbar-hue),
|
||||
var(--taskbar-saturation),
|
||||
var(--taskbar-lightness),
|
||||
var(--taskbar-alpha));
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.taskbar .taskbar-icon img {
|
||||
filter: drop-shadow(0px 0px 0.5px rgb(51, 51, 51));
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.window-copy-auth-token {
|
||||
width: 100% !important;
|
||||
@@ -2923,172 +2585,6 @@ label {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.taskbar {
|
||||
justify-content: center;
|
||||
overflow: visible !important;
|
||||
overflow-x: scroll !important;
|
||||
overflow-y: hidden !important;
|
||||
max-width: calc(100% - 40px);
|
||||
}
|
||||
|
||||
.taskbar .taskbar-item, .taskbar .taskbar-item-sortable-placeholder {
|
||||
width: 40px !important;
|
||||
height: 40px !important;
|
||||
margin-right: 5px;
|
||||
overflow: visible !important;
|
||||
padding: 5px 5px 10px 5px;
|
||||
}
|
||||
|
||||
.taskbar-icon {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
.taskbar ::-webkit-scrollbar {
|
||||
width: 0 !important;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for IE, Edge and Firefox */
|
||||
.taskbar {
|
||||
-ms-overflow-style: none;
|
||||
/* IE and Edge */
|
||||
scrollbar-width: none;
|
||||
/* Firefox */
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*****************************************************
|
||||
* Tooltip
|
||||
*****************************************************/
|
||||
|
||||
.ui-tooltip, .arrow:after {
|
||||
background-color: rgba(231, 238, 245, .92);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.ui-tooltip {
|
||||
padding: 7px 11px;
|
||||
border-radius: 2px;
|
||||
font: 14px "Helvetica Neue", Sans-Serif;
|
||||
border: none !important;
|
||||
backdrop-filter: blur(3px);
|
||||
filter: drop-shadow(0 0 3px rgba(0, 0, 0, .455));
|
||||
}
|
||||
|
||||
/* Base arrow styles */
|
||||
.arrow {
|
||||
width: 70px;
|
||||
height: 16px;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
margin-left: -35px;
|
||||
bottom: -16px;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.arrow:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: -20px;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
-webkit-transform: rotate(45deg);
|
||||
-ms-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
background-color: rgba(231, 238, 245, .92);
|
||||
}
|
||||
|
||||
/* Arrow pointing up (tooltip below taskbar item) */
|
||||
.arrow.bottom {
|
||||
bottom: auto;
|
||||
top: 31px !important;
|
||||
transform: scaleY(-1);
|
||||
left: calc(50% + 2px) !important;
|
||||
}
|
||||
|
||||
.arrow.bottom:after {
|
||||
bottom: -20px;
|
||||
top: auto;
|
||||
}
|
||||
|
||||
/* Arrow pointing down (tooltip above taskbar item) */
|
||||
.arrow.top {
|
||||
top: auto;
|
||||
bottom: -16px;
|
||||
}
|
||||
|
||||
.arrow.top:after {
|
||||
top: -20px;
|
||||
bottom: auto;
|
||||
}
|
||||
|
||||
/* Arrow pointing right (tooltip to the right of taskbar item) */
|
||||
.arrow.left {
|
||||
width: 16px;
|
||||
height: 70px;
|
||||
left: -16px;
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
margin-left: 0;
|
||||
margin-top: -37px;
|
||||
transform: scaleX(-1);
|
||||
top:18px !important;
|
||||
}
|
||||
|
||||
.arrow.left:after {
|
||||
left: -20px;
|
||||
top: 20px;
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
}
|
||||
|
||||
/* Arrow pointing left (tooltip to the left of taskbar item) */
|
||||
.arrow.right {
|
||||
width: 16px;
|
||||
height: 70px;
|
||||
right: -16px !important;
|
||||
left: auto;
|
||||
margin-left: 0;
|
||||
margin-top: 35px;
|
||||
transform: scaleX(-1);
|
||||
position: absolute;
|
||||
top:18px !important;
|
||||
}
|
||||
|
||||
.arrow.right:after {
|
||||
right: -20px !important;
|
||||
left: auto !important;
|
||||
top: 20px;
|
||||
bottom: auto;
|
||||
}
|
||||
|
||||
/* Center positioning adjustments */
|
||||
.arrow.center {
|
||||
left: 50%;
|
||||
margin-left: -35px;
|
||||
}
|
||||
|
||||
.arrow.middle {
|
||||
top: 50%;
|
||||
margin-top: -35px;
|
||||
}
|
||||
|
||||
/* Horizontal center adjustments for left/right arrows */
|
||||
.arrow.left.middle,
|
||||
.arrow.right.middle {
|
||||
margin-top: -35px;
|
||||
}
|
||||
|
||||
/* Vertical center adjustments for top/bottom arrows */
|
||||
.arrow.top.center,
|
||||
.arrow.bottom.center {
|
||||
margin-left: -35px;
|
||||
}
|
||||
|
||||
/******************************************************/
|
||||
@@ -3165,8 +2661,8 @@ label {
|
||||
}
|
||||
|
||||
.device-phone .popover {
|
||||
height: calc(100vh - 65px);
|
||||
height: calc(100dvh - 65px);
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 100%;
|
||||
@@ -5617,85 +5113,6 @@ html.dark-mode .usage-table-fade-overlay {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* Taskbar container */
|
||||
.device-phone .taskbar {
|
||||
/* Force taskbar to bottom on mobile devices, overriding any position classes */
|
||||
position: fixed !important;
|
||||
bottom: 5px !important;
|
||||
left: 50% !important;
|
||||
right: auto !important;
|
||||
top: auto !important;
|
||||
width: auto !important;
|
||||
height: 50px !important;
|
||||
transform: translateX(-50%) !important;
|
||||
flex-direction: row !important;
|
||||
justify-content: left !important;
|
||||
writing-mode: initial !important;
|
||||
padding: 0 7px !important;
|
||||
border-radius: 10px !important;
|
||||
|
||||
/* Enable smooth scrolling */
|
||||
-webkit-overflow-scrolling: touch;
|
||||
/* Allow horizontal touch scrolling */
|
||||
touch-action: pan-x;
|
||||
/* Enable horizontal scroll */
|
||||
overflow-x: auto;
|
||||
/* Hide scrollbars while keeping functionality */
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
|
||||
/* Base styling */
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* Hide scrollbar while keeping functionality */
|
||||
.device-phone .taskbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Taskbar items */
|
||||
.device-phone .taskbar .taskbar-item {
|
||||
/* Allow dragging while preventing unwanted touch actions */
|
||||
touch-action: pan-x pinch-zoom;
|
||||
/* Ensure items can be dragged */
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
cursor: grab;
|
||||
|
||||
/* Base styling */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.device-phone .desktop {
|
||||
position: relative;
|
||||
|
||||
/* Enable smooth scrolling on iOS */
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
/* Allow vertical touch scrolling while preventing horizontal */
|
||||
touch-action: pan-x;
|
||||
|
||||
/* Hide scrollbars while keeping functionality */
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
|
||||
/* Scrolling behavior */
|
||||
overflow-y: visible !important;
|
||||
overflow-x: scroll;
|
||||
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
|
||||
.device-phone .desktop * {
|
||||
touch-action: pan-x;
|
||||
}
|
||||
|
||||
.device-phone .desktop::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* height of 100% should not be applied to file dialogs */
|
||||
.device-phone .window:not(.window-filedialog) .window-body.item-container {
|
||||
height: 100%;
|
||||
@@ -5724,11 +5141,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;
|
||||
|
||||
+3
-17
@@ -25,12 +25,9 @@ window.window_nav_history_current_position = {};
|
||||
window.dashboard_nav_history = [];
|
||||
window.dashboard_nav_history_current_position = 0;
|
||||
window.progress_tracker = [];
|
||||
window.upload_item_global_id = 0;
|
||||
window.app_instance_ids = new Set();
|
||||
|
||||
window.menubars = [];
|
||||
window.download_progress = [];
|
||||
window.download_item_global_id = 0;
|
||||
|
||||
// This is the minimum width of the window for the sidebar to be shown
|
||||
window.window_width_threshold_for_sidebar = 500;
|
||||
@@ -114,8 +111,6 @@ if ( window.user_preferences === null ) {
|
||||
|
||||
window.window_stack = [];
|
||||
window.toolbar_height = 0;
|
||||
window.default_taskbar_height = 50;
|
||||
window.taskbar_height = window.default_taskbar_height;
|
||||
window.upload_progress_hide_delay = 500;
|
||||
window.active_uploads = {};
|
||||
window.copy_progress_hide_delay = 1000;
|
||||
@@ -130,7 +125,6 @@ window.window_counter = 0;
|
||||
window.keypress_item_seach_term = '';
|
||||
window.keypress_item_seach_buffer_timeout = undefined;
|
||||
window.first_visit_animation = false;
|
||||
window.show_twitter_link = true;
|
||||
window.animate_window_opening = true;
|
||||
window.animate_window_closing = true;
|
||||
window.desktop_loading_fade_delay = (window.first_visit_ever && window.first_visit_animation ? 6000 : 1000);
|
||||
@@ -153,20 +147,15 @@ window.launch_apps = [];
|
||||
window.launch_apps.recent = [];
|
||||
window.launch_apps.recommended = [];
|
||||
|
||||
// Map of { child_instance_id -> { parent_instance_id, launch_msg_id } }
|
||||
window.child_launch_callbacks = {};
|
||||
|
||||
// Is puter being loaded inside an iframe?
|
||||
if ( window.location !== window.parent.location ) {
|
||||
window.is_embedded = true;
|
||||
// taskbar is not needed in embedded mode
|
||||
window.taskbar_height = 0;
|
||||
} else {
|
||||
window.is_embedded = false;
|
||||
}
|
||||
|
||||
// calculate desktop height and width
|
||||
window.desktop_height = window.innerHeight - window.toolbar_height - window.taskbar_height;
|
||||
window.desktop_height = window.innerHeight - window.toolbar_height;
|
||||
window.desktop_width = window.innerWidth;
|
||||
|
||||
// {id: {left: 0, top: 0}}
|
||||
@@ -180,7 +169,7 @@ $(window).on( 'resize', function () {
|
||||
if ( window.a_window_is_resizing ) return;
|
||||
if ( window.a_window_sidebar_is_resizing ) return;
|
||||
|
||||
const new_desktop_height = window.innerHeight - window.toolbar_height - window.taskbar_height;
|
||||
const new_desktop_height = window.innerHeight - window.toolbar_height;
|
||||
const new_desktop_width = window.innerWidth;
|
||||
|
||||
window.desktop_height = new_desktop_height;
|
||||
@@ -188,7 +177,7 @@ $(window).on( 'resize', function () {
|
||||
|
||||
// Update all maximized windows to fit the new viewport
|
||||
$('.window[data-is_maximized="1"]').each(function () {
|
||||
window.update_maximized_window_for_taskbar(this);
|
||||
window.update_maximized_window(this);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -220,9 +209,6 @@ window.feature_flags = {
|
||||
download_directory: true,
|
||||
};
|
||||
|
||||
window.is_auto_arrange_enabled = true;
|
||||
window.desktop_item_positions = {};
|
||||
window.reset_item_positions = true; // The variable decides if the item positions should be reset when the user enabled auto arrange
|
||||
|
||||
window.file_templates = [];
|
||||
|
||||
|
||||
+4
-108
@@ -615,7 +615,7 @@ window.check_fsentry_against_allowed_file_types_string = function (fsentry, allo
|
||||
window.refresh_user_data = async (auth_token) => {
|
||||
let whoami;
|
||||
try {
|
||||
whoami = await puter.os.user({ query: 'icon_size=64' });
|
||||
whoami = await puter.os.user();
|
||||
} catch (e) {
|
||||
// Ignored
|
||||
}
|
||||
@@ -677,6 +677,7 @@ window.update_auth_data = async (auth_token, user) => {
|
||||
|
||||
const to_storable_user = user => {
|
||||
const storable_user = { ...user };
|
||||
// Older sessions stored taskbar_items, with app icons, in these copies.
|
||||
delete storable_user.taskbar_items;
|
||||
return storable_user;
|
||||
};
|
||||
@@ -1726,14 +1727,13 @@ window.trigger_download = (paths) => {
|
||||
|
||||
/**
|
||||
* Updates every Trash icon in the UI to reflect whether the trash is empty:
|
||||
* the taskbar item, desktop items (and shortcuts to Trash), open explorer
|
||||
* window head icons, and the Dashboard's Files-tab sidebar.
|
||||
* desktop items (and shortcuts to Trash), open explorer window head icons,
|
||||
* and the Dashboard's Files-tab sidebar.
|
||||
*
|
||||
* @param {boolean} is_empty - Whether the trash is empty
|
||||
*/
|
||||
window.update_trash_icons = function (is_empty) {
|
||||
const icon = is_empty ? window.icons['trash.svg'] : window.icons['trash-full.svg'];
|
||||
$('[data-app="trash"]').find('.taskbar-icon > img').attr('src', icon);
|
||||
$(`.item[data-path="${html_encode(window.trash_path)}" i], .item[data-shortcut_to_path="${html_encode(window.trash_path)}" i]`).find('.item-icon > img').attr('src', icon);
|
||||
$(`.window[data-path="${html_encode(window.trash_path)}" i]`).find('.window-head-icon').attr('src', icon);
|
||||
$('.directories [data-folder="Trash"] img').attr('src', icon);
|
||||
@@ -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));
|
||||
};
|
||||
@@ -3420,57 +3416,6 @@ window.undo_delete = async (items) => {
|
||||
}
|
||||
};
|
||||
|
||||
window.store_auto_arrange_preference = (preference) => {
|
||||
// localStorage still carries it for this device either way.
|
||||
puter.kv.set('user_preferences.auto_arrange_desktop', preference)
|
||||
.catch(err => console.warn('Could not save auto_arrange_desktop:', err));
|
||||
localStorage.setItem('auto_arrange', preference);
|
||||
};
|
||||
|
||||
window.get_auto_arrange_data = async () => {
|
||||
// Best-effort, like every other preference read: falling back to the
|
||||
// default keeps the desktop arranging itself rather than not appearing.
|
||||
const preferenceValue = await puter.kv.get('user_preferences.auto_arrange_desktop').catch(() => null);
|
||||
window.is_auto_arrange_enabled = preferenceValue === null ? true : preferenceValue;
|
||||
const positions = await puter.kv.get('desktop_item_positions').catch(() => null);
|
||||
window.desktop_item_positions = (!positions || typeof positions !== 'object' || Array.isArray(positions)) ? {} : positions;
|
||||
};
|
||||
|
||||
window.clear_desktop_item_positions = async (el_desktop) => {
|
||||
$(el_desktop).find('.item').each(function () {
|
||||
const el_item = $(this)[0];
|
||||
$(el_item).css('position', '');
|
||||
$(el_item).css('left', '');
|
||||
$(el_item).css('top', '');
|
||||
});
|
||||
if ( window.reset_item_positions ) {
|
||||
window.delete_desktop_item_positions();
|
||||
}
|
||||
};
|
||||
|
||||
window.set_desktop_item_positions = async (el_desktop) => {
|
||||
$(el_desktop).find('.item').each(async function () {
|
||||
const position = window.desktop_item_positions[$(this).attr('data-uid')];
|
||||
const el_item = $(this)[0];
|
||||
if ( position ) {
|
||||
$(el_item).css('position', 'absolute');
|
||||
$(el_item).css('left', `${position.left }px`);
|
||||
$(el_item).css('top', `${position.top }px`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.save_desktop_item_positions = () => {
|
||||
puter.kv.set('desktop_item_positions', window.desktop_item_positions)
|
||||
.catch(err => console.warn('Could not save desktop_item_positions:', err));
|
||||
};
|
||||
|
||||
window.delete_desktop_item_positions = () => {
|
||||
window.desktop_item_positions = {};
|
||||
puter.kv.del('desktop_item_positions')
|
||||
.catch(err => console.warn('Could not clear desktop_item_positions:', err));
|
||||
};
|
||||
|
||||
// Finds the `.window` element for the given app instance ID
|
||||
window.window_for_app_instance = (instance_id) => {
|
||||
return $(`.window[data-element_uuid="${instance_id}"]`).get(0);
|
||||
@@ -3526,22 +3471,6 @@ window.set_menu_item_prop = (items, item_id, prop, val) => {
|
||||
}
|
||||
};
|
||||
|
||||
window.countSubstr = (str, substring) => {
|
||||
if ( substring.length === 0 ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
let pos = str.indexOf(substring);
|
||||
|
||||
while ( pos !== -1 ) {
|
||||
count++;
|
||||
pos = str.indexOf(substring, pos + 1);
|
||||
}
|
||||
|
||||
return count;
|
||||
};
|
||||
|
||||
// Updates the signed-in user's profile; `username` is kept for callers that
|
||||
// still pass it.
|
||||
window.update_profile = function (username, key_vals) {
|
||||
@@ -3556,44 +3485,11 @@ window.update_profile = function (username, key_vals) {
|
||||
});
|
||||
};
|
||||
|
||||
window.blob2str = (blob) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result);
|
||||
reader.onerror = reject;
|
||||
reader.readAsText(blob);
|
||||
});
|
||||
};
|
||||
|
||||
// Another user's picture is only available while they are on a paid plan.
|
||||
window.get_profile_picture = async function (username) {
|
||||
return (await puter.auth.getProfilePicture(username)) ?? undefined;
|
||||
};
|
||||
|
||||
window.format_with_units = (num, { mulUnits, divUnits, precision = 3 }) => {
|
||||
if ( num === 0 ) return '0';
|
||||
|
||||
mulUnits = mulUnits ?? ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];
|
||||
divUnits = divUnits ?? ['m', 'µ', 'n', 'p', 'f', 'a', 'z', 'y'];
|
||||
|
||||
const abs = Math.abs(num);
|
||||
let exp = Math.floor(Math.log10(abs) / 3);
|
||||
let symbol = '';
|
||||
|
||||
symbol = exp >= 0
|
||||
? mulUnits[exp]
|
||||
: divUnits[-exp - 1] ;
|
||||
|
||||
if ( ! symbol ) {
|
||||
symbol = `e${exp * 3}`;
|
||||
}
|
||||
|
||||
const scaled = num / Math.pow(10, exp * 3);
|
||||
const rounded = Number.parseFloat(scaled.toPrecision(precision));
|
||||
|
||||
return `${rounded}${symbol}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* General-purpose number formatting function with support for decimal places,
|
||||
* thousand separators, and various formatting options.
|
||||
|
||||
@@ -29,8 +29,8 @@ const FALLBACK_ATTR = 'data-icon-fallback';
|
||||
/**
|
||||
* Pick the URL to load an app's icon from, plus the one URL to retry with.
|
||||
*
|
||||
* @param {Object} app - App record from the backend (taskbar item, launch app,
|
||||
* installedApps row). Reads `iconCdnUrl` and `iconUrl`/`icon`.
|
||||
* @param {Object} app - App record from the backend (launch app, installedApps
|
||||
* row). Reads `iconCdnUrl` and `iconUrl`/`icon`.
|
||||
* @param {string} [defaultIcon] - Used when the app carries no icon at all.
|
||||
* @returns {{ src: string; fallback: string }} `fallback` is empty when there
|
||||
* is nothing left to try.
|
||||
|
||||
@@ -57,8 +57,8 @@ describe('authLogoHeader', () => {
|
||||
});
|
||||
|
||||
it('makes the logo a named link when it leads somewhere', () => {
|
||||
const h = authLogoHeader({ logoSrc: LOGO, logoHref: '/desktop' });
|
||||
expect(h).toContain('<a class="auth-logo-link" href="/desktop">');
|
||||
const h = authLogoHeader({ logoSrc: LOGO, logoHref: '/' });
|
||||
expect(h).toContain('<a class="auth-logo-link" href="/">');
|
||||
expect(h).toContain('alt="Puter"');
|
||||
});
|
||||
|
||||
|
||||
@@ -63,11 +63,11 @@ export const get_auth_redirect_url = () => {
|
||||
*/
|
||||
const oidc_return_path = () => {
|
||||
const pathname = window.location.pathname;
|
||||
if ( pathname === '/' || pathname === '/desktop' || pathname === '/dashboard' ) {
|
||||
if ( pathname === '/' || pathname === '/dashboard' ) {
|
||||
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;
|
||||
@@ -76,10 +76,9 @@ 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.
|
||||
* whitelists these (never a client-supplied URL): `/dashboard` 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
|
||||
* recipient usually has to sign in before they can see what was shared, and an
|
||||
|
||||
@@ -42,7 +42,6 @@ afterEach(() => {
|
||||
|
||||
describe('get_oidc_return_to', () => {
|
||||
it('returns the interface pages the backend whitelists', () => {
|
||||
expect(at('/desktop')).toBe('/desktop');
|
||||
expect(at('/dashboard')).toBe('/dashboard');
|
||||
});
|
||||
|
||||
@@ -51,17 +50,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 +77,10 @@ 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('returns null for anything the backend would reject', () => {
|
||||
@@ -94,6 +88,8 @@ 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')).toBe(null);
|
||||
expect(at('/desktop/app/editor')).toBe(null);
|
||||
expect(at('/desktop/app/editor/extra')).toBe(null);
|
||||
expect(at('/dashboard/app/editor')).toBe(null);
|
||||
});
|
||||
|
||||
@@ -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,58 @@
|
||||
/*
|
||||
* 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 on action pages', () => {
|
||||
expect(boots('/action/sign-in')).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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -465,7 +459,7 @@ const generate_file_context_menu = async function (options) {
|
||||
left = left > (window.innerWidth - window_width) ? (window.innerWidth - window_width) : left;
|
||||
|
||||
let top = $(el_item).position().top + $(el_item).height();
|
||||
top = top > (window.innerHeight - (window_height + window.taskbar_height + window.toolbar_height)) ? (window.innerHeight - (window_height + window.taskbar_height + window.toolbar_height)) : top;
|
||||
top = top > (window.innerHeight - (window_height + window.toolbar_height)) ? (window.innerHeight - (window_height + window.toolbar_height)) : top;
|
||||
|
||||
UIWindowItemProperties(
|
||||
$(el_item).attr('data-name'),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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],
|
||||
@@ -625,7 +622,7 @@ const runLaunch = async (options) => {
|
||||
window_height = parseFloat(app_info.metadata.window_size.height);
|
||||
} if ( options.maximized )
|
||||
{
|
||||
window_height = `calc(100% - ${window.taskbar_height + window.toolbar_height + 1}px)`;
|
||||
window_height = `calc(100% - ${window.toolbar_height + 1}px)`;
|
||||
}
|
||||
|
||||
// top
|
||||
@@ -678,18 +675,6 @@ const runLaunch = async (options) => {
|
||||
title = options.file_path ? path.basename(options.file_path) : title;
|
||||
}
|
||||
|
||||
// show_in_taskbar
|
||||
// Deliberately keyed on the app's own `background`, not on this launch's:
|
||||
// an app that always runs windowless has nothing to put in the taskbar,
|
||||
// ever. A background *launch* is a normal app that happens to start
|
||||
// hidden, so it keeps asking for an item — UIWindow just holds it back
|
||||
// until the window is first shown (see its taskbar block).
|
||||
let show_in_taskbar = app_info.background ? false : window_options?.show_in_taskbar;
|
||||
if ( window_options?.show_in_taskbar !== undefined )
|
||||
{
|
||||
show_in_taskbar = window_options.show_in_taskbar;
|
||||
}
|
||||
|
||||
// has_head
|
||||
let has_head = app_info.metadata?.has_head !== undefined ? app_info.metadata.has_head : window_options?.has_head;
|
||||
if ( app_info.metadata?.hide_titlebar !== undefined && typeof app_info.metadata.hide_titlebar === 'boolean' && app_info.metadata.hide_titlebar === true )
|
||||
@@ -748,7 +733,6 @@ const runLaunch = async (options) => {
|
||||
...window_options,
|
||||
is_resizable: window_resizable,
|
||||
has_head: has_head,
|
||||
show_in_taskbar: show_in_taskbar,
|
||||
update_window_url: update_window_url,
|
||||
custom_path: custom_path,
|
||||
});
|
||||
|
||||
@@ -276,9 +276,9 @@ Please try recreating the link.`);
|
||||
}
|
||||
}
|
||||
//----------------------------------------------------------------
|
||||
// Dashboard: is this file already open in an app window? In
|
||||
// dashboard mode there is no taskbar — the file's row is where a
|
||||
// user goes looking for a window they minimized — so restore that
|
||||
// Dashboard: is this file already open in an app window? The
|
||||
// file's row is where a user goes looking for a window they
|
||||
// minimized, so restore that
|
||||
// window (edits intact) instead of minting a second instance.
|
||||
//----------------------------------------------------------------
|
||||
// The user's own windows only: a file opened in an instance another app
|
||||
@@ -323,7 +323,7 @@ Please try recreating the link.`);
|
||||
app: 'explorer',
|
||||
top: options.maximized ? 0 : undefined,
|
||||
left: options.maximized ? 0 : undefined,
|
||||
height: options.maximized ? `calc(100% - ${window.taskbar_height + window.toolbar_height + 1}px)` : undefined,
|
||||
height: options.maximized ? `calc(100% - ${window.toolbar_height + 1}px)` : undefined,
|
||||
width: options.maximized ? '100%' : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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] }))
|
||||
|
||||
@@ -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;
|
||||
@@ -246,7 +246,6 @@ const refresh_item_container = function (el_item_container, options) {
|
||||
if ( item_path !== window.trash_path && item_path !== window.appdata_path ) {
|
||||
// if this is trash, get original name from item metadata
|
||||
fsentry.name = metadata.original_name ?? fsentry.name;
|
||||
const position = window.desktop_item_positions[fsentry.uid] ?? undefined;
|
||||
UIItem({
|
||||
appendTo: el_item_container,
|
||||
uid: fsentry.uid,
|
||||
@@ -269,7 +268,6 @@ const refresh_item_container = function (el_item_container, options) {
|
||||
suggested_apps: fsentry.suggested_apps,
|
||||
disabled: is_disabled,
|
||||
visible: visible,
|
||||
position: position,
|
||||
shared_with_me: fsentry.shared_with_me,
|
||||
share_mode: fsentry.share_mode,
|
||||
shared_by: fsentry.shared_by,
|
||||
@@ -281,31 +279,6 @@ const refresh_item_container = function (el_item_container, options) {
|
||||
}
|
||||
}
|
||||
|
||||
// if this is desktop, add Trash
|
||||
if ( $(el_item_container).hasClass('desktop') ) {
|
||||
try {
|
||||
const trash = await puter.fs.stat({ path: window.trash_path, consistency: options.consistency ?? 'eventual' });
|
||||
if ( ! is_current() ) {
|
||||
return;
|
||||
}
|
||||
UIItem({
|
||||
appendTo: el_item_container,
|
||||
uid: trash.id,
|
||||
immutable: trash.immutable,
|
||||
path: window.trash_path,
|
||||
icon: { image: (trash.is_empty ? window.icons['trash.svg'] : window.icons['trash-full.svg']), type: 'icon' },
|
||||
name: trash.name,
|
||||
is_dir: trash.is_dir,
|
||||
sort_by: trash.sort_by,
|
||||
type: trash.type,
|
||||
is_trash: true,
|
||||
sortable: false,
|
||||
});
|
||||
window.sort_items(el_item_container, $(el_item_container).attr('data-sort_by'), $(el_item_container).attr('data-sort_order'));
|
||||
} catch (e) {
|
||||
// Ignored
|
||||
}
|
||||
}
|
||||
// sort items
|
||||
window.sort_items(
|
||||
el_item_container,
|
||||
|
||||
@@ -47,7 +47,6 @@ Object.assign(window, {
|
||||
shared_path: '/alice/Shared',
|
||||
trash_path: '/alice/Trash',
|
||||
user_preferences: {},
|
||||
desktop_item_positions: {},
|
||||
check_fsentry_against_allowed_file_types_string: () => true,
|
||||
sort_items: () => {},
|
||||
update_explorer_footer_item_count: () => {},
|
||||
|
||||
@@ -23,10 +23,9 @@
|
||||
* for one (`puter.ui.launchApp(name, args, { background: true })`), which lets a
|
||||
* dual-purpose app serve another app without a window flashing on screen.
|
||||
*
|
||||
* Either way the window starts out of sight and out of reach: no taskbar item,
|
||||
* no running dot, and not what reopening the app finds (see
|
||||
* user_facing_windows) — it earns all three the moment it first becomes
|
||||
* visible, and an always-windowless app never asks for a taskbar item at all.
|
||||
* Either way the window starts out of sight and out of reach: no running dot,
|
||||
* and not what reopening the app finds (see user_facing_windows). It earns both
|
||||
* the moment it first becomes visible.
|
||||
*
|
||||
* @param {{ background?: boolean }} [appInfo] the app record being launched
|
||||
* @param {{ background?: boolean }} [options] this launch's options
|
||||
|
||||
@@ -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);
|
||||
@@ -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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -83,7 +83,7 @@ export const is_unseen_background_window = (el_window) => {
|
||||
* Of the windows an app has open, the ones that are the USER's. An instance
|
||||
* another app launched in the background and never showed exists to serve that
|
||||
* app alone, so everything the user drives — reopening the app, its running
|
||||
* dot, its taskbar item, the dashboard's Back/Forward — has to look straight
|
||||
* dot, the dashboard's Back/Forward — has to look straight
|
||||
* past it and act on the user's own instances instead, launching a fresh one
|
||||
* when there are none. (Paths that act on the APP rather than on a window it
|
||||
* happens to have open — uninstall, and the launcher's close taking its
|
||||
|
||||
@@ -132,7 +132,7 @@ describe('user_facing_windows', () => {
|
||||
|
||||
it('keeps a background instance once it has been shown', () => {
|
||||
// makeWindowVisible drops the marker: from then on the window is the
|
||||
// user's, and the tile/taskbar/history paths act on it again.
|
||||
// user's, and the tile and history paths act on it again.
|
||||
const shown = win({ visible: '1', parent: 'parent-uuid' });
|
||||
expect(user_facing_windows([shown])).toEqual([shown]);
|
||||
});
|
||||
|
||||
@@ -31,14 +31,12 @@ const ar = {
|
||||
allow: 'السماح',
|
||||
ascending: 'تصاعدي',
|
||||
associated_websites: 'المواقع المرتبطة',
|
||||
auto_arrange: 'ترتيب تلقائي',
|
||||
cancel: 'إلغاء',
|
||||
change_email: 'تغيير البريد الإلكتروني',
|
||||
change_language: 'تغيير اللغة',
|
||||
change_password: 'تغيير كلمة المرور',
|
||||
change_username: 'تغيير اسم المستخدم',
|
||||
close: 'إغلاق',
|
||||
close_all_windows: 'إغلاق جميع النوافذ',
|
||||
close_all_windows_and_log_out: 'إغلاق النوافذ وتسجيل الخروج',
|
||||
change_always_open_with: 'هل تريد دائمًا فتح هذا النوع من الملفات باستخدام',
|
||||
confirm: 'تأكيد',
|
||||
@@ -93,7 +91,6 @@ const ar = {
|
||||
desktop: 'سطح المكتب',
|
||||
desktop_background_fit: 'ملائمة',
|
||||
developers: 'المطورين',
|
||||
dir_published_as_website: ':%strong% تم نشره إلى',
|
||||
disable_2fa: 'تعطيل المصادقة الثنائية',
|
||||
disable_2fa_confirm: 'هل أنت متأكد أنك تريد تعطيل المصادقة الثنائية؟',
|
||||
disable_2fa_instructions: '.أدخل كلمة المرور لتعطيل المصادقة الثنائية',
|
||||
@@ -101,7 +98,6 @@ const ar = {
|
||||
dont_allow: 'عدم السماح',
|
||||
download: 'تنزيل',
|
||||
download_file: 'تنزيل الملف',
|
||||
downloading: 'جارٍ التنزيل',
|
||||
email: 'البريد الإلكتروني',
|
||||
email_change_confirmation_sent:
|
||||
'.تم إرسال بريد تأكيد إلى عنوان بريدك الإلكتروني الجديد. يرجى التحقق من صندوق الوارد واتباع التعليمات لإكمال العملية',
|
||||
@@ -132,7 +128,6 @@ const ar = {
|
||||
general: 'عام',
|
||||
get_a_copy_of_on_puter: "Puter.com! :احصل على نسخة من '%%' على",
|
||||
get_copy_link: 'احصل على رابط النسخ',
|
||||
hide_all_windows: 'إخفاء جميع النوافذ',
|
||||
home: 'الصفحة الرئيسية',
|
||||
html_document: 'HTML مستند',
|
||||
hue: 'درجة اللون',
|
||||
@@ -142,7 +137,6 @@ const ar = {
|
||||
items_in_trash_cannot_be_renamed:
|
||||
'.لا يمكن إعادة تسمية هذا العنصر لأنه في سلة المهملات. لإعادة تسمية هذا العنصر، اسحبه أولاً خارج سلة المهملات',
|
||||
jpeg_image: 'JPEG صورة',
|
||||
keep_in_taskbar: 'الاحتفاظ في شريط المهام',
|
||||
language: 'اللغة',
|
||||
license: 'رخصة',
|
||||
lightness: 'إضاءة',
|
||||
@@ -192,7 +186,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}} مدعوم بواسطة ',
|
||||
@@ -215,7 +208,6 @@ const ar = {
|
||||
refer_friends_social_media_c2a:
|
||||
'Puter.com! احصل على 1 جيجابايت من التخزين المجاني على',
|
||||
refresh: 'تحديث',
|
||||
remove_from_taskbar: 'إزالة من شريط المهام',
|
||||
rename: 'إعادة تسمية',
|
||||
replace: 'استبدال',
|
||||
replace_all: 'استبدال الكل',
|
||||
@@ -243,7 +235,6 @@ const ar = {
|
||||
share: 'مشاركة',
|
||||
share_to: 'مشاركة إلى',
|
||||
shortcut_to: 'اختصار إلى',
|
||||
show_all_windows: 'عرض جميع النوافذ',
|
||||
show_hidden: 'إظهار المخفي',
|
||||
sign_in_with_puter: 'Puter تسجيل الدخول باستخدام',
|
||||
sign_up: 'تسجيل',
|
||||
@@ -390,8 +381,6 @@ const ar = {
|
||||
create_shortcut_s: 'إنشاء اختصارات', // In English: "Create Shortcuts"
|
||||
minimize: 'تصغير', // In English: "Minimize"
|
||||
reload_app: 'إعادة تحميل التطبيق', // In English: "Reload App"
|
||||
new_window: 'نافذة جديدة', // In English: "New Window"
|
||||
open_trash: 'فتح سلة المهملات', // In English: "Open Trash"
|
||||
publish_as_serverless_worker: 'النشر كعامل', // In English: "Publish as Worker"
|
||||
'toolbar.enter_fullscreen': 'وضع ملء الشاشة', // In English: "Enter Full Screen"
|
||||
'toolbar.github': 'GitHub', // In English: "GitHub"
|
||||
@@ -424,14 +413,8 @@ const ar = {
|
||||
window_title_publish_worker: 'نشر العامل',
|
||||
window_title_authenticating: '…جارٍ التحقق',
|
||||
window_title_refer_friend: '!أوصِ صديقًا',
|
||||
desktop_show_desktop: 'عرض سطح المكتب',
|
||||
desktop_show_open_windows: 'عرض النوافذ المفتوحة',
|
||||
desktop_exit_full_screen: 'الخروج من وضع ملء الشاشة',
|
||||
desktop_enter_full_screen: 'وضع ملء الشاشة',
|
||||
desktop_position: 'الموضع',
|
||||
desktop_position_left: 'اليسار',
|
||||
desktop_position_bottom: 'الأسفل',
|
||||
desktop_position_right: 'اليمين',
|
||||
item_shared_with_you: '.قام مستخدم بمشاركة هذا العنصر معك',
|
||||
item_shared_by_you: '.لقد قمت بمشاركة هذا العنصر مع مستخدم آخر على الأقل',
|
||||
item_shortcut: 'اختصار',
|
||||
|
||||
@@ -31,7 +31,6 @@ const bg = {
|
||||
allow: 'Разреши',
|
||||
ascending: 'Възходящ',
|
||||
associated_websites: 'Свързани уебсайтове',
|
||||
auto_arrange: 'Автоматично подреждане',
|
||||
cancel: 'Отказ',
|
||||
change: 'Промени',
|
||||
change_always_open_with: 'Искате ли винаги да отваряте този тип файл с',
|
||||
@@ -40,7 +39,6 @@ const bg = {
|
||||
change_password: 'Промяна на паролата',
|
||||
change_username: 'Промяна на потребителското име',
|
||||
close: 'Затвори',
|
||||
close_all_windows: 'Затвори всички прозорци',
|
||||
close_all_windows_and_log_out: 'Затвори прозорците и излез',
|
||||
confirm: 'Потвърди',
|
||||
confirm_2fa_setup: 'Добавих кода към приложението си за удостоверяване',
|
||||
@@ -99,7 +97,6 @@ const bg = {
|
||||
desktop: 'Работен плот',
|
||||
desktop_background_fit: 'Побери',
|
||||
developers: 'Разработчици',
|
||||
dir_published_as_website: '%strong% беше публикувана на:',
|
||||
disable_2fa: 'Изключи 2FA',
|
||||
disable_2fa_confirm: 'Сигурни ли сте, че искате да изключите 2FA?',
|
||||
disable_2fa_instructions: 'Въведете паролата си, за да изключите 2FA.',
|
||||
@@ -107,7 +104,6 @@ const bg = {
|
||||
dont_allow: 'Не разрешавай',
|
||||
download: 'Изтегли',
|
||||
download_file: 'Изтегли файл',
|
||||
downloading: 'Изтегляне',
|
||||
email: 'Имейл',
|
||||
email_change_confirmation_sent:
|
||||
'Потвърдителен имейл беше изпратен на новия ви имейл адрес. Моля, проверете входящата си поща и следвайте инструкциите, за да завършите процеса.',
|
||||
@@ -139,7 +135,6 @@ const bg = {
|
||||
general: 'Общи',
|
||||
get_a_copy_of_on_puter: 'Вземете копие на %% в Puter.com!',
|
||||
get_copy_link: 'Вземи връзка за копие',
|
||||
hide_all_windows: 'Скрий всички прозорци',
|
||||
home: 'Начало',
|
||||
html_document: 'HTML документ',
|
||||
hue: 'Нюанс',
|
||||
@@ -149,7 +144,6 @@ const bg = {
|
||||
items_in_trash_cannot_be_renamed:
|
||||
'Този елемент не може да бъде преименуван, защото е в Кошчето. За да го преименувате, първо го извадете от Кошчето.',
|
||||
jpeg_image: 'JPEG изображение',
|
||||
keep_in_taskbar: 'Задръж в лентата на задачите',
|
||||
language: 'Език',
|
||||
license: 'Лиценз',
|
||||
lightness: 'Светлина',
|
||||
@@ -179,10 +173,8 @@ const bg = {
|
||||
no: 'Не',
|
||||
ok: 'ОК',
|
||||
open: 'Отвори',
|
||||
new_window: 'Нов прозорец',
|
||||
open_in_new_tab: 'Отвори в нов раздел',
|
||||
open_in_new_window: 'Отвори в нов прозорец',
|
||||
open_trash: 'Отвори Кошчето',
|
||||
open_with: 'Отвори с',
|
||||
original_name: 'Оригинално име',
|
||||
original_path: 'Оригинален път',
|
||||
@@ -203,7 +195,6 @@ const bg = {
|
||||
paste_into_folder: 'Постави в папката',
|
||||
path: 'Път',
|
||||
personalization: 'Персонализация',
|
||||
pick_name_for_website: 'Изберете име за вашия уебсайт:',
|
||||
pictures: 'Снимки',
|
||||
plural_suffix: 'и',
|
||||
powered_by_puter_js: 'Работи с {{link=docs}}Puter.js{{/link}}',
|
||||
@@ -228,7 +219,6 @@ const bg = {
|
||||
refer_friends_social_media_c2a:
|
||||
'Получете 1 GB безплатно пространство в Puter.com!',
|
||||
refresh: 'Опресни',
|
||||
remove_from_taskbar: 'Премахни от лентата на задачите',
|
||||
rename: 'Преименувай',
|
||||
replace: 'Замени',
|
||||
replace_all: 'Замени всички',
|
||||
@@ -258,7 +248,6 @@ const bg = {
|
||||
share: 'Сподели',
|
||||
share_to: 'Сподели с',
|
||||
shortcut_to: 'Пряк път до',
|
||||
show_all_windows: 'Покажи всички прозорци',
|
||||
show_hidden: 'Покажи скритите',
|
||||
sign_in_with_puter: 'Влез с Puter',
|
||||
sign_up: 'Регистрирай се',
|
||||
@@ -453,14 +442,8 @@ const bg = {
|
||||
window_title_refer_friend: 'Препоръчайте приятел!',
|
||||
|
||||
// Desktop UI
|
||||
desktop_show_desktop: 'Покажи работния плот',
|
||||
desktop_show_open_windows: 'Покажи отворените прозорци',
|
||||
desktop_exit_full_screen: 'Изход от режим на цял екран',
|
||||
desktop_enter_full_screen: 'Вход в режим на цял екран',
|
||||
desktop_position: 'Позиция',
|
||||
desktop_position_left: 'Ляво',
|
||||
desktop_position_bottom: 'Долу',
|
||||
desktop_position_right: 'Дясно',
|
||||
|
||||
// Item UI
|
||||
item_shared_with_you: 'Потребител е споделил този елемент с вас.',
|
||||
|
||||
@@ -31,14 +31,12 @@ const bn = {
|
||||
allow: 'অনুমতি দিন',
|
||||
ascending: 'ঊর্ধ্বক্রমে',
|
||||
associated_websites: 'সংযুক্ত ওয়েবসাইটগুলি',
|
||||
auto_arrange: 'অটো বিন্যাস',
|
||||
cancel: 'বাতিল করুন',
|
||||
change_email: 'ই-মেইল পরিবর্তন করুন',
|
||||
change_language: 'ভাষা পরিবর্তন করুন',
|
||||
change_password: 'পাসওয়ার্ড পরিবর্তন করুন',
|
||||
change_username: 'ইউজারনেম পরিবর্তন করুন',
|
||||
close: 'বন্ধ করুন',
|
||||
close_all_windows: 'সমস্ত উইন্ডো বন্ধ করুন',
|
||||
close_all_windows_and_log_out: 'উইন্ডো বন্ধ এবং লগ আউট করুন',
|
||||
change_always_open_with: 'আপনি কি এই ধরনের ফাইলটি সবসময় এই সাথে খোলা রাখতে চান',
|
||||
confirm: 'অনুমোদন',
|
||||
@@ -86,7 +84,6 @@ const bn = {
|
||||
desktop: 'ডেস্কটপ',
|
||||
desktop_background_fit: 'ফিট',
|
||||
developers: 'ডেভেলপারগণ',
|
||||
dir_published_as_website: '%strong% প্রকাশিত হয়েছে:',
|
||||
disable_2fa: '2FA অক্ষম করুন',
|
||||
disable_2fa_confirm: 'আপনি কি নিশ্চিত যে আপনি 2FA অক্ষম করতে চান?',
|
||||
disable_2fa_instructions: '2FA অক্ষম করতে আপনার পাসওয়ার্ড লিখুন।',
|
||||
@@ -94,7 +91,6 @@ const bn = {
|
||||
dont_allow: 'অনুমতি নেই',
|
||||
download: 'ডাউনলোড',
|
||||
download_file: 'ফাইল ডাউনলোড করুন',
|
||||
downloading: 'ডাউনলোড হচ্ছে',
|
||||
email: 'ই-মেইল',
|
||||
email_change_confirmation_sent: 'নতুন ই-মেইল ঠিকানা নিশ্চিতকরণের জন্য একটি নিশ্চিতকরণ ইমেল পাঠানো হয়েছে। আপনার ইনবক্স পরীক্ষা করুন এবং নির্দেশানুযায়ী প্রক্রিয়াটি সম্পন্ন করুন।',
|
||||
email_invalid: 'ই-মেইল অবৈধ।',
|
||||
@@ -121,7 +117,6 @@ const bn = {
|
||||
general: 'সাধারণ',
|
||||
get_a_copy_of_on_puter: 'পিউটার ডটকমে \'%%\' এর একটি অনুলিপি পান!',
|
||||
get_copy_link: 'কপি লিংক নিন',
|
||||
hide_all_windows: 'সমস্ত উইন্ডো লুকান',
|
||||
home: 'হোম',
|
||||
html_document: 'এইচটিএমএল ডকুমেন্ট',
|
||||
hue: 'হিউ',
|
||||
@@ -130,7 +125,6 @@ const bn = {
|
||||
item: 'আইটেম',
|
||||
items_in_trash_cannot_be_renamed: 'এই আইটেমটি নাম পরিবর্তন করা যাবে না কারণ এটি ট্র্যাশে রয়েছে। এই আইটেমটির নাম পরিবর্তন করতে, প্রথমে এটি ট্র্যাশ থেকে তুলে নিন।',
|
||||
jpeg_image: 'জেপিইজি ইমেজ',
|
||||
keep_in_taskbar: 'টাস্কবারে রাখুন',
|
||||
language: 'ভাষা',
|
||||
license: 'লাইসেন্স',
|
||||
lightness: 'উজ্জ্বলতা',
|
||||
@@ -179,7 +173,6 @@ const bn = {
|
||||
paste_into_folder: 'ফোল্ডারে পেস্ট করুন',
|
||||
path: 'পথ',
|
||||
personalization: 'ব্যক্তিগতীকরণ',
|
||||
pick_name_for_website: 'আপনার ওয়েবসাইটের জন্য নাম নির্বাচন করুন:',
|
||||
pictures: 'চিত্র',
|
||||
plural_suffix: 'গুলি',
|
||||
powered_by_puter_js: '{{link=docs}}Puter.js{{/link}} দ্বারা প্রচালিত',
|
||||
@@ -199,7 +192,6 @@ const bn = {
|
||||
refer_friends_c2a: 'Puter তে অ্যাকাউন্ট তৈরি এবং নিশ্চিতকরণ করে ১ জিবি পান। আপনার বন্ধুও ১ জিবি পাবে!',
|
||||
refer_friends_social_media_c2a: 'Puter.com এ 1 GB বিনামূল্যের সংরক্ষণ পান!',
|
||||
refresh: 'রিফ্রেশ',
|
||||
remove_from_taskbar: 'টাস্কবার থেকে সরান',
|
||||
rename: 'পুনঃনামকরণ',
|
||||
replace: 'বদলান',
|
||||
replace_all: 'সকল কিছু বদলান',
|
||||
@@ -227,7 +219,6 @@ const bn = {
|
||||
share: 'শেয়ার করুন',
|
||||
share_to: 'শেয়ার করুন প্রতি',
|
||||
shortcut_to: 'শর্টকাট',
|
||||
show_all_windows: 'সমস্ত উইন্ডো দেখান',
|
||||
show_hidden: 'লুকানো দেখান',
|
||||
sign_in_with_puter: 'Puter দিয়ে সাইন ইন করুন',
|
||||
sign_up: 'নিবন্ধন করুন',
|
||||
@@ -370,8 +361,6 @@ const bn = {
|
||||
'create_shortcut_s': 'শর্টকাটগুলো তৈরি করুন',
|
||||
'minimize': 'আড়াল করুন',
|
||||
'reload_app': 'অ্যাপ রিলোড করুন',
|
||||
'new_window': 'নতুন উইন্ডো',
|
||||
'open_trash': 'আবর্জনা বক্স খুলুন',
|
||||
'publish_as_serverless_worker': 'কর্মী হিসেবে পাবলিশ করুন',
|
||||
'toolbar.enter_fullscreen': 'পরিপূর্ণ পর্দায় প্রবেশ করুন',
|
||||
'toolbar.github': 'গিটহাব',
|
||||
@@ -403,14 +392,8 @@ const bn = {
|
||||
'window_title_publish_worker': 'কর্মী পাবলিশ করুন',
|
||||
'window_title_authenticating': 'প্রমানীকরণ চলমান...',
|
||||
'window_title_refer_friend': 'বন্ধুকে রেফার করুন!',
|
||||
'desktop_show_desktop': 'ডেস্কটপ দেখান',
|
||||
'desktop_show_open_windows': 'খোলা উইন্ডোগুলো দেখান',
|
||||
'desktop_exit_full_screen': 'পরিপূর্ণ পর্দা থেকে বের হয়ে যান',
|
||||
'desktop_enter_full_screen': 'পরিপূর্ণ পর্দায় প্রবেশ করুন',
|
||||
'desktop_position': 'অবস্থান',
|
||||
'desktop_position_left': 'বামে',
|
||||
'desktop_position_bottom': 'নিচে',
|
||||
'desktop_position_right': 'ডানে',
|
||||
'item_shared_with_you': 'একজন ব্যবহারকারী এটি আপনার সাথে শেয়ার করেছে।',
|
||||
'item_shared_by_you': 'আপনি এই বস্তুটি কমপক্ষে একজন ব্যবহারকারীর সাথে শেয়ার করেছেন।',
|
||||
'item_shortcut': 'শর্টকাট',
|
||||
|
||||
@@ -31,14 +31,12 @@ const br = {
|
||||
allow: 'Permitir',
|
||||
ascending: 'Ascendente',
|
||||
associated_websites: 'Sites Associados',
|
||||
auto_arrange: 'Organizar Automaticamente',
|
||||
cancel: 'Cancelar',
|
||||
change_email: 'Mudar Email',
|
||||
change_language: 'Mudar Idioma',
|
||||
change_password: 'Mudar Senha',
|
||||
change_username: 'Mudar Nome de Usuário',
|
||||
close: 'Fechar',
|
||||
close_all_windows: 'Fechar Todas as Janelas',
|
||||
close_all_windows_and_log_out: 'Fechar Janelas e Sair',
|
||||
change_always_open_with: 'Deseja sempre abrir este tipo de arquivo com',
|
||||
confirm: 'Confirmar',
|
||||
@@ -86,7 +84,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.',
|
||||
@@ -94,7 +91,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.',
|
||||
@@ -121,7 +117,6 @@ const br = {
|
||||
general: 'Geral',
|
||||
get_a_copy_of_on_puter: 'Obtenha uma cópia de \'%%\' em Puter.com!',
|
||||
get_copy_link: 'Obter Link de Cópia',
|
||||
hide_all_windows: 'Ocultar Todas as Janelas',
|
||||
home: 'Início',
|
||||
html_document: 'Documento HTML',
|
||||
hue: 'Matiz',
|
||||
@@ -130,7 +125,6 @@ const br = {
|
||||
item: 'item',
|
||||
items_in_trash_cannot_be_renamed: 'Este item não pode ser renomeado porque está na lixeira. Para renomear este item, primeiro arraste-o para fora da Lixeira.',
|
||||
jpeg_image: 'Imagem JPEG',
|
||||
keep_in_taskbar: 'Manter na Barra de Tarefas',
|
||||
language: 'Idioma',
|
||||
license: 'Licença',
|
||||
lightness: 'Luminosidade',
|
||||
@@ -176,7 +170,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}}',
|
||||
@@ -196,7 +189,6 @@ const br = {
|
||||
'refer_friends_c2a': 'Ganhe 1 GB para cada amigo que criar e confirmar uma conta no Puter. Seu amigo também ganhará 1 GB!',
|
||||
'refer_friends_social_media_c2a': 'Ganhe 1 GB de armazenamento gratuito no Puter.com!',
|
||||
'refresh': 'Atualizar',
|
||||
'remove_from_taskbar': 'Remover da Barra de Tarefas',
|
||||
'rename': 'Renomear',
|
||||
'replace': 'Substituir',
|
||||
'replace_all': 'Substituir Todos',
|
||||
@@ -225,7 +217,6 @@ const br = {
|
||||
'share_to': 'Compartilhar para',
|
||||
'share_with': 'Compartilhar com:',
|
||||
'shortcut_to': 'Atalho para',
|
||||
'show_all_windows': 'Mostrar Todas as Janelas',
|
||||
'show_hidden': 'Mostrar ocultos',
|
||||
'sign_in_with_puter': 'Entrar com Puter',
|
||||
'sign_up': 'Cadastrar-se',
|
||||
@@ -364,8 +355,6 @@ const br = {
|
||||
'create_shortcut_s': 'Criar atalhos', // In English: "Create Shortcuts"
|
||||
'minimize': 'Minimizar', // In English: "Minimize"
|
||||
'reload_app': 'Recarregar App', // In English: "Reload App"
|
||||
'new_window': 'Nova Janela', // In English: "New Window"
|
||||
'open_trash': 'Abrir Lixeira', // In English: "Open Trash"
|
||||
'publish_as_serverless_worker': 'Publicar como Worker', // In English: "Publish as Worker"
|
||||
'toolbar.enter_fullscreen': 'Entrar em Tela Cheia', // In English: "Enter Full Screen"
|
||||
'toolbar.github': 'GitHub', // In English: "GitHub"
|
||||
@@ -397,14 +386,8 @@ const br = {
|
||||
'window_title_publish_worker': 'Publicar Worker', // In English: "Publish Worker"
|
||||
'window_title_authenticating': 'Autenticando...', // In English: "Authenticating..."
|
||||
'window_title_refer_friend': 'Indicar um amigo!', // In English: "Refer a friend!"
|
||||
'desktop_show_desktop': 'Mostrar Área de Trabalho', // In English: "Show Desktop"
|
||||
'desktop_show_open_windows': 'Mostrar Janelas Abertas', // In English: "Show Open Windows"
|
||||
'desktop_exit_full_screen': 'Sair de Tela Cheia', // In English: "Exit Full Screen"
|
||||
'desktop_enter_full_screen': 'Entrar em Tela Cheia', // In English: "Enter Full Screen"
|
||||
'desktop_position': 'Posição', // In English: "Position"
|
||||
'desktop_position_left': 'Esquerda', // In English: "Left"
|
||||
'desktop_position_bottom': 'Fundo', // In English: "Bottom"
|
||||
'desktop_position_right': 'Direita', // In English: "Right"
|
||||
'item_shared_with_you': 'Um usuário compartilhou este item com você.', // In English: "A user has shared this item with you."
|
||||
'item_shared_by_you': 'Você compartilhou este item com pelo menos um usuário.', // In English: "You have shared this item with at least one other user."
|
||||
'item_shortcut': 'Atalho', // In English: "Shortcut"
|
||||
|
||||
@@ -62,7 +62,6 @@ const cs = {
|
||||
"app_group_remove_from_folder": "Odebrat ze složky",
|
||||
"associated_websites": "Přidružené weby",
|
||||
"app_feedback_sent": "Zpětná vazba byla odeslána. Děkujeme!",
|
||||
"auto_arrange": "Automaticky uspořádat",
|
||||
"browser_version": "Verze prohlížeče",
|
||||
"change_language": "Změnit jazyk",
|
||||
"captcha_required": "Dokončete prosím ověření CAPTCHA",
|
||||
@@ -72,7 +71,6 @@ const cs = {
|
||||
"revalidate_flow_notice": "Až budete pokračovat, budete požádáni o přihlášení pomocí propojeného účtu.",
|
||||
"reauth_required_message": "Vaše relace byla z důvodu zabezpečení aktualizována – přihlaste se prosím znovu.",
|
||||
"close": "Zavřít",
|
||||
"close_all_windows": "Zavřete všechna okna",
|
||||
"change_email": "Změnit e-mail",
|
||||
"revalidated": "Znovu ověřeno.",
|
||||
"color_depth": "Barevná hloubka",
|
||||
@@ -132,7 +130,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",
|
||||
@@ -141,15 +138,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…",
|
||||
@@ -187,8 +180,6 @@ const cs = {
|
||||
"keep_both": "Nechte si obojí",
|
||||
"html_document": "HTML dokument",
|
||||
"invite_link": "Odkaz na pozvánku",
|
||||
"keep_in_taskbar": "Ponechat na hlavním panelu",
|
||||
"hide_all_windows": "Skrýt všechna okna",
|
||||
"log_in": "Přihlásit se",
|
||||
"log_out": "Odhlásit se",
|
||||
"looks_good": "Vypadá to dobře!",
|
||||
@@ -211,7 +202,6 @@ const cs = {
|
||||
"ok": "OK",
|
||||
"or": "nebo",
|
||||
"name_too_long": "Název nesmí být delší než %% znaků.",
|
||||
"new_window": "Nové okno",
|
||||
"open": "Otevřít",
|
||||
"open_in_new_tab": "Otevřít na nové kartě",
|
||||
"open_in_new_window": "Otevřít v novém okně",
|
||||
@@ -229,7 +219,6 @@ const cs = {
|
||||
"original_name": "Původní název",
|
||||
"password_required": "Heslo je povinné.",
|
||||
"open_with": "Otevřít pomocí",
|
||||
"open_trash": "Otevřít koš",
|
||||
"path": "Cesta",
|
||||
"personalization": "Personalizace",
|
||||
"paste": "Vložit",
|
||||
@@ -264,7 +253,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",
|
||||
@@ -289,7 +277,6 @@ const cs = {
|
||||
"replace_all": "Nahradit vše",
|
||||
"recover_password": "Obnovit heslo",
|
||||
"publish_as_serverless_worker": "Publikovat jako pracovník",
|
||||
"remove_from_taskbar": "Odebrat z hlavního panelu",
|
||||
"save_session": "Uložit relaci",
|
||||
"save_account_to_get_copy_link": "Chcete-li pokračovat, vytvořte si účet.",
|
||||
"resend_confirmation_code": "Znovu odeslat potvrzovací kód",
|
||||
@@ -381,7 +368,6 @@ const cs = {
|
||||
"blocked_none": "Nikoho jste nezablokovali.",
|
||||
"blocked_failed": "Seznam blokovaných se nepodařilo aktualizovat.",
|
||||
"shortcut_to": "Zkratka k",
|
||||
"show_all_windows": "Zobrazit všechna okna",
|
||||
"share_with": "Sdílet s:",
|
||||
"share_inherited_via": "přes {{folder}}",
|
||||
"sign_in": "Přihlásit se",
|
||||
@@ -444,7 +430,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",
|
||||
@@ -574,18 +559,12 @@ const cs = {
|
||||
"window_title_set_new_password": "Nastavit nové heslo",
|
||||
"window_title_authenticating": "Ověřování...",
|
||||
"window_title_publish_worker": "Publikovat pracovníka",
|
||||
"desktop_show_desktop": "Zobrazit plochu",
|
||||
"window_title_select_font": "Vybrat písmo…",
|
||||
"window_title_publish_website": "Publikovat webové stránky",
|
||||
"desktop_show_open_windows": "Zobrazit otevřená okna",
|
||||
"sign_up_with_email": "Zaregistrujte se pomocí e-mailu",
|
||||
"window_title_refer_friend": "Doporučte příteli!",
|
||||
"window_title_session_list": "Seznam relací!",
|
||||
"desktop_position_right": "Vpravo",
|
||||
"desktop_position_left": "Vlevo",
|
||||
"desktop_position_bottom": "Dole",
|
||||
"desktop_enter_full_screen": "Přejít na celou obrazovku",
|
||||
"desktop_position": "Pozice",
|
||||
"item_shortcut": "Zkratka",
|
||||
"item_shared_by_you": "Tuto položku jste sdíleli s alespoň jedním dalším uživatelem.",
|
||||
"item_shared_with_you": "Tuto položku s vámi sdílí uživatel.",
|
||||
@@ -624,7 +603,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.",
|
||||
|
||||
@@ -31,14 +31,12 @@ const da = {
|
||||
allow: 'Tillad',
|
||||
ascending: 'Stigende',
|
||||
associated_websites: 'Tilknyttede websteder',
|
||||
auto_arrange: 'Auto Arrangere',
|
||||
cancel: 'Annuller',
|
||||
change_email: 'Ændre e-mail',
|
||||
change_language: 'Ændre sprog',
|
||||
change_password: 'Ændre adgangskode',
|
||||
change_username: 'Ændre brugernavn',
|
||||
close: 'Luk',
|
||||
close_all_windows: 'Luk alle vinduer',
|
||||
close_all_windows_and_log_out: 'Luk vinduer og log ud',
|
||||
change_always_open_with: 'Vil du altid åbne denne filtype med',
|
||||
confirm: 'Bekræft',
|
||||
@@ -86,7 +84,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.',
|
||||
@@ -94,7 +91,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.',
|
||||
@@ -121,7 +117,6 @@ const da = {
|
||||
general: 'Generelt',
|
||||
get_a_copy_of_on_puter: 'Få en kopi af \'%%\' på Puter.com!',
|
||||
get_copy_link: 'Få kopi-link',
|
||||
hide_all_windows: 'Skjul alle vinduer',
|
||||
home: 'Hjem',
|
||||
html_document: 'HTML-dokument',
|
||||
hue: 'Farvetone',
|
||||
@@ -130,7 +125,6 @@ const da = {
|
||||
item: 'element',
|
||||
items_in_trash_cannot_be_renamed: 'Dette element kan ikke omdøbes, fordi det er i papirkurven. For at omdøbe dette element, skal du først trække det ud af papirkurven.',
|
||||
jpeg_image: 'JPEG-billede',
|
||||
keep_in_taskbar: 'Behold i proceslinjen',
|
||||
language: 'Sprog',
|
||||
license: 'Licens',
|
||||
lightness: 'Lysstyrke',
|
||||
@@ -176,7 +170,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}}',
|
||||
@@ -196,7 +189,6 @@ const da = {
|
||||
refer_friends_c2a: 'Få 1 GB for hver ven, der opretter og bekræfter en konto på Puter. Din ven vil også få 1 GB!',
|
||||
refer_friends_social_media_c2a: 'Få 1 GB gratis lagerplads på Puter.com!',
|
||||
refresh: 'Opdater',
|
||||
remove_from_taskbar: 'Fjern fra proceslinje',
|
||||
rename: 'Omdøb',
|
||||
replace: 'Erstat',
|
||||
replace_all: 'Erstat alle',
|
||||
@@ -224,7 +216,6 @@ const da = {
|
||||
share: 'Del',
|
||||
share_to: 'Del til',
|
||||
shortcut_to: 'Genvej til',
|
||||
show_all_windows: 'Vis alle vinduer',
|
||||
show_hidden: 'Vis skjulte',
|
||||
sign_in_with_puter: 'Log ind med Puter',
|
||||
sign_up: 'Tilmeld dig',
|
||||
@@ -364,8 +355,6 @@ const da = {
|
||||
'create_shortcut_s': undefined, // In English: "Create Shortcuts"
|
||||
'minimize': undefined, // In English: "Minimize"
|
||||
'reload_app': undefined, // In English: "Reload App"
|
||||
'new_window': undefined, // In English: "New Window"
|
||||
'open_trash': undefined, // In English: "Open Trash"
|
||||
'publish_as_serverless_worker': undefined, // In English: "Publish as Worker"
|
||||
'toolbar.enter_fullscreen': undefined, // In English: "Enter Full Screen"
|
||||
'toolbar.github': undefined, // In English: "GitHub"
|
||||
@@ -448,14 +437,8 @@ const da = {
|
||||
'window_title_publish_worker': undefined, // In English: "Publish Worker"
|
||||
'window_title_authenticating': undefined, // In English: "Authenticating..."
|
||||
'window_title_refer_friend': undefined, // In English: "Refer a friend!"
|
||||
'desktop_show_desktop': undefined, // In English: "Show Desktop"
|
||||
'desktop_show_open_windows': undefined, // In English: "Show Open Windows"
|
||||
'desktop_exit_full_screen': undefined, // In English: "Exit Full Screen"
|
||||
'desktop_enter_full_screen': undefined, // In English: "Enter Full Screen"
|
||||
'desktop_position': undefined, // In English: "Position"
|
||||
'desktop_position_left': undefined, // In English: "Left"
|
||||
'desktop_position_bottom': undefined, // In English: "Bottom"
|
||||
'desktop_position_right': undefined, // In English: "Right"
|
||||
'item_shared_with_you': undefined, // In English: "A user has shared this item with you."
|
||||
'item_shared_by_you': undefined, // In English: "You have shared this item with at least one other user."
|
||||
'item_shortcut': undefined, // In English: "Shortcut"
|
||||
|
||||
@@ -31,14 +31,12 @@ const de = {
|
||||
allow: 'Erlauben',
|
||||
ascending: 'Aufsteigend',
|
||||
associated_websites: 'Zugeordnete Webseiten',
|
||||
auto_arrange: 'Automatisch anordnen',
|
||||
cancel: 'Abbrechen',
|
||||
change_email: 'E-Mail ändern',
|
||||
change_language: 'Sprache ändern',
|
||||
change_password: 'Passwort ändern',
|
||||
change_username: 'Benutzername ändern',
|
||||
close: 'Schließen',
|
||||
close_all_windows: 'Alle Fenster schließen',
|
||||
close_all_windows_and_log_out: 'Fenster schließen und abmelden',
|
||||
change_always_open_with: 'Öffnen Sie diesen Dateityp immer mit',
|
||||
confirm_2fa_setup: 'Ich habe den Code in meine Authentifizierungs-App eingegeben',
|
||||
@@ -85,7 +83,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.',
|
||||
@@ -93,7 +90,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.',
|
||||
@@ -120,7 +116,6 @@ const de = {
|
||||
general: 'Allgemein',
|
||||
get_a_copy_of_on_puter: 'Holen Sie sich eine Kopie von \'%%\' auf Puter.com!',
|
||||
get_copy_link: 'Kopierlink erhalten',
|
||||
hide_all_windows: 'Alle Fenster verstecken',
|
||||
home: 'Startseite',
|
||||
html_document: 'HTML-Dokument',
|
||||
hue: 'Farbton',
|
||||
@@ -129,7 +124,6 @@ const de = {
|
||||
item: 'Element',
|
||||
items_in_trash_cannot_be_renamed: 'Dieses Element kann nicht umbenannt werden, da es sich im Papierkorb befindet. Um dieses Element umzubenennen, ziehen Sie es zunächst aus dem Papierkorb.',
|
||||
jpeg_image: 'JPEG-Bild',
|
||||
keep_in_taskbar: 'In Taskleiste behalten',
|
||||
language: 'Sprache',
|
||||
license: 'Lizenz',
|
||||
lightness: 'Helligkeit',
|
||||
@@ -175,7 +169,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}}',
|
||||
@@ -195,7 +188,6 @@ const de = {
|
||||
refer_friends_c2a: 'Erhalten Sie 1 GB für jeden Freund, der ein Konto auf Puter erstellt und bestätigt. Ihr Freund erhält ebenfalls 1 GB!',
|
||||
refer_friends_social_media_c2a: 'Erhalten Sie 1 GB kostenlosen Speicherplatz auf Puter.com!',
|
||||
refresh: 'Aktualisieren',
|
||||
remove_from_taskbar: 'Von Taskleiste entfernen',
|
||||
rename: 'Umbenennen',
|
||||
replace: 'Ersetzen',
|
||||
replace_all: 'Alles ersetzen',
|
||||
@@ -223,7 +215,6 @@ const de = {
|
||||
share: 'Teilen',
|
||||
share_to: 'Teilen mit',
|
||||
shortcut_to: 'Verknüpfung zu',
|
||||
show_all_windows: 'Alle Fenster anzeigen',
|
||||
show_hidden: 'Zeige versteckte',
|
||||
sign_in_with_puter: 'Mit Puter anmelden',
|
||||
sign_up: 'Registrieren',
|
||||
@@ -362,8 +353,6 @@ const de = {
|
||||
'create_shortcut_s': 'Verknüpfungen erstellen', // In English: "Create Shortcuts"
|
||||
'minimize': 'Minimieren', // In English: "Minimize"
|
||||
'reload_app': 'App neu laden', // In English: "Reload App"
|
||||
'new_window': 'Neues Fenster', // In English: "New Window"
|
||||
'open_trash': 'Papierkorb öffnen', // In English: "Open Trash"
|
||||
'publish_as_serverless_worker': 'Als Worker veröffentlichen', // In English: "Publish as Worker"
|
||||
'toolbar.enter_fullscreen': 'Vollbildmodus aktivieren', // In English: "Enter Full Screen"
|
||||
'toolbar.github': 'GitHub', // In English: "GitHub"
|
||||
@@ -395,14 +384,8 @@ const de = {
|
||||
'window_title_publish_worker': 'Veröffentlichungsmitarbeiter', // In English: "Publish Worker"
|
||||
'window_title_authenticating': 'Authentifizierung...', // In English: "Authenticating..."
|
||||
'window_title_refer_friend': 'Empfehlen Sie uns weiter!', // In English: "Refer a friend!"
|
||||
'desktop_show_desktop': 'Desktop anzeigen', // In English: "Show Desktop"
|
||||
'desktop_show_open_windows': 'Offene Fenster anzeigen', // In English: "Show Open Windows"
|
||||
'desktop_exit_full_screen': 'Vollbildmodus beenden', // In English: "Exit Full Screen"
|
||||
'desktop_enter_full_screen': 'Vollbildmodus aktivieren', // In English: "Enter Full Screen"
|
||||
'desktop_position': 'Position', // In English: "Position"
|
||||
'desktop_position_left': 'Links', // In English: "Left"
|
||||
'desktop_position_bottom': 'Unten', // In English: "Bottom"
|
||||
'desktop_position_right': 'Rechts', // In English: "Right"
|
||||
'item_shared_with_you': 'Ein Benutzer hat diesen Artikel mit Ihnen geteilt.', // In English: "A user has shared this item with you."
|
||||
'item_shared_by_you': 'Sie haben diesen Artikel mit mindestens einem anderen Benutzer geteilt.', // In English: "You have shared this item with at least one other user."
|
||||
'item_shortcut': 'Verknüpfung', // In English: "Shortcut"
|
||||
|
||||
@@ -26,12 +26,10 @@ const emoji = {
|
||||
add_another_account: '➕👤',
|
||||
all_fields_required: '📝🔒✅',
|
||||
ascending: '🔼',
|
||||
auto_arrange: '🔄📂📄',
|
||||
cancel: '❌',
|
||||
change_language: '🔄🌐',
|
||||
change_password: '🔑🔄',
|
||||
change_username: '👤🔄',
|
||||
close_all_windows: '🖼️❌🖼️',
|
||||
close_all_windows_and_log_out: '❌🔄🖼️🖼️🔚',
|
||||
confirm_account_for_free_referral_storage_c2a: '📧🆓👤📂📦🆓',
|
||||
confirm_delete_multiple_items: '❓❌📂❓', // folder emoji indicates plurality
|
||||
@@ -54,10 +52,8 @@ const emoji = {
|
||||
deploy_as_app: '🚀📱',
|
||||
descending: '🔽',
|
||||
desktop_background_fit: '🖥️🖼️',
|
||||
dir_published_as_website: '📂📰🌐',
|
||||
download: '⬇️',
|
||||
download_file: '⬇️📄',
|
||||
downloading: '⬇️➡️',
|
||||
email: '📧',
|
||||
email_or_username: '📧👤',
|
||||
empty_trash: '🗑️🆓',
|
||||
@@ -71,13 +67,11 @@ const emoji = {
|
||||
general: '⚙️',
|
||||
get_a_copy_of_on_puter: '📩🔄📂',
|
||||
get_copy_link: '🔗🔄',
|
||||
hide_all_windows: '🔚🔄🖼️🖼️',
|
||||
html_document: '📄🌐',
|
||||
invite_link: '🔗📩',
|
||||
item: '📂',
|
||||
items_in_trash_cannot_be_renamed: '🗑️🆓❌',
|
||||
jpeg_image: '🖼️',
|
||||
keep_in_taskbar: '📌📁',
|
||||
loading: '🔄',
|
||||
log_in: '👤🔓',
|
||||
log_out: '👤🔚',
|
||||
@@ -105,7 +99,6 @@ const emoji = {
|
||||
passwords_do_not_match: '🔑❌🔑',
|
||||
paste: '📋➡️',
|
||||
paste_into_folder: '📋➡️📂',
|
||||
pick_name_for_website: '🌐📛❓:',
|
||||
powered_by_puter_js: '⚙️🔌🔗',
|
||||
preparing: '🔄🔜',
|
||||
preparing_for_upload: '🔄🔜',
|
||||
@@ -118,7 +111,6 @@ const emoji = {
|
||||
refer_friends_c2a: '👤📞📧👤',
|
||||
refer_friends_social_media_c2a: '📲👤🆓',
|
||||
refresh: '🔄🔄',
|
||||
remove_from_taskbar: '📌❌📁',
|
||||
rename: '🔄📛',
|
||||
replace: '🔄🔄',
|
||||
replace_all: '🔄🔄',
|
||||
@@ -136,7 +128,6 @@ const emoji = {
|
||||
session_saved: '👤💾🔄',
|
||||
set_new_password: '🔑🆕',
|
||||
share_to: '🔁➡️',
|
||||
show_all_windows: '🖼️🔓🖼️',
|
||||
show_hidden: '👁️🔄',
|
||||
sign_in_with_puter: '👤🆔',
|
||||
sign_up: '👤🆕',
|
||||
|
||||
@@ -67,7 +67,6 @@ const en = {
|
||||
app_launch_no_access_unnamed: "You don't have access to this app.",
|
||||
ascending: 'Ascending',
|
||||
associated_websites: 'Associated Websites',
|
||||
auto_arrange: 'Auto Arrange',
|
||||
back: 'Back',
|
||||
bio: 'Bio',
|
||||
browser: 'Browser',
|
||||
@@ -91,7 +90,6 @@ const en = {
|
||||
reauth_required_message: 'Your session was updated for security — please sign in again.',
|
||||
color_depth: 'Color Depth',
|
||||
close: 'Close',
|
||||
close_all_windows: 'Close All Windows',
|
||||
close_all_windows_and_log_out: 'Close Windows and Log Out',
|
||||
close_menu: 'Close menu',
|
||||
collapse_sidebar: 'Collapse sidebar',
|
||||
@@ -216,7 +214,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?',
|
||||
@@ -230,12 +227,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.',
|
||||
@@ -269,7 +262,6 @@ const en = {
|
||||
general: 'General',
|
||||
get_a_copy_of_on_puter: 'Get a copy of \'%%\' on Puter.com!',
|
||||
get_copy_link: 'Get Copy Link',
|
||||
hide_all_windows: 'Hide All Windows',
|
||||
hide_password: 'Hide password',
|
||||
home: 'Home',
|
||||
html_document: 'HTML document',
|
||||
@@ -293,7 +285,6 @@ const en = {
|
||||
items_in_trash_cannot_be_renamed: 'This item can\'t be renamed because it\'s in the trash. To rename this item, first drag it out of the Trash.',
|
||||
jpeg_image: 'JPEG image',
|
||||
keep_both: 'Keep Both',
|
||||
keep_in_taskbar: 'Keep in Taskbar',
|
||||
language: 'Language',
|
||||
language_save_failed: 'Your language could not be saved. Please try again.',
|
||||
license: 'License',
|
||||
@@ -349,7 +340,6 @@ const en = {
|
||||
ok: 'OK',
|
||||
or: 'or',
|
||||
open: 'Open',
|
||||
new_window: 'New Window',
|
||||
open_with_ai: 'Open with AI',
|
||||
open_in_new_tab: 'Open in New Tab',
|
||||
app_pages: 'App pages',
|
||||
@@ -374,7 +364,6 @@ const en = {
|
||||
uninstall_named: 'Uninstall {{name}}',
|
||||
open_in_new_window: 'Open in New Window',
|
||||
open_menu: 'Open menu',
|
||||
open_trash: 'Open Trash',
|
||||
open_with: 'Open With',
|
||||
original_name: 'Original Name',
|
||||
original_path: 'Original Path',
|
||||
@@ -425,7 +414,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',
|
||||
@@ -507,7 +495,6 @@ const en = {
|
||||
recover_password: 'Recover Password',
|
||||
refer_friends_social_media_c2a: 'Get 1 GB of free storage on Puter.com!',
|
||||
refresh: 'Refresh',
|
||||
remove_from_taskbar: 'Remove from Taskbar',
|
||||
remove_profile_picture: 'Remove photo',
|
||||
rename: 'Rename',
|
||||
replace: 'Replace',
|
||||
@@ -694,7 +681,6 @@ const en = {
|
||||
share_copy_link: 'Copy link',
|
||||
share_link_copied: 'Link copied',
|
||||
shortcut_to: 'Shortcut to',
|
||||
show_all_windows: 'Show All Windows',
|
||||
show_hidden: 'Show hidden',
|
||||
show_password: 'Show password',
|
||||
sign_in: 'Sign in',
|
||||
@@ -790,7 +776,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}}',
|
||||
@@ -984,14 +969,8 @@ const en = {
|
||||
'window_title_refer_friend': 'Refer a friend!',
|
||||
|
||||
// Desktop UI
|
||||
'desktop_show_desktop': 'Show Desktop',
|
||||
'desktop_show_open_windows': 'Show Open Windows',
|
||||
'desktop_exit_full_screen': 'Exit Full Screen',
|
||||
'desktop_enter_full_screen': 'Enter Full Screen',
|
||||
'desktop_position': 'Position',
|
||||
'desktop_position_left': 'Left',
|
||||
'desktop_position_bottom': 'Bottom',
|
||||
'desktop_position_right': 'Right',
|
||||
// Item UI
|
||||
'item_shared_with_you': 'A user has shared this item with you.',
|
||||
'item_shared_by_you': 'You have shared this item with at least one other user.',
|
||||
@@ -1067,7 +1046,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',
|
||||
|
||||
@@ -38,14 +38,12 @@ const es = {
|
||||
allow: 'Permitir',
|
||||
ascending: 'Ascendiente',
|
||||
associated_websites: 'Sitios Web Asociados',
|
||||
auto_arrange: 'Organización Automática',
|
||||
cancel: 'Cancelar',
|
||||
change_email: 'Cambiar Correo Electrónico',
|
||||
change_language: 'Cambiar Idioma',
|
||||
change_password: 'Cambiar Contraseña',
|
||||
change_username: 'Cambiar Nombre de Usuario',
|
||||
close: 'Cerrar',
|
||||
close_all_windows: 'Cerrar todas las ventanas',
|
||||
close_all_windows_and_log_out: 'Cerrar ventanas y cerrar sesión',
|
||||
change_always_open_with: '¿Quieres abrir siempre este tipo de archivos con',
|
||||
confirm: 'Confirmar',
|
||||
@@ -102,7 +100,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.',
|
||||
@@ -110,7 +107,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.',
|
||||
@@ -141,7 +137,6 @@ const es = {
|
||||
general: 'General',
|
||||
get_a_copy_of_on_puter: '¡Consigue una copia de \'%%\' en Puter.com!',
|
||||
get_copy_link: 'Copiar el enlace',
|
||||
hide_all_windows: 'Ocultar todas las ventanas',
|
||||
home: 'Inicio',
|
||||
html_document: 'Documento HTML',
|
||||
hue: 'Hue',
|
||||
@@ -150,7 +145,6 @@ const es = {
|
||||
item: 'elemento',
|
||||
items_in_trash_cannot_be_renamed: 'Este elemento no se puede renombrar porque está en la papelera. Para cambiar el nombre de este archivo, primero extráelo fuera de la misma.',
|
||||
jpeg_image: 'Imagen JPEG',
|
||||
keep_in_taskbar: 'Mantener en la barra de tareas',
|
||||
language: 'Lenguage',
|
||||
license: 'Licencia',
|
||||
lightness: 'Claridad',
|
||||
@@ -202,7 +196,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}}',
|
||||
@@ -223,7 +216,6 @@ const es = {
|
||||
'Consigue 1 GB por cada amigo que cree y confirme una cuenta en Puter ¡Tu amigo recibirá 1GB también!',
|
||||
refer_friends_social_media_c2a: '¡Consigue 1 GB de almacenamiento gratuito en Puter.com!',
|
||||
refresh: 'Refrescar',
|
||||
remove_from_taskbar: 'Eliminar de la barra de tareas',
|
||||
rename: 'Renombrar',
|
||||
replace: 'Remplazar',
|
||||
replace_all: 'Replace All',
|
||||
@@ -253,7 +245,6 @@ const es = {
|
||||
share: 'Compartir',
|
||||
share_to: 'Compartir a',
|
||||
shortcut_to: 'Acceso directo a',
|
||||
show_all_windows: 'Mostrar todas las ventanas',
|
||||
show_hidden: 'Mostrar ocultos',
|
||||
sign_in_with_puter: 'Inicia sesión con Puter',
|
||||
sign_up: 'Registrarse',
|
||||
@@ -410,8 +401,6 @@ const es = {
|
||||
'create_shortcut_s': 'Crear atajos', // In English: "Create Shortcuts"
|
||||
'minimize': 'Minimizar', // In English: "Minimize"
|
||||
'reload_app': 'Recargar la Aplicación', // In English: "Reload App"
|
||||
'new_window': 'Nueva Ventana', // In English: "New Window"
|
||||
'open_trash': 'Abrir Papelera', // In English: "Open Trash"
|
||||
'publish_as_serverless_worker': 'Publicar como un Worker', // In English: "Publish as Worker"
|
||||
'toolbar.enter_fullscreen': 'Abrir pantalla completa', // In English: "Enter Full Screen"
|
||||
'toolbar.github': 'GitHub', // In English: "GitHub"
|
||||
@@ -443,14 +432,8 @@ const es = {
|
||||
'window_title_publish_worker': 'Publicar Worker', // In English: "Publish Worker"
|
||||
'window_title_authenticating': 'Autenticando...', // In English: "Authenticating..."
|
||||
'window_title_refer_friend': '¡Invitar a un amigo!', // In English: "Refer a friend!"
|
||||
'desktop_show_desktop': 'Mostrar Escritorio', // In English: "Show Desktop"
|
||||
'desktop_show_open_windows': 'Mostrar Ventanas Abiertas', // In English: "Show Open Windows"
|
||||
'desktop_exit_full_screen': 'Salir de Pantalla Completa', // In English: "Exit Full Screen"
|
||||
'desktop_enter_full_screen': 'Abrir Pantalla Completa', // In English: "Enter Full Screen"
|
||||
'desktop_position': 'Posición', // In English: "Position"
|
||||
'desktop_position_left': 'Izquierda', // In English: "Left"
|
||||
'desktop_position_bottom': 'Parte inferior', // In English: "Bottom"
|
||||
'desktop_position_right': 'Derecha', // In English: "Right"
|
||||
'item_shared_with_you': 'Un usuario compartió este elemento contigo.', // In English: "A user has shared this item with you."
|
||||
'item_shared_by_you': 'Has compartido este elemento con otro usuario', // In English: "You have shared this item with at least one other user."
|
||||
'item_shortcut': 'Atajo', // In English: "Shortcut"
|
||||
|
||||
@@ -31,7 +31,6 @@ const fa = {
|
||||
allow: 'اجازه دسترسی',
|
||||
ascending: 'صعودی',
|
||||
associated_websites: 'وب سایت های مرتبط',
|
||||
auto_arrange: 'ترتیب خودکار',
|
||||
cancel: 'لغو',
|
||||
change: 'تغییر',
|
||||
change_always_open_with: 'آیا میخواهید همیشه این نوع فایل را با ... باز کنید؟',
|
||||
@@ -40,7 +39,6 @@ const fa = {
|
||||
change_password: 'تغییر رمز عبور',
|
||||
change_username: 'تغییر نام کاربری',
|
||||
close: 'بستن',
|
||||
close_all_windows: 'بستن همه پنجره ها',
|
||||
close_all_windows_and_log_out: 'بستن پنجرهها و خروج',
|
||||
confirm: 'تایید',
|
||||
confirm_2fa_setup: 'کد را به برنامه تأیید هویت خود اضافه کردهام',
|
||||
@@ -95,7 +93,6 @@ const fa = {
|
||||
desktop: 'دسکتاپ',
|
||||
desktop_background_fit: 'متناسب',
|
||||
developers: 'تولیدکنندگان نرم افزار',
|
||||
dir_published_as_website: '%strong% منتشر شده به:',
|
||||
disable_2fa: 'غیر فعال کردن احراز هویت دو مرحله ای',
|
||||
disable_2fa_confirm:
|
||||
'آیا مطمئن هستید که میخواهید احراز هویت دو مرحله ای را غیرفعال کنید؟',
|
||||
@@ -105,7 +102,6 @@ const fa = {
|
||||
dont_allow: 'عدم اجازه',
|
||||
download: 'دانلود',
|
||||
download_file: 'دانلود فایل',
|
||||
downloading: 'دانلود',
|
||||
email: 'ایمیل',
|
||||
email_change_confirmation_sent:
|
||||
'یک ایمیل تاییدیه به آدرس ایمیل جدید شما ارسال شده است. لطفاً صندوق ایمیلهای دریافتی خود را بررسی کرده و دستورالعملها را برای تکمیل فرایند دنبال کنید.',
|
||||
@@ -136,7 +132,6 @@ const fa = {
|
||||
general: 'عمومی',
|
||||
get_a_copy_of_on_puter: 'یک نسخه از \'%%\' را در Puter.com بگیرید!',
|
||||
get_copy_link: 'گرفتن لینک کپی',
|
||||
hide_all_windows: 'پنهان کردن همه پنجره ها',
|
||||
home: 'خانه',
|
||||
html_document: 'سند HTML',
|
||||
hue: 'رنگ',
|
||||
@@ -145,7 +140,6 @@ const fa = {
|
||||
item: 'مورد',
|
||||
items_in_trash_cannot_be_renamed: 'این مورد نمی تواند تغییر نام دهد زیرا در سطل زباله است. برای تغییر نام این مورد، ابتدا آن را از سطل زباله بیرون بکشید.',
|
||||
jpeg_image: 'تصویر JPEG',
|
||||
keep_in_taskbar: 'در نوار وظایف نگه دارید',
|
||||
language: 'زبان',
|
||||
license: 'مجوز',
|
||||
lightness: 'روشنایی',
|
||||
@@ -196,7 +190,6 @@ const fa = {
|
||||
paste_into_folder: 'چسباندن در پوشه',
|
||||
path: 'مسیر',
|
||||
personalization: 'شخصی سازی',
|
||||
pick_name_for_website: 'یک نام برای وبسایت خود انتخاب کنید:',
|
||||
pictures: 'تصاویر',
|
||||
plural_suffix: 'ها',
|
||||
powered_by_puter_js: 'پشتیبانی شده توسط {{link=docs}}Puter.js{{/link}}',
|
||||
@@ -219,7 +212,6 @@ const fa = {
|
||||
'برای هر دوستی که حساب کاربری Puter ایجاد و تأیید کند، 1 گیگابایت دریافت کنید. دوست شما هم 1 گیگابایت دریافت خواهد کرد!',
|
||||
refer_friends_social_media_c2a: '1 گیگابایت فضای ذخیره سازی رایگان را در Puter.com بگیرید!',
|
||||
refresh: 'تازه کردن',
|
||||
remove_from_taskbar: 'از نوار وظایف حذف کن',
|
||||
rename: 'تغییر نام',
|
||||
replace: 'جایگزین کردن',
|
||||
replace_all: 'جایگزینی همه',
|
||||
@@ -248,7 +240,6 @@ const fa = {
|
||||
share: 'به اشتراک گذاری',
|
||||
share_to: 'اشتراک گذاری به',
|
||||
shortcut_to: 'میانبر به',
|
||||
show_all_windows: 'نمایش همه پنجره ها',
|
||||
show_hidden: 'نمایش مخفی',
|
||||
sign_in_with_puter: 'ورود با Puter',
|
||||
sign_up: 'ثبت نام',
|
||||
@@ -384,8 +375,6 @@ const fa = {
|
||||
'create_shortcut_s': 'ایجاد میانبرها',
|
||||
'minimize': 'کوچککردن',
|
||||
'reload_app': 'بارگذاری دوباره برنامه',
|
||||
'new_window': 'پنجره جدید',
|
||||
'open_trash': 'باز کردن سطل زباله',
|
||||
'publish_as_serverless_worker': 'انتشار بهعنوان کارگر',
|
||||
'toolbar.enter_fullscreen': 'ورود به تمامصفحه',
|
||||
'toolbar.github': 'گیتهاب',
|
||||
@@ -417,14 +406,8 @@ const fa = {
|
||||
'window_title_publish_worker': 'انتشار کارگر',
|
||||
'window_title_authenticating': 'در حال احراز هویت...',
|
||||
'window_title_refer_friend': 'معرفی یک دوست!',
|
||||
'desktop_show_desktop': 'نمایش دسکتاپ',
|
||||
'desktop_show_open_windows': 'نمایش پنجرههای باز',
|
||||
'desktop_exit_full_screen': 'خروج از تمامصفحه',
|
||||
'desktop_enter_full_screen': 'ورود به تمامصفحه',
|
||||
'desktop_position': 'موقعیت',
|
||||
'desktop_position_left': 'چپ',
|
||||
'desktop_position_bottom': 'پایین',
|
||||
'desktop_position_right': 'راست',
|
||||
'item_shared_with_you': 'یک کاربر این مورد را با شما به اشتراک گذاشته است.',
|
||||
'item_shared_by_you': 'شما این مورد را با حداقل یک کاربر دیگر به اشتراک گذاشتهاید.',
|
||||
'item_shortcut': 'میانبر',
|
||||
|
||||
@@ -37,14 +37,12 @@ const fi = {
|
||||
|
||||
ascending: 'Nouseva',
|
||||
associated_websites: 'Tähän liittyvät verkkosivustot',
|
||||
auto_arrange: 'Järjestä automaattisesti',
|
||||
cancel: 'Peruuta',
|
||||
change_email: 'Vaihda sähköpostiosoite',
|
||||
change_language: 'Vaihda kieli',
|
||||
change_password: 'Vaihda salasana',
|
||||
change_username: 'Vaihda käyttäjänimi',
|
||||
close: 'Sulje',
|
||||
close_all_windows: 'Sulje kaikki ikkunat',
|
||||
close_all_windows_and_log_out: 'Sulje ikkunat ja kirjaudu ulos',
|
||||
change_always_open_with:
|
||||
'Haluatko aina avata tämän tyyppisen tiedoston sovelluksella',
|
||||
@@ -113,7 +111,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ä?',
|
||||
@@ -123,7 +120,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.',
|
||||
@@ -167,7 +163,6 @@ const fi = {
|
||||
// 'get a copy of a link' => 'Ota Kopio Linkkiin' or
|
||||
// 'get a link to the copy' => 'Ota Linkki Kopioon' - More probable, just want to be sure
|
||||
|
||||
hide_all_windows: 'Piilota kaikki ikkunat',
|
||||
home: 'Koti',
|
||||
html_document: 'HTML-dokumentti',
|
||||
hue: 'Sävy',
|
||||
@@ -176,7 +171,6 @@ const fi = {
|
||||
item: 'kohde',
|
||||
items_in_trash_cannot_be_renamed: 'Tätä kohdetta ei voi nimetä uudelleen, koska se on roskakorissa. Jos haluat nimetä kohteen uudelleen, palauta se ensin roskakorista.',
|
||||
jpeg_image: 'JPEG-kuva',
|
||||
keep_in_taskbar: 'Pidä tehtäväpalkissa',
|
||||
language: 'Kieli',
|
||||
license: 'Lisenssi',
|
||||
lightness: 'Valoisuus',
|
||||
@@ -238,7 +232,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}}',
|
||||
@@ -263,7 +256,6 @@ const fi = {
|
||||
// "get rid of" => "Oletko varma, että haluat luovuttaa tämän osoitteen?" or
|
||||
// "publish" => "Oletko varma, että haluat julkaista tämän osoitteen?"
|
||||
|
||||
remove_from_taskbar: 'Poista tehtäväpalkista',
|
||||
rename: 'Nimeä uudelleen',
|
||||
replace: 'Replace',
|
||||
replace_all: 'Korvaa kaikki',
|
||||
@@ -296,7 +288,6 @@ const fi = {
|
||||
// If "share to" is followed by the name of a user, it will not make any sense, as the name can be suffixed by for example "Jaa %%lle".
|
||||
|
||||
shortcut_to: 'Pikakuvake',
|
||||
show_all_windows: 'Näytä kaikki ikkunat',
|
||||
show_hidden: 'Näytä piilotetut',
|
||||
sign_in_with_puter: 'Kirjaudu sisään Puterilla',
|
||||
sign_up: 'Rekisteröidy',
|
||||
@@ -450,8 +441,6 @@ const fi = {
|
||||
'create_shortcut_s': 'Luo pikakuvakkeita',
|
||||
'minimize': 'Pienennä',
|
||||
'reload_app': 'Lataa sovellus uudelleen',
|
||||
'new_window': 'Uusi ikkuna',
|
||||
'open_trash': 'Avaa roskakori',
|
||||
'publish_as_serverless_worker': 'Julkaise työntekijänä',
|
||||
'toolbar.enter_fullscreen': 'Koko näyttöön',
|
||||
'toolbar.github': 'GitHub',
|
||||
@@ -483,14 +472,8 @@ const fi = {
|
||||
'window_title_publish_worker': 'Julkaise työntekijä',
|
||||
'window_title_authenticating': 'Todennetaan...',
|
||||
'window_title_refer_friend': 'Suosittele ystävälle!',
|
||||
'desktop_show_desktop': 'Näytä työpöytä',
|
||||
'desktop_show_open_windows': 'Näytä avoimet ikkunat',
|
||||
'desktop_exit_full_screen': 'Poistu koko näytöstä',
|
||||
'desktop_enter_full_screen': 'Koko näyttöön',
|
||||
'desktop_position': 'Sijainti',
|
||||
'desktop_position_left': 'Vasen',
|
||||
'desktop_position_bottom': 'Ala',
|
||||
'desktop_position_right': 'Oikea',
|
||||
'item_shared_with_you': 'Käyttäjä on jakanut tämän kohteen kanssasi.',
|
||||
'item_shared_by_you': 'Olet jakanut tämän kohteen ainakin yhden muun käyttäjän kanssa.',
|
||||
'item_shortcut': 'Pikakuvake',
|
||||
|
||||
@@ -31,14 +31,12 @@ const fil = {
|
||||
allow: 'Payagan',
|
||||
ascending: 'Pataas',
|
||||
associated_websites: 'Mga kaugnay na website',
|
||||
auto_arrange: 'Ayusin nang awtomatiko',
|
||||
cancel: 'Kanselahin',
|
||||
change_email: 'Baguhin ang email',
|
||||
change_language: 'Baguhin ang wika',
|
||||
change_password: 'Baguhin ang password',
|
||||
change_username: 'Baguhin ang username',
|
||||
close: 'Isara',
|
||||
close_all_windows: 'Isara ang lahat ng window',
|
||||
close_all_windows_and_log_out: 'Isara ang mga window at mag-log out',
|
||||
change_always_open_with: 'Gusto mo bang laging buksan ang ganitong file gamit ang',
|
||||
confirm: 'Kumpirmahin',
|
||||
@@ -91,7 +89,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.',
|
||||
@@ -99,7 +96,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.',
|
||||
@@ -130,7 +126,6 @@ const fil = {
|
||||
general: 'Pangkalahatan',
|
||||
get_a_copy_of_on_puter: 'Kumuha ng kopya ng \'%%\' sa Puter.com!',
|
||||
get_copy_link: 'Kunin ang copy link',
|
||||
hide_all_windows: 'Itago ang lahat ng window',
|
||||
home: 'Home',
|
||||
html_document: 'HTML na dokumento',
|
||||
hue: 'Hue',
|
||||
@@ -139,7 +134,6 @@ const fil = {
|
||||
item: 'item',
|
||||
items_in_trash_cannot_be_renamed: 'Hindi mapalitan ang pangalan ng item na ito dahil nasa Trash. Ilabas muna ito sa Trash.',
|
||||
jpeg_image: 'JPEG na larawan',
|
||||
keep_in_taskbar: 'Panatilihin sa taskbar',
|
||||
language: 'Wika',
|
||||
license: 'Lisensya',
|
||||
lightness: 'Liwanag',
|
||||
@@ -189,7 +183,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}}',
|
||||
@@ -207,7 +200,6 @@ const fil = {
|
||||
recover_password: 'I-recover ang password',
|
||||
refer_friends_social_media_c2a: 'Makakuha ng 1 GB na libreng storage sa Puter.com!',
|
||||
refresh: 'I-refresh',
|
||||
remove_from_taskbar: 'Alisin sa taskbar',
|
||||
rename: 'Palitan ang pangalan',
|
||||
replace: 'Palitan',
|
||||
replace_all: 'Palitan lahat',
|
||||
@@ -236,7 +228,6 @@ const fil = {
|
||||
share: 'Ibahagi',
|
||||
share_to: 'Ibahagi kay',
|
||||
shortcut_to: 'Shortcut papunta sa',
|
||||
show_all_windows: 'Ipakita ang lahat ng window',
|
||||
show_hidden: 'Ipakita ang nakatago',
|
||||
sign_in_with_puter: 'Mag-sign in gamit ang Puter',
|
||||
sign_up: 'Mag-sign up',
|
||||
@@ -377,8 +368,6 @@ const fil = {
|
||||
'create_shortcut_s': 'Gumawa ng mga shortcut',
|
||||
'minimize': 'I-minimize',
|
||||
'reload_app': 'I-reload ang app',
|
||||
'new_window': 'Bagong window',
|
||||
'open_trash': 'Buksan ang Trash',
|
||||
'publish_as_serverless_worker': 'I-publish bilang worker',
|
||||
'toolbar.enter_fullscreen': 'Pumunta sa full screen',
|
||||
'toolbar.github': 'GitHub',
|
||||
@@ -410,14 +399,8 @@ const fil = {
|
||||
'window_title_publish_worker': 'I-publish ang worker',
|
||||
'window_title_authenticating': 'Nag-a-authenticate...',
|
||||
'window_title_refer_friend': 'Mag-refer ng kaibigan!',
|
||||
'desktop_show_desktop': 'Ipakita ang desktop',
|
||||
'desktop_show_open_windows': 'Ipakita ang mga bukas na window',
|
||||
'desktop_exit_full_screen': 'Lumabas sa full screen',
|
||||
'desktop_enter_full_screen': 'Pumunta sa full screen',
|
||||
'desktop_position': 'Posisyon',
|
||||
'desktop_position_left': 'Kaliwa',
|
||||
'desktop_position_bottom': 'Ibaba',
|
||||
'desktop_position_right': 'Kanan',
|
||||
'item_shared_with_you': 'May user na nagbahagi ng item na ito sa iyo.',
|
||||
'item_shared_by_you': 'Ibinahagi mo ang item na ito sa kahit isang ibang user.',
|
||||
'item_shortcut': 'Shortcut',
|
||||
|
||||
@@ -31,7 +31,6 @@ const fr = {
|
||||
allow: 'Autoriser',
|
||||
ascending: 'Ascendant',
|
||||
associated_websites: 'Sites associés',
|
||||
auto_arrange: 'Organisation automatique',
|
||||
browser: 'Navigateur',
|
||||
browser_version: 'Version du navigateur',
|
||||
captcha_required: 'Veuillez compléter la vérification CAPTCHA',
|
||||
@@ -44,7 +43,6 @@ const fr = {
|
||||
change_username: "Changer le nom d'utilisateur",
|
||||
color_depth: 'Profondeur de couleur',
|
||||
close: 'Fermer',
|
||||
close_all_windows: 'Fermer toutes les fenêtres',
|
||||
close_all_windows_and_log_out: 'Fermer les fenêtres et se déconnecter',
|
||||
confirm: 'Confirmer',
|
||||
confirm_2fa_setup: "J'ai ajouté le code à mon application d'authentification",
|
||||
@@ -98,7 +96,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.",
|
||||
@@ -106,11 +103,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.',
|
||||
@@ -137,7 +130,6 @@ const fr = {
|
||||
general: 'Général',
|
||||
get_a_copy_of_on_puter: 'Obtenez une copie de \'%%\' sur Puter.com !',
|
||||
get_copy_link: 'Obtenir le lien de copie',
|
||||
hide_all_windows: 'Masquer toutes les fenêtres',
|
||||
home: 'Accueil',
|
||||
html_document: 'Document HTML',
|
||||
hue: 'Teinte',
|
||||
@@ -146,7 +138,6 @@ const fr = {
|
||||
item: 'élément',
|
||||
items_in_trash_cannot_be_renamed: 'Cet élément ne peut pas être renommé car il se trouve dans la corbeille. Pour renommer cet élément, faites-le d\'abord glisser hors de la corbeille.',
|
||||
jpeg_image: 'Image JPEG',
|
||||
keep_in_taskbar: 'Garder dans la barre des tâches',
|
||||
language: 'Langue',
|
||||
license: 'Licence',
|
||||
lightness: 'Luminosité',
|
||||
@@ -176,11 +167,9 @@ const fr = {
|
||||
no: 'Non',
|
||||
ok: 'OK',
|
||||
open: 'Ouvrir',
|
||||
new_window: 'Nouvelle fenêtre',
|
||||
open_with_ai: 'Ouvrir avec l\'IA',
|
||||
open_in_new_tab: 'Ouvrir dans un nouvel onglet',
|
||||
open_in_new_window: 'Ouvrir dans une nouvelle fenêtre',
|
||||
open_trash: 'Ouvrir la corbeille',
|
||||
open_with: 'Ouvrir avec',
|
||||
original_name: "Nom d'origine",
|
||||
original_path: "Chemin d'origine",
|
||||
@@ -199,7 +188,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',
|
||||
@@ -223,7 +211,6 @@ const fr = {
|
||||
refer_friends_c2a: 'Obtenez 1 Go pour chaque ami qui crée et confirme un compte sur Puter. Votre ami recevra également 1 Go !',
|
||||
refer_friends_social_media_c2a: 'Obtenez 1 Go de stockage gratuit sur Puter.com !',
|
||||
refresh: 'Actualiser',
|
||||
remove_from_taskbar: 'Retirer de la barre des tâches',
|
||||
rename: 'Renommer',
|
||||
replace: 'Remplacer',
|
||||
replace_all: 'Tout remplacer',
|
||||
@@ -254,7 +241,6 @@ const fr = {
|
||||
share: 'Partager',
|
||||
share_to: 'Partager à',
|
||||
shortcut_to: 'Raccourci vers',
|
||||
show_all_windows: 'Afficher toutes les fenêtres',
|
||||
show_hidden: 'Afficher les fichiers cachés',
|
||||
sign_in_with_puter: 'Se connecter avec Puter',
|
||||
sign_up: "S'inscrire",
|
||||
@@ -292,7 +278,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}}',
|
||||
@@ -436,14 +421,8 @@ const fr = {
|
||||
'window_title_refer_friend': 'Parrainer un ami !',
|
||||
|
||||
// Desktop UI
|
||||
'desktop_show_desktop': 'Afficher le bureau',
|
||||
'desktop_show_open_windows': 'Afficher les fenêtres ouvertes',
|
||||
'desktop_exit_full_screen': 'Quitter le mode plein écran',
|
||||
'desktop_enter_full_screen': 'Passer en plein écran',
|
||||
'desktop_position': 'Position',
|
||||
'desktop_position_left': 'Gauche',
|
||||
'desktop_position_bottom': 'Bas',
|
||||
'desktop_position_right': 'Droite',
|
||||
|
||||
// Item UI
|
||||
'item_shared_with_you': 'Un utilisateur a partagé cet élément avec vous.',
|
||||
@@ -474,7 +453,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',
|
||||
|
||||
@@ -31,14 +31,12 @@ const he = {
|
||||
allow: 'להרשות',
|
||||
ascending: 'בסדר עולה',
|
||||
associated_websites: 'אתרים קשורים',
|
||||
auto_arrange: 'סידור אוטומטי',
|
||||
cancel: 'ביטול',
|
||||
change_email: 'שינוי כתובת אימייל',
|
||||
change_language: 'החלפת שפה',
|
||||
change_password: 'שינוי סיסמה',
|
||||
change_username: 'שינוי שם משתמש',
|
||||
close: 'סגירה',
|
||||
close_all_windows: 'סגירת כל החלונות',
|
||||
close_all_windows_and_log_out: 'סגור את החלונות והתנתק',
|
||||
change_always_open_with: 'האם אתה רוצה תמיד לפתוח סוג זה של קובץ עם',
|
||||
confirm: 'לאשר',
|
||||
@@ -92,7 +90,6 @@ const he = {
|
||||
desktop: 'שולחן העבודה ',
|
||||
desktop_background_fit: 'התאים',
|
||||
developers: 'מפתחים',
|
||||
dir_published_as_website: '%strong% פורסם ל:',
|
||||
disable_2fa: 'השבתת אימות דו-שלבי',
|
||||
disable_2fa_confirm: 'האם אתם בטוחים שברצונכם להשבית אימות דו-שלבי?',
|
||||
disable_2fa_instructions: 'הזינו את הסיסמה שלכם כדי להשבית אימות דו-שלבי.',
|
||||
@@ -100,7 +97,6 @@ const he = {
|
||||
dont_allow: 'אל תאפשר',
|
||||
download: 'הורדה',
|
||||
download_file: 'הורדת קובץ',
|
||||
downloading: 'מוריד',
|
||||
email: 'אימייל',
|
||||
email_change_confirmation_sent:
|
||||
'אימייל אישור נשלח לכתובת האימייל החדשה שלך. אנא בדוק את תיבת הדואר הנכנס ופעל לפי ההוראות להשלמת התהליך.',
|
||||
@@ -131,7 +127,6 @@ const he = {
|
||||
general: 'כללי',
|
||||
get_a_copy_of_on_puter: 'קבל עותק של \'%%\' ב Puter.com!',
|
||||
get_copy_link: 'קבל העתק קישור',
|
||||
hide_all_windows: 'הסתר את כל החלונות',
|
||||
home: 'בית',
|
||||
html_document: 'מסמך HTML',
|
||||
hue: 'דרגת צבע',
|
||||
@@ -140,7 +135,6 @@ const he = {
|
||||
item: 'פריט',
|
||||
items_in_trash_cannot_be_renamed: 'לא ניתן לשנות את שם הפריט הזה כי הוא נמצא באשפה. כדי לשנות את שם הפריט הזה, תחילה גרור אותו מחוץ לאשפה.',
|
||||
jpeg_image: 'JPEG תמונת',
|
||||
keep_in_taskbar: 'שמירה בשורת המשימות',
|
||||
language: 'שפה',
|
||||
license: 'רישיון',
|
||||
lightness: 'בהירות',
|
||||
@@ -189,7 +183,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}}',
|
||||
@@ -210,7 +203,6 @@ const he = {
|
||||
'קבל 1 גיגה בייט עבור כל חבר שיוצר ומאשר חשבון ב Puter. גם החבר שלך יקבל 1 גיגה בייט!',
|
||||
refer_friends_social_media_c2a: 'קבל שטח אחסון של 1 גיגה בייט בחינם Puter.com!',
|
||||
refresh: 'רענן',
|
||||
remove_from_taskbar: 'הסרה משורת המשימות',
|
||||
rename: 'שנה שם',
|
||||
replace: 'החלף',
|
||||
replace_all: 'החלף הכל',
|
||||
@@ -239,7 +231,6 @@ const he = {
|
||||
share: 'שיתוף',
|
||||
share_to: 'שתף אל',
|
||||
shortcut_to: 'קיצור דרך אל',
|
||||
show_all_windows: 'הצג את כל החלונות',
|
||||
show_hidden: 'הצג מוסתר',
|
||||
sign_in_with_puter: 'להתחבר עם Puter',
|
||||
sign_up: 'הרשמה',
|
||||
@@ -378,8 +369,6 @@ const he = {
|
||||
'create_shortcut_s': 'צור קיצורי דרך', // In English: "Create Shortcuts"
|
||||
'minimize': 'מזער', // In English: "Minimize"
|
||||
'reload_app': 'טען מחדש', // In English: "Reload App"
|
||||
'new_window': 'חלון חדש', // In English: "New Window"
|
||||
'open_trash': 'פתח אשפה', // In English: "Open Trash"
|
||||
'publish_as_serverless_worker': 'פרסם כעובד', // In English: "Publish as Worker"
|
||||
'toolbar.enter_fullscreen': 'מסך מלא', // In English: "Enter Full Screen"
|
||||
'toolbar.github': 'גיטהאב', // In English: "GitHub"
|
||||
@@ -411,14 +400,8 @@ const he = {
|
||||
'window_title_publish_worker': 'פרסם עובד', // In English: "Publish Worker"
|
||||
'window_title_authenticating': 'מאמת', // In English: "Authenticating..."
|
||||
'window_title_refer_friend': '!הפנה חבר', // In English: "Refer a friend!"
|
||||
'desktop_show_desktop': 'הראה שולחן עבודה', // In English: "Show Desktop"
|
||||
'desktop_show_open_windows': 'הראה חלונות פתוחים', // In English: "Show Open Windows"
|
||||
'desktop_exit_full_screen': 'בטל מסך מלא', // In English: "Exit Full Screen"
|
||||
'desktop_enter_full_screen': 'הפעל מסך מלא', // In English: "Enter Full Screen"
|
||||
'desktop_position': 'מיקום', // In English: "Position"
|
||||
'desktop_position_left': 'שמאל', // In English: "Left"
|
||||
'desktop_position_bottom': 'מטה', // In English: "Bottom"
|
||||
'desktop_position_right': 'ימין', // In English: "Right"
|
||||
'item_shared_with_you': 'משתמש שיתף עמך פריט זה', // In English: "A user has shared this item with you."
|
||||
'item_shared_by_you': 'שיתפת פריט זה עם לפחות משתמש אחד נוסף', // In English: "You have shared this item with at least one other user."
|
||||
'item_shortcut': 'קיצור דרך', // In English: "Shortcut"
|
||||
|
||||
@@ -31,14 +31,12 @@ const hi = {
|
||||
allow: 'अनुमति दें',
|
||||
ascending: 'आरोही',
|
||||
associated_websites: 'संबंधित वेबसाइटें',
|
||||
auto_arrange: 'स्वचालित व्यवस्तित',
|
||||
cancel: 'रद्द',
|
||||
change_email: 'ई - मेल बदले',
|
||||
change_language: 'भाषा बदलें',
|
||||
change_password: 'पासवर्ड बदलें',
|
||||
change_username: 'उपयोगकर्ता नाम बदलें',
|
||||
close: 'बंद ',
|
||||
close_all_windows: 'सभी विंडोज़ बंद करें',
|
||||
close_all_windows_and_log_out: 'विंडोज़ बंद करें और लॉग आउट करें',
|
||||
change_always_open_with: 'क्या आप इस प्रकार की फ़ाइल को हमेशा खोलना चाहते हैं?',
|
||||
confirm_2fa_setup: 'मैंने अपने प्रमाणक ऐप में कोड जोड़ दिया है',
|
||||
@@ -85,7 +83,6 @@ const hi = {
|
||||
desktop: 'डेस्कटॉप',
|
||||
desktop_background_fit: 'उपयुक्त',
|
||||
developers: 'डेवलपर्स',
|
||||
dir_published_as_website: 'निर्देशिका को एक वेबसाइट के रूप में प्रकाशित किया गया है',
|
||||
disable_2fa: '2एफए गायब करें',
|
||||
disable_2fa_confirm: 'क्या आप वाकई 2एफए को गायब करना चाहते हैं?',
|
||||
disable_2fa_instructions: '2एफए को गायब करने के लिए अपना पासवर्ड दर्ज करें।',
|
||||
@@ -93,7 +90,6 @@ const hi = {
|
||||
dont_allow: 'अनुमति न दें',
|
||||
download: 'डाउनलोड',
|
||||
download_file: 'डाउनलोड फ़ाइल',
|
||||
downloading: 'डाउनलोड हो रहा है',
|
||||
email: 'ईमेल',
|
||||
email_change_confirmation_sent: 'आपके नए ईमेल पते पर एक पुष्टिकरण ईमेल भेज दिया गया है। कृपया अपना इनबॉक्स जांचें और प्रक्रिया पूरी करने के लिए निर्देशों का पालन करें।',
|
||||
email_invalid: 'ईमेल अमान्य है।',
|
||||
@@ -120,7 +116,6 @@ const hi = {
|
||||
general: 'सामान्य',
|
||||
get_a_copy_of_on_puter: "Purer.com पर '%%' की एक कॉपी प्राप्त करें!",
|
||||
get_copy_link: 'कॉपी लिंक प्राप्त करें',
|
||||
hide_all_windows: 'सभी विंडोज़ छिपाएँ',
|
||||
home: 'घर',
|
||||
html_document: 'एचटीएमएल दस्तावेज़',
|
||||
hue: 'रंग',
|
||||
@@ -129,7 +124,6 @@ const hi = {
|
||||
item: 'वस्तु',
|
||||
items_in_trash_cannot_be_renamed: 'इस वस्तु का नाम नहीं बदला जा सकता क्योंकि यह कूड़ेदान में है। इस वस्तु का नाम बदलने के लिए, पहले इसे ट्रैश से बाहर खींचें।',
|
||||
jpeg_image: 'जेपीईजी छवि',
|
||||
keep_in_taskbar: 'टास्कबार में रखें',
|
||||
language: 'भाषा',
|
||||
license: 'लाइसेंस',
|
||||
lightness: 'चमक',
|
||||
@@ -175,7 +169,6 @@ const hi = {
|
||||
paste_into_folder: 'फ़ोल्डर में पेस्ट करें',
|
||||
path: 'पथ',
|
||||
personalization: 'वैयक्तिकरण',
|
||||
pick_name_for_website: 'अपनी वेबसाइट के लिए एक नाम चुनें:',
|
||||
pictures: 'चित्रों',
|
||||
plural_suffix: 'एस',
|
||||
powered_by_puter_js: '{{link=docs}}Puter.js{{/link}} द्वारा संचालित',
|
||||
@@ -195,7 +188,6 @@ const hi = {
|
||||
refer_friends_c2a: 'पुटर पर खाता बनाने और पुष्टि करने वाले प्रत्येक मित्र के लिए 1 जीबी प्राप्त करें। आपके दोस्त को भी मिलेगा 1 जीबी!',
|
||||
refer_friends_social_media_c2a: 'Purer.com पर 1 जीबी निःशुल्क स्टोरेज प्राप्त करें!',
|
||||
refresh: 'ताजा करना ',
|
||||
remove_from_taskbar: 'टास्कबार से हटाएँ',
|
||||
rename: 'नाम बदलें',
|
||||
replace: 'प्रतिस्थापित करें',
|
||||
replace_all: 'सबको बदली करें',
|
||||
@@ -223,7 +215,6 @@ const hi = {
|
||||
share: 'आदान-प्रदान',
|
||||
share_to: 'साझा',
|
||||
shortcut_to: 'के लिए शॉर्टकट',
|
||||
show_all_windows: 'सभी विंडोज़ दिखाएँ',
|
||||
show_hidden: 'छिपा हुआ दिखाएं',
|
||||
sign_in_with_puter: 'पुटर के साथ साइन इन करें',
|
||||
sign_up: 'साइन अप करें',
|
||||
@@ -355,8 +346,6 @@ const hi = {
|
||||
'create_shortcut_s': 'शॉर्टकट्स बनाएँ', // In English: "Create Shortcuts"
|
||||
'minimize': 'मिनिमाइज़ करें', // In English: "Minimize"
|
||||
'reload_app': 'ऐप पुनः लोड करें', // In English: "Reload App"
|
||||
'new_window': 'नई विंडो', // In English: "New Window"
|
||||
'open_trash': 'ट्रैश खोलें', // In English: "Open Trash"
|
||||
'publish_as_serverless_worker': 'वर्कर के रूप में प्रकाशित करें', // In English: "Publish as Worker"
|
||||
'toolbar.enter_fullscreen': 'पूर्ण स्क्रीन में जाएँ', // In English: "Enter Full Screen"
|
||||
'toolbar.github': 'GitHub', // In English: "GitHub"
|
||||
@@ -388,14 +377,8 @@ const hi = {
|
||||
'window_title_publish_worker': 'वर्कर प्रकाशित करें', // In English: "Publish Worker"
|
||||
'window_title_authenticating': 'प्रमाणित किया जा रहा है...', // In English: "Authenticating..."
|
||||
'window_title_refer_friend': 'मित्र को रेफ़र करें!', // In English: "Refer a friend!"
|
||||
'desktop_show_desktop': 'डेस्कटॉप दिखाएँ', // In English: "Show Desktop"
|
||||
'desktop_show_open_windows': 'खुली विंडो दिखाएँ', // In English: "Show Open Windows"
|
||||
'desktop_exit_full_screen': 'पूर्ण स्क्रीन से बाहर निकलें', // In English: "Exit Full Screen"
|
||||
'desktop_enter_full_screen': 'पूर्ण स्क्रीन में जाएँ', // In English: "Enter Full Screen"
|
||||
'desktop_position': 'स्थिति', // In English: "Position"
|
||||
'desktop_position_left': 'बाएँ', // In English: "Left"
|
||||
'desktop_position_bottom': 'नीचे', // In English: "Bottom"
|
||||
'desktop_position_right': 'दाएँ', // In English: "Right"
|
||||
'item_shared_with_you': 'एक उपयोगकर्ता ने यह वस्तु आपके साथ साझा की है।', // In English: "A user has shared this item with you."
|
||||
'item_shared_by_you': 'आपने यह वस्तु कम से कम एक अन्य उपयोगकर्ता के साथ साझा की है।', // In English: "You have shared this item with at least one other user."
|
||||
'item_shortcut': 'शॉर्टकट', // In English: "Shortcut"
|
||||
|
||||
@@ -31,14 +31,12 @@ const hu = {
|
||||
allow: 'Engedélyez',
|
||||
ascending: 'Növekvő',
|
||||
associated_websites: 'Kapcsolódó weboldalak',
|
||||
auto_arrange: 'Automatikus elrendezés',
|
||||
cancel: 'Mégsem',
|
||||
change_email: 'Email módosítása',
|
||||
change_language: 'Nyelv módosítása',
|
||||
change_password: 'Jelszó módosítása',
|
||||
change_username: 'Felhasználónév módosítása',
|
||||
close: 'Bezár',
|
||||
close_all_windows: 'Minden ablak bezárása',
|
||||
close_all_windows_and_log_out: 'Ablakok bezárása és kijelentkezés',
|
||||
change_always_open_with: 'Mindig ezzel az alkalmazással nyitja meg ezt a fájlt?',
|
||||
confirm: 'Megerősít',
|
||||
@@ -86,7 +84,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.',
|
||||
@@ -94,7 +91,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.',
|
||||
@@ -121,7 +117,6 @@ const hu = {
|
||||
general: 'Általános',
|
||||
get_a_copy_of_on_puter: "Szerezz egy példányt a(z) '%%' Puter.com-on!",
|
||||
get_copy_link: 'Másolási link megszerzése',
|
||||
hide_all_windows: 'Minden ablak elrejtése',
|
||||
home: 'Otthon',
|
||||
html_document: 'HTML dokumentum',
|
||||
hue: 'Színárnyalat',
|
||||
@@ -130,7 +125,6 @@ const hu = {
|
||||
item: 'elem',
|
||||
items_in_trash_cannot_be_renamed: 'Ez az elem nem nevezhető át, mert a kukában van. A név megváltoztatásához először húzd ki a Kukából.',
|
||||
jpeg_image: 'JPEG kép',
|
||||
keep_in_taskbar: 'Tartsa a tálcán',
|
||||
language: 'Nyelv',
|
||||
license: 'Licenc',
|
||||
lightness: 'Világosság',
|
||||
@@ -176,7 +170,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',
|
||||
@@ -196,7 +189,6 @@ const hu = {
|
||||
refer_friends_c2a: 'Kapj 1 GB-ot minden barátodért, aki létrehoz és megerősít egy fiókot a Puteren. A barátod is kap 1 GB-ot!',
|
||||
refer_friends_social_media_c2a: 'Kapj 1 GB ingyenes tárhelyet a Puter.com-on!',
|
||||
refresh: 'Frissítés',
|
||||
remove_from_taskbar: 'Eltávolítás a tálcáról',
|
||||
rename: 'Átnevezés',
|
||||
replace: 'Csere',
|
||||
replace_all: 'Összes cseréje',
|
||||
@@ -224,7 +216,6 @@ const hu = {
|
||||
share: 'Megosztás',
|
||||
share_to: 'Megosztás ide:',
|
||||
shortcut_to: 'Parancsikon ide:',
|
||||
show_all_windows: 'Összes ablak megjelenítése',
|
||||
show_hidden: 'Rejtett megjelenítése',
|
||||
sign_in_with_puter: 'Bejelentkezés Puterrel',
|
||||
sign_up: 'Regisztráció',
|
||||
@@ -356,8 +347,6 @@ const hu = {
|
||||
'create_shortcut_s': 'Parancsikonok létrehozása', // English: "Create Shortcuts"
|
||||
'minimize': 'Minimalizálás', // English: "Minimize"
|
||||
'reload_app': 'Alkalmazás újratöltése', // English: "Reload App"
|
||||
'new_window': 'Új ablak', // English: "New Window"
|
||||
'open_trash': 'Szemetes megnyitása', // English: "Open Trash"
|
||||
'publish_as_serverless_worker': 'Munkás közzététele', // English: "Publish as Worker"
|
||||
'toolbar.enter_fullscreen': 'Teljes képernyő', // English: "Enter Full Screen"
|
||||
'toolbar.github': 'GitHub', // English: "GitHub"
|
||||
@@ -389,14 +378,8 @@ const hu = {
|
||||
'window_title_publish_worker': 'Munkás közzététele', // English: "Publish Worker"
|
||||
'window_title_authenticating': 'Hitelesítés...', // English: "Authenticating..."
|
||||
'window_title_refer_friend': 'Barát ajánlása!', // English: "Refer a friend!"
|
||||
'desktop_show_desktop': 'Asztal megjelenítése', // English: "Show Desktop"
|
||||
'desktop_show_open_windows': 'Megnyitott ablakok megjelenítése', // English: "Show Open Windows"
|
||||
'desktop_exit_full_screen': 'Kilépés teljes képernyőből', // English: "Exit Full Screen"
|
||||
'desktop_enter_full_screen': 'Teljes képernyő', // English: "Enter Full Screen"
|
||||
'desktop_position': 'Pozíció', // English: "Position"
|
||||
'desktop_position_left': 'Bal', // English: "Left"
|
||||
'desktop_position_bottom': 'Alul', // English: "Bottom"
|
||||
'desktop_position_right': 'Jobb', // English: "Right"
|
||||
'item_shared_with_you': 'Egy felhasználó megosztotta Önnel ezt az elemet.', // English: "A user has shared this item with you."
|
||||
'item_shared_by_you': 'Legalább egy másik felhasználóval megosztotta ezt az elemet.', // English: "You have shared this item with at least one other user."
|
||||
'item_shortcut': 'Parancsikon', // English: "Shortcut"
|
||||
|
||||
@@ -31,14 +31,12 @@ const hy = {
|
||||
allow: 'Թույլատրել',
|
||||
ascending: 'Աճող',
|
||||
associated_websites: 'Կապված կայքեր',
|
||||
auto_arrange: 'Ավտոմատ դասավորել',
|
||||
cancel: 'Չեղարկել',
|
||||
change_email: 'Փոխել էլ. փոստը',
|
||||
change_language: 'Փոխել լեզուն',
|
||||
change_password: 'Փոխել գաղտնաբառը',
|
||||
change_username: 'Փոխել օգտանունը',
|
||||
close: 'Փակել',
|
||||
close_all_windows: 'Փակել բոլոր պատուհանները',
|
||||
close_all_windows_and_log_out: 'Փակել պատուհաններն ու դուրս գալ',
|
||||
change_always_open_with: 'Ցանկանու՞մ եք այս տեսակի ֆայլը միշտ բացել',
|
||||
confirm: 'Հաստատել',
|
||||
@@ -93,7 +91,6 @@ const hy = {
|
||||
desktop: 'Աշխատասեղան',
|
||||
desktop_background_fit: 'Հարմարեցնել',
|
||||
developers: 'Ծրագրավորողներ',
|
||||
dir_published_as_website: '%strong% հրապարակվել է',
|
||||
disable_2fa: 'Անջատել 2FA',
|
||||
disable_2fa_confirm: 'Վստա՞հ եք, որ ցանկանում եք անջատել 2FA-ն:',
|
||||
disable_2fa_instructions:
|
||||
@@ -102,7 +99,6 @@ const hy = {
|
||||
dont_allow: 'Թույլ չտալ',
|
||||
download: 'Ներբեռնել',
|
||||
download_file: 'Ներբեռնել ֆայլը',
|
||||
downloading: 'Ներբեռնվում է',
|
||||
email: 'Էլեկտրոնային հասցե',
|
||||
email_change_confirmation_sent:
|
||||
'Հաստատման նամակը ուղարկվել է ձեր նոր էլ. հասցեին։ Խնդրում ենք ստուգեք ձեր փոստարկղը և հետևեք ցուցումներին՝ գործընթացը ավարտելու համար:',
|
||||
@@ -134,7 +130,6 @@ const hy = {
|
||||
general: 'Ընդհանուր',
|
||||
get_a_copy_of_on_puter: "Ստանալ '%%'-ի պատճենը Puter.com-ում!",
|
||||
get_copy_link: 'Ստանալ պատճենված հղումը',
|
||||
hide_all_windows: 'Թաքցնել բոլոր պատուհանները',
|
||||
home: 'Գլխավոր',
|
||||
html_document: 'HTML փաստաթուղթ',
|
||||
hue: 'Երանգ',
|
||||
@@ -144,7 +139,6 @@ const hy = {
|
||||
items_in_trash_cannot_be_renamed:
|
||||
'Այս տարրը չի կարող վերանվանվել, քանի որ այն աղբարկղում է: Այս տարրը վերանվանելու համար նախ տեղափոխեք այն աղբարկղից:',
|
||||
jpeg_image: 'JPEG պատկեր',
|
||||
keep_in_taskbar: 'Պահպանել խնդրագոտում',
|
||||
language: 'Լեզու',
|
||||
license: 'Լիցենզիա',
|
||||
lightness: 'Լուսավորություն',
|
||||
@@ -195,7 +189,6 @@ const hy = {
|
||||
paste_into_folder: 'Տեղադրել պանակում',
|
||||
path: 'Ուղի',
|
||||
personalization: 'Անհատականացում',
|
||||
pick_name_for_website: 'Ընտրեք անուն ձեր կայքի համար',
|
||||
pictures: 'Նկարներ',
|
||||
plural_suffix: 'ներ',
|
||||
powered_by_puter_js: 'Աջակցվում է {{link=docs}}Puter.js{{/link}}-ի կողմից',
|
||||
@@ -217,7 +210,6 @@ const hy = {
|
||||
'Ստացեք 1 ԳԲ յուրաքանչյուր ընկերոջ համար, ով ստեղծում և հաստատում է հաշիվ Փութերում: Ձեր ընկերը նույնպես կստանա 1 ԳԲ!',
|
||||
refer_friends_social_media_c2a: 'Ստացեք 1 ԳԲ անվճար պահեստ Puter.com-ում!',
|
||||
refresh: 'Թարմացնել',
|
||||
remove_from_taskbar: 'Հանել խնդրագոտուց',
|
||||
rename: 'Վերանվանել',
|
||||
replace: 'Փոխարինել',
|
||||
replace_all: 'Փոխարինել բոլորը',
|
||||
@@ -249,7 +241,6 @@ const hy = {
|
||||
share: 'Տարածել',
|
||||
share_to: 'Տարածել դեպի',
|
||||
shortcut_to: 'Դյուրանցում դեպի',
|
||||
show_all_windows: 'Ցույց տալ բոլոր պատուհանները',
|
||||
show_hidden: 'Ցույց տալ թաքնված տարրերը',
|
||||
sign_in_with_puter: 'Մուտք գործել Փութերի միջոցով',
|
||||
sign_up: 'Գրանցվել',
|
||||
@@ -405,8 +396,6 @@ const hy = {
|
||||
create_shortcut_s: 'Ստեղծել դյուրանցումներ',
|
||||
minimize: 'Փոքրացնել',
|
||||
reload_app: 'Վերաբեռնել հավելվածը',
|
||||
new_window: 'Նոր պատուհան',
|
||||
open_trash: 'Բացել Աղբամանը',
|
||||
publish_as_serverless_worker: 'Հրապարակել որպես Worker',
|
||||
'toolbar.enter_fullscreen': 'Լիաէկրան ռեժիմ',
|
||||
'toolbar.github': 'GitHub',
|
||||
@@ -440,14 +429,8 @@ const hy = {
|
||||
window_title_publish_worker: 'Հրապարակել Worker',
|
||||
window_title_authenticating: 'Նույնականացում…',
|
||||
window_title_refer_friend: 'Հրավիրել ընկերոջը',
|
||||
desktop_show_desktop: 'Ցուցադրել աշխատասեղանը',
|
||||
desktop_show_open_windows: 'Ցուցադրել բաց պատուհանները',
|
||||
desktop_exit_full_screen: 'Ելք լիաէկրան ռեժիմից',
|
||||
desktop_enter_full_screen: 'Լիաէկրան ռեժիմ',
|
||||
desktop_position: 'Դիրք',
|
||||
desktop_position_left: 'Ձախ',
|
||||
desktop_position_bottom: 'Ներքև',
|
||||
desktop_position_right: 'Աջ',
|
||||
item_shared_with_you: 'Օգտատերը կիսվել է այս տարրով ձեզ հետ.',
|
||||
item_shared_by_you:
|
||||
'Դուք կիսվել եք այս տարրով առնվազն մեկ այլ օգտատիրոջ հետ.',
|
||||
|
||||
@@ -31,14 +31,12 @@ const id = {
|
||||
allow: 'Izinkan',
|
||||
ascending: 'Menaik',
|
||||
associated_websites: 'Situs Web Terkait',
|
||||
auto_arrange: 'Atur Otomatis',
|
||||
cancel: 'Batal',
|
||||
change_email: 'Ubah Email',
|
||||
change_language: 'Ubah Bahasa',
|
||||
change_password: 'Ubah Kata Sandi',
|
||||
change_username: 'Ubah Nama Pengguna',
|
||||
close: 'Tutup',
|
||||
close_all_windows: 'Tutup Semua Jendela',
|
||||
close_all_windows_and_log_out: 'Tutup Jendela dan Keluar',
|
||||
change_always_open_with: 'Apakah Anda ingin selalu membuka jenis file ini dengan',
|
||||
confirm: 'Konfirmasi',
|
||||
@@ -93,7 +91,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.',
|
||||
@@ -101,7 +98,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.',
|
||||
@@ -132,7 +128,6 @@ const id = {
|
||||
general: 'Umum',
|
||||
get_a_copy_of_on_puter: 'Dapatkan salinan \'%%\' di Puter.com!',
|
||||
get_copy_link: 'Dapatkan Salinan Tautan',
|
||||
hide_all_windows: 'Sembunyikan Semua Jendela',
|
||||
home: 'Beranda',
|
||||
html_document: 'Dokumen HTML',
|
||||
hue: 'Hue',
|
||||
@@ -141,7 +136,6 @@ const id = {
|
||||
item: 'barang',
|
||||
items_in_trash_cannot_be_renamed: 'Barang ini tidak dapat diganti namanya karena berada di sampah. Untuk mengganti nama barang ini, pindahkan dahulu dari Sampah.',
|
||||
jpeg_image: 'Gambar JPEG',
|
||||
keep_in_taskbar: 'Pertahankan di Bilah Tugas',
|
||||
language: 'Bahasa',
|
||||
license: 'Lisensi',
|
||||
lightness: 'Kecerahan',
|
||||
@@ -191,7 +185,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}}',
|
||||
@@ -212,7 +205,6 @@ const id = {
|
||||
'Dapatkan 1 GB untuk setiap teman yang membuat dan mengonfirmasi akun di Puter. Teman Anda juga akan mendapatkan 1 GB!',
|
||||
refer_friends_social_media_c2a: 'Dapatkan 1 GB penyimpanan gratis di Puter.com!',
|
||||
refresh: 'Muat Ulang',
|
||||
remove_from_taskbar: 'Hapus dari Bilah Tugas',
|
||||
rename: 'Ganti Nama',
|
||||
replace: 'Ganti',
|
||||
replace_all: 'Ganti Semua',
|
||||
@@ -241,7 +233,6 @@ const id = {
|
||||
share: 'Bagikan',
|
||||
share_to: 'Bagikan ke',
|
||||
shortcut_to: 'Pintasan ke',
|
||||
show_all_windows: 'Tampilkan Semua Jendela',
|
||||
show_hidden: 'Tampilkan yang tersembunyi',
|
||||
sign_in_with_puter: 'Masuk dengan Puter',
|
||||
sign_up: 'Daftar',
|
||||
@@ -388,8 +379,6 @@ const id = {
|
||||
'create_shortcut_s': 'Buat Pintasan', // In English: "Create Shortcuts"
|
||||
'minimize': 'Kecilkan', // In English: "Minimize"
|
||||
'reload_app': 'Muat Ulang Aplikasi', // In English: "Reload App"
|
||||
'new_window': 'Jendela Baru', // In English: "New Window"
|
||||
'open_trash': 'Buka Tempat Sampah', // In English: "Open Trash"
|
||||
'publish_as_serverless_worker': 'Publikasikan sebagai Worker', // In English: "Publish as Worker"
|
||||
'toolbar.enter_fullscreen': 'Masuk ke Layar Penuh', // In English: "Enter Full Screen"
|
||||
'toolbar.github': 'Github', // In English: "GitHub"
|
||||
@@ -421,14 +410,8 @@ const id = {
|
||||
'window_title_publish_worker': 'Publikasikan Worker', // In English: "Publish Worker"
|
||||
'window_title_authenticating': 'Proses Autentikasi...', // In English: "Authenticating..."
|
||||
'window_title_refer_friend': 'Ajak Teman!', // In English: "Refer a friend!"
|
||||
'desktop_show_desktop': 'Tunjukkan Desktop', // In English: "Show Desktop"
|
||||
'desktop_show_open_windows': 'Tampilkan Jendela yang Terbuka', // In English: "Show Open Windows"
|
||||
'desktop_exit_full_screen': 'Keluar dari Layar Penuh', // In English: "Exit Full Screen"
|
||||
'desktop_enter_full_screen': 'Masuk ke Layar Penuh', // In English: "Enter Full Screen"
|
||||
'desktop_position': 'Posisi', // In English: "Position"
|
||||
'desktop_position_left': 'Kiri', // In English: "Left"
|
||||
'desktop_position_bottom': 'Bawah', // In English: "Bottom"
|
||||
'desktop_position_right': 'Kanan', // In English: "Right"
|
||||
'item_shared_with_you': 'Pengguna lain berbagi barang ini dengan Anda', // In English: "A user has shared this item with you."
|
||||
'item_shared_by_you': 'Anda telah membagikan barang ini dengan setidaknya satu pengguna lain', // In English: "You have shared this item with at least one other user."
|
||||
'item_shortcut': 'Pintasan', // In English: "Shortcut"
|
||||
|
||||
@@ -31,14 +31,12 @@ const ig = {
|
||||
allow: 'ekwe',
|
||||
ascending: 'Na-arịgo',
|
||||
associated_websites: 'Weebụsaịtị Ejikọtara',
|
||||
auto_arrange: 'ndokwa onwe',
|
||||
cancel: 'Kagbuo',
|
||||
change_email: 'ịgbanwe email',
|
||||
change_language: 'ịgbanwe Asụsụ',
|
||||
change_password: 'ịgbanwe paswọọdụ',
|
||||
change_username: 'ịgbanwe aha njirimara',
|
||||
close: 'mmechi',
|
||||
close_all_windows: 'mmechi Windows niile',
|
||||
close_all_windows_and_log_out: 'mmechi Windows na wee pụọ',
|
||||
change_always_open_with: 'Ị chọrọ I na mepee ụdị faịlụ site na',
|
||||
confirm: 'gosi',
|
||||
@@ -94,7 +92,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.',
|
||||
@@ -102,7 +99,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ụ.',
|
||||
@@ -133,7 +129,6 @@ const ig = {
|
||||
general: 'Izugbe',
|
||||
get_a_copy_of_on_puter: 'Nweta otu \'%%\' na Puter.com!',
|
||||
get_copy_link: 'Nweta njikọ nke',
|
||||
hide_all_windows: 'Ezo Windows niile',
|
||||
home: 'ụlọ',
|
||||
html_document: 'akwụkwọ HTML',
|
||||
hue: 'Hue',
|
||||
@@ -142,7 +137,6 @@ const ig = {
|
||||
item: 'ihe',
|
||||
items_in_trash_cannot_be_renamed: 'Enweghị ike ịnyegharị ihe a aha n\'ihi na ọ nọ na ahịhịa. Iji nyegharịa ihe a aha, buru ụzọ dọrọ ya na ahịhịa.',
|
||||
jpeg_image: 'Foto JPEG',
|
||||
keep_in_taskbar: 'Debe na Taskbar',
|
||||
language: 'Asụsụ',
|
||||
license: 'ikike',
|
||||
lightness: 'ìhè',
|
||||
@@ -193,7 +187,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}}',
|
||||
@@ -214,7 +207,6 @@ const ig = {
|
||||
'Nweta 1 GB maka enyi ọ bụla mepụtara ma kwado akaụntụ na Puter. Enyi gị ga-enwetakwa 1 GB!',
|
||||
refer_friends_social_media_c2a: 'Nweta 1 GB nke nchekwa efu na Puter.com!',
|
||||
refresh: 'Weghachite ume',
|
||||
remove_from_taskbar: 'Wepu na Taskbar',
|
||||
rename: 'Nyegharịa aha',
|
||||
replace: 'Dochie',
|
||||
replace_all: 'Dochie ihe niile',
|
||||
@@ -243,7 +235,6 @@ const ig = {
|
||||
share: 'ike',
|
||||
share_to: 'ike nye',
|
||||
shortcut_to: 'Ụzọ mkpirisi ka',
|
||||
show_all_windows: 'Gosi Windows niile',
|
||||
show_hidden: 'Gosi ihe ezozo',
|
||||
sign_in_with_puter: 'Jiri Puter banye',
|
||||
sign_up: 'Debanye aha',
|
||||
@@ -390,8 +381,6 @@ const ig = {
|
||||
'create_shortcut_s': 'Mepụta ụzọ mkpirisi', // In English: "Create Shortcuts"
|
||||
'minimize': 'Wedata', // In English: "Minimize"
|
||||
'reload_app': 'Bugharịa ngwa', // In English: "Reload App"
|
||||
'new_window': 'Window ọhụrụ', // In English: "New Window"
|
||||
'open_trash': 'Mepee ahịhịa', // In English: "Open Trash"
|
||||
'publish_as_serverless_worker': 'Bipụta dị ka onye ọrụ', // In English: "Publish as Worker"
|
||||
'toolbar.enter_fullscreen': 'Tinye ihuenyo zuru oke', // In English: "Enter Full Screen"
|
||||
'toolbar.github': 'GitHub', // In English: "GitHub"
|
||||
@@ -423,14 +412,8 @@ const ig = {
|
||||
'window_title_publish_worker': 'Bipụta Onye Ọrụ', // In English: "Publish Worker"
|
||||
'window_title_authenticating': 'Na-achọpụta...', // In English: "Authenticating..."
|
||||
'window_title_refer_friend': 'Tụgharịa enyi!', // In English: "Refer a friend!"
|
||||
'desktop_show_desktop': 'Gosi Desktọpụ', // In English: "Show Desktop"
|
||||
'desktop_show_open_windows': 'Gosi Windows mepere emepe', // In English: "Show Open Windows"
|
||||
'desktop_exit_full_screen': 'Wepụ ihuenyo zuru oke', // In English: "Exit Full Screen"
|
||||
'desktop_enter_full_screen': 'Tinye ihuenyo zuru oke', // In English: "Enter Full Screen"
|
||||
'desktop_position': 'Ọnọdụ', // In English: "Position"
|
||||
'desktop_position_left': 'Aka ekpe', // In English: "Left"
|
||||
'desktop_position_bottom': "N'okpuru", // In English: "Bottom"
|
||||
'desktop_position_right': 'Right', // In English: "Right"
|
||||
'item_shared_with_you': 'Onye ọrụ ekenyela gị ihe a.', // In English: "A user has shared this item with you."
|
||||
'item_shared_by_you': 'Ị kekọrịtala ihe a na opekata mpe otu onye ọrụ ọzọ', // In English: " with at least one other user."
|
||||
'item_shortcut': 'Ụzọ mkpirisi', // In English: "Shortcut"
|
||||
|
||||
@@ -31,14 +31,12 @@ const it = {
|
||||
allow: 'Consenti',
|
||||
ascending: 'Ascendente',
|
||||
associated_websites: 'Siti associati',
|
||||
auto_arrange: 'Organizzazione automatica',
|
||||
cancel: 'Annulla',
|
||||
change_email: 'Modifica Email',
|
||||
change_language: 'Cambia lingua',
|
||||
change_password: 'Modifica password',
|
||||
change_username: 'Modifica Nome Utente',
|
||||
close: 'Chiudi',
|
||||
close_all_windows: 'Chiudi tutte le finestre',
|
||||
close_all_windows_and_log_out: 'Chiudi tutte le finestre e disconnettiti',
|
||||
change_always_open_with: 'Vuoi sempre aprire questo tipo di file con',
|
||||
confirm: 'Conferma',
|
||||
@@ -94,7 +92,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:
|
||||
@@ -103,7 +100,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.",
|
||||
@@ -134,7 +130,6 @@ const it = {
|
||||
general: 'Generale',
|
||||
get_a_copy_of_on_puter: 'Ottieni una copia di \'%%\' su Puter.com!',
|
||||
get_copy_link: 'Ottieni link di copia',
|
||||
hide_all_windows: 'Nascondi tutte le finestre',
|
||||
home: 'Home',
|
||||
html_document: 'Documento HTML',
|
||||
hue: 'Tonalità',
|
||||
@@ -143,7 +138,6 @@ const it = {
|
||||
item: 'elemento',
|
||||
items_in_trash_cannot_be_renamed: 'Impossibile rinominare un elemento nel Cestino. Per rinominarlo, è necessario ripristinarlo.',
|
||||
jpeg_image: 'Immagine JPEG',
|
||||
keep_in_taskbar: 'Blocca nella barra delle applicazioni',
|
||||
language: 'Lingua',
|
||||
license: 'Licenza',
|
||||
lightness: 'Luminosità',
|
||||
@@ -194,7 +188,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}}',
|
||||
@@ -215,7 +208,6 @@ const it = {
|
||||
'Ottieni 1 GB di spazio di archiviazione per ogni amico che crea un account e conferma l’email su Puter. Anche il tuo amico riceverà dello spazio extra!',
|
||||
refer_friends_social_media_c2a: 'Ottieni 1GB di spazio di spazio di archiviazione gratuito su Puter.com!',
|
||||
refresh: 'Ricarica',
|
||||
remove_from_taskbar: 'Sblocca dalla barra delle applicazioni',
|
||||
rename: 'Rinomina',
|
||||
replace: 'Sostituisci',
|
||||
replace_all: 'Sostituisci tutto',
|
||||
@@ -247,7 +239,6 @@ const it = {
|
||||
share: 'Condividi',
|
||||
share_to: 'Condividi con',
|
||||
shortcut_to: 'Scorciatoia per',
|
||||
show_all_windows: 'Mostra tutte le finestre',
|
||||
show_hidden: 'Mostra nascosti',
|
||||
sign_in_with_puter: 'Accedi con Puter',
|
||||
sign_up: 'Registrati',
|
||||
@@ -398,8 +389,6 @@ const it = {
|
||||
'create_shortcut_s': 'Crea scorciatoie',
|
||||
'minimize': 'Riduci',
|
||||
'reload_app': 'Ricarica app',
|
||||
'new_window': 'Nuova finestra',
|
||||
'open_trash': 'Apri cestino',
|
||||
'publish_as_serverless_worker': 'Pubblica come funzione serverless', // In English: "Publish as Worker"
|
||||
'toolbar.enter_fullscreen': 'Visualizza a schermo intero',
|
||||
'toolbar.github': 'GitHub',
|
||||
@@ -431,14 +420,8 @@ const it = {
|
||||
'window_title_publish_worker': 'Pubblica worker', // In English: "Publish Worker"
|
||||
'window_title_authenticating': 'Autenticazione in corso...',
|
||||
'window_title_refer_friend': 'Invita un amico!',
|
||||
'desktop_show_desktop': 'Mostra desktop',
|
||||
'desktop_show_open_windows': 'Mostra finestre aperte',
|
||||
'desktop_exit_full_screen': 'Esci da schermo intero',
|
||||
'desktop_enter_full_screen': 'Visualizza a schermo intero',
|
||||
'desktop_position': 'Posizione',
|
||||
'desktop_position_left': 'Sinistra',
|
||||
'desktop_position_bottom': 'In basso',
|
||||
'desktop_position_right': 'Destra',
|
||||
'item_shared_with_you': 'Un utente ha condiviso questo elemento con te.',
|
||||
'item_shared_by_you': 'Hai condiviso questo elemento con almeno un altro utente',
|
||||
'item_shortcut': 'Scorciatoia',
|
||||
|
||||
@@ -31,14 +31,12 @@ const ja = {
|
||||
allow: '許可',
|
||||
ascending: '昇順',
|
||||
associated_websites: '関連ウェブサイト',
|
||||
auto_arrange: '自動配置',
|
||||
cancel: 'キャンセル',
|
||||
change_email: 'メールアドレスを変更',
|
||||
change_language: '言語を変更',
|
||||
change_password: 'パスワードを変更',
|
||||
change_username: 'ユーザー名を変更',
|
||||
close: '閉じる',
|
||||
close_all_windows: 'すべてのウィンドウを閉じる',
|
||||
close_all_windows_and_log_out: 'ウィンドウを閉じてログアウト',
|
||||
change_always_open_with: 'この種類のファイルを常にこのアプリで開きますか?',
|
||||
confirm: '確認',
|
||||
@@ -86,7 +84,6 @@ const ja = {
|
||||
desktop: 'デスクトップ',
|
||||
desktop_background_fit: '画面背景をフィット',
|
||||
developers: '開発者',
|
||||
dir_published_as_website: '%strong% が公開されました:',
|
||||
disable_2fa: '2FAを無効にする',
|
||||
disable_2fa_confirm: '2FAを無効にしてよろしいですか?',
|
||||
disable_2fa_instructions: 'パスワードを入力して2FAを無効にします。',
|
||||
@@ -94,7 +91,6 @@ const ja = {
|
||||
dont_allow: '許可しない',
|
||||
download: 'ダウンロード',
|
||||
download_file: 'ファイルをダウンロード',
|
||||
downloading: 'ダウンロード中',
|
||||
email: 'メール',
|
||||
email_change_confirmation_sent: '確認メールが新しいメールアドレスに送信されました。受信トレイを確認し、指示に従って手続きを完了してください。',
|
||||
email_invalid: 'メールアドレスが無効です。',
|
||||
@@ -121,7 +117,6 @@ const ja = {
|
||||
general: '一般',
|
||||
get_a_copy_of_on_puter: 'Puter.comで \'%%\' のコピーを取得!',
|
||||
get_copy_link: 'コピーリンクを取得',
|
||||
hide_all_windows: 'すべてのウィンドウを隠す',
|
||||
home: 'ホーム',
|
||||
html_document: 'HTML文書',
|
||||
hue: '色合い',
|
||||
@@ -130,7 +125,6 @@ const ja = {
|
||||
item: 'アイテム',
|
||||
items_in_trash_cannot_be_renamed: 'このアイテムはゴミ箱にあるため、名前を変更できません。このアイテムの名前を変更するには、まずゴミ箱からドラッグしてください。',
|
||||
jpeg_image: 'JPEG画像',
|
||||
keep_in_taskbar: 'タスクバーに保持',
|
||||
language: '言語',
|
||||
license: 'ライセンス',
|
||||
lightness: '明るさ',
|
||||
@@ -176,7 +170,6 @@ const ja = {
|
||||
paste_into_folder: 'フォルダーに貼り付け',
|
||||
path: 'パス',
|
||||
personalization: 'パーソナライズ',
|
||||
pick_name_for_website: 'ウェブサイトの名前を選んでください:',
|
||||
pictures: '写真',
|
||||
plural_suffix: '',
|
||||
powered_by_puter_js: '{{link=docs}}Puter.js{{/link}} によって提供されています',
|
||||
@@ -196,7 +189,6 @@ const ja = {
|
||||
refer_friends_c2a: '友達がPuterでアカウントを作成して確認すると、1GBを獲得できます。友達も1GBを獲得できます!',
|
||||
refer_friends_social_media_c2a: 'Puter.comで1GBの無料ストレージを手に入れよう!',
|
||||
refresh: '更新',
|
||||
remove_from_taskbar: 'タスクバーから削除',
|
||||
rename: '名前を変更',
|
||||
replace: '置換',
|
||||
replace_all: 'すべて置換',
|
||||
@@ -224,7 +216,6 @@ const ja = {
|
||||
share: '共有',
|
||||
share_to: '共有先',
|
||||
shortcut_to: 'ショートカット先',
|
||||
show_all_windows: 'すべてのウィンドウを表示',
|
||||
show_hidden: '隠しファイルを表示',
|
||||
sign_in_with_puter: 'Puterでサインイン',
|
||||
sign_up: 'サインアップ',
|
||||
@@ -369,8 +360,6 @@ const ja = {
|
||||
'create_shortcut_s': 'ショートカットを作成', // In English: "Create Shortcuts"
|
||||
'minimize': '最小化', // In English: "Minimize"
|
||||
'reload_app': 'アプリを再読み込み', // In English: "Reload App"
|
||||
'new_window': '新しいウィンドウ', // In English: "New Window"
|
||||
'open_trash': 'ゴミ箱を開く', // In English: "Open Trash"
|
||||
'publish_as_serverless_worker': 'ワーカーとして公開', // In English: "Publish as Worker"
|
||||
'toolbar.enter_fullscreen': '全画面表示にする', // In English: "Enter Full Screen"
|
||||
'toolbar.github': 'GitHub', // In English: "GitHub"
|
||||
@@ -402,14 +391,8 @@ const ja = {
|
||||
'window_title_publish_worker': 'ワーカーを公開', // In English: "Publish Worker"
|
||||
'window_title_authenticating': '認証中...', // In English: "Authenticating..."
|
||||
'window_title_refer_friend': '友達を紹介しよう!', // In English: "Refer a friend!"
|
||||
'desktop_show_desktop': 'デスクトップを表示', // In English: "Show Desktop"
|
||||
'desktop_show_open_windows': '開いているウィンドウを表示', // In English: "Show Open Windows"
|
||||
'desktop_exit_full_screen': '全画面表示を終了', // In English: "Exit Full Screen"
|
||||
'desktop_enter_full_screen': '全画面表示にする', // In English: "Enter Full Screen"
|
||||
'desktop_position': '位置', // In English: "Position"
|
||||
'desktop_position_left': '左', // In English: "Left"
|
||||
'desktop_position_bottom': '下', // In English: "Bottom"
|
||||
'desktop_position_right': '右', // In English: "Right"
|
||||
'item_shared_with_you': 'ユーザーがこのアイテムをあなたと共有しました。', // In English: "A user has shared this item with you."
|
||||
'item_shared_by_you': 'このアイテムは少なくとも1人以上のユーザーと共有されています。', // In English: "You have shared this item with at least one other user."
|
||||
'item_shortcut': 'ショートカット', // In English: "Shortcut"
|
||||
|
||||
@@ -31,14 +31,12 @@ const ko = {
|
||||
allow: '허용',
|
||||
ascending: '오름차순',
|
||||
associated_websites: '연결된 웹사이트',
|
||||
auto_arrange: '자동 정렬',
|
||||
cancel: '취소',
|
||||
change_email: '이메일 변경',
|
||||
change_language: '언어 변경',
|
||||
change_password: '비밀번호 변경',
|
||||
change_username: '사용자 이름 변경',
|
||||
close: '닫기',
|
||||
close_all_windows: '모든 창 닫기',
|
||||
close_all_windows_and_log_out: '창을 닫고 로그아웃',
|
||||
change_always_open_with: '이 형식의 파일을 항상 이 앱으로 여시겠습니까?',
|
||||
confirm: '확인',
|
||||
@@ -95,7 +93,6 @@ const ko = {
|
||||
desktop: '바탕화면',
|
||||
desktop_background_fit: '맞추기',
|
||||
developers: '개발자',
|
||||
dir_published_as_website: '%strong%에 게시되었습니다.',
|
||||
disable_2fa: '2FA 비활성화',
|
||||
disable_2fa_confirm: '정말 2FA를 비활성화하시겠습니까?',
|
||||
disable_2fa_instructions: '2FA를 비활성화하려면 비밀번호를 입력해주세요.',
|
||||
@@ -103,7 +100,6 @@ const ko = {
|
||||
dont_allow: '허용하지 않음',
|
||||
download: '다운로드',
|
||||
download_file: '파일 다운로드',
|
||||
downloading: '다운로드 중',
|
||||
email: '이메일',
|
||||
email_change_confirmation_sent:
|
||||
'새 이메일 주소로 확인 메일이 전송되었습니다. 받은 편지함을 확인 후 안내에 따라 절차를 완료해주세요.',
|
||||
@@ -134,7 +130,6 @@ const ko = {
|
||||
general: '일반',
|
||||
get_a_copy_of_on_puter: 'Puter.com에서 \'%%\'의 사본을 받으세요!',
|
||||
get_copy_link: '링크 복사',
|
||||
hide_all_windows: '모든 창 숨기기',
|
||||
home: '홈',
|
||||
html_document: 'HTML 문서',
|
||||
hue: '색조',
|
||||
@@ -143,7 +138,6 @@ const ko = {
|
||||
item: '항목', //
|
||||
items_in_trash_cannot_be_renamed: '이 항목은 휴지통에 있어 이름을 변경할 수 없습니다. 이름을 변경하려면 먼저 휴지통에서 꺼내야 합니다.',
|
||||
jpeg_image: 'JPEG 이미지',
|
||||
keep_in_taskbar: '작업 표시줄에 유지',
|
||||
language: '언어',
|
||||
license: '라이선스',
|
||||
lightness: '밝기',
|
||||
@@ -170,13 +164,11 @@ const ko = {
|
||||
new_folder: '새 폴더',
|
||||
new_password: '새 비밀번호',
|
||||
new_username: '새 사용자 이름',
|
||||
new_window: '새 창',
|
||||
no: '아니오',
|
||||
ok: '확인',
|
||||
open: '열기',
|
||||
open_in_new_tab: '새 탭에서 열기',
|
||||
open_in_new_window: '새 창에서 열기',
|
||||
open_trash: '휴지통 열기',
|
||||
open_with: '다른 앱으로 열기',
|
||||
original_name: '원본 이름',
|
||||
original_path: '원본 경로',
|
||||
@@ -198,7 +190,6 @@ const ko = {
|
||||
paste_into_folder: '폴더에 붙여넣기',
|
||||
path: '경로',
|
||||
personalization: '개인 설정',
|
||||
pick_name_for_website: '웹사이트 이름을 선택하세요.',
|
||||
pictures: '사진',
|
||||
plural_suffix: '',
|
||||
powered_by_puter_js: '{{link=docs}}Puter.js{{/link}} 제공',
|
||||
@@ -221,7 +212,6 @@ const ko = {
|
||||
'Puter에 가입하고 이메일 인증을 완료하는 친구 한 명당 1GB의 저장 공간을 드립니다. 친구도 1GB를 받을 수 있습니다!',
|
||||
refer_friends_social_media_c2a: 'Puter.com에서 1GB의 무료 저장 공간을 받아보세요!',
|
||||
refresh: '새로 고침',
|
||||
remove_from_taskbar: '작업 표시줄에서 제거',
|
||||
rename: '이름 변경',
|
||||
replace: '교체',
|
||||
replace_all: '모두 교체',
|
||||
@@ -250,7 +240,6 @@ const ko = {
|
||||
share: '공유',
|
||||
share_to: '공유처',
|
||||
shortcut_to: '바로 가기',
|
||||
show_all_windows: '모든 창 표시',
|
||||
show_hidden: '숨김 항목 표시',
|
||||
sign_in_with_puter: 'Puter로 로그인',
|
||||
sign_up: '가입',
|
||||
@@ -426,14 +415,8 @@ const ko = {
|
||||
'window_title_refer_friend': '친구 추천!',
|
||||
|
||||
// Desktop UI
|
||||
'desktop_show_desktop': '바탕화면 보기',
|
||||
'desktop_show_open_windows': '열려있는 창 보기',
|
||||
'desktop_exit_full_screen': '전체 화면 종료',
|
||||
'desktop_enter_full_screen': '전체 화면 시작',
|
||||
'desktop_position': '위치',
|
||||
'desktop_position_left': '왼쪽',
|
||||
'desktop_position_bottom': '아래',
|
||||
'desktop_position_right': '오른쪽',
|
||||
// Item UI
|
||||
'item_shared_with_you': '다른 사용자가 이 항목을 당신과 공유했습니다.',
|
||||
'item_shared_by_you': '이 항목을 한 명 이상의 다른 사용자와 공유했습니다.',
|
||||
|
||||
@@ -31,14 +31,12 @@ const ku = {
|
||||
allow: 'ڕێگەدان',
|
||||
ascending: 'سەرووەی',
|
||||
associated_websites: 'وێبسایتەکان پەیوەندیدار',
|
||||
auto_arrange: 'ڕیزکردنی ئۆتۆماتیکی',
|
||||
cancel: 'ڕەتکردنەوە',
|
||||
change_email: 'گۆڕینی ئیمەیل',
|
||||
change_language: 'گۆڕینی زمان',
|
||||
change_password: 'گۆڕینی وشەی تێپەڕ',
|
||||
change_username: 'گۆڕینی ناوی بەکارهێنەر',
|
||||
close: 'داخستن',
|
||||
close_all_windows: 'داخستنی هەموو پەنجەکان',
|
||||
close_all_windows_and_log_out: 'داخستن و چوونە دەرەوە',
|
||||
change_always_open_with: 'ئەتەوێت هەمیشە ئەم جۆرە فایلە بکرێتەوە بە',
|
||||
confirm: 'پشتڕاستکردنەوە',
|
||||
@@ -93,7 +91,6 @@ const ku = {
|
||||
desktop: 'سەروروومیزی',
|
||||
desktop_background_fit: 'ڕێکخستن',
|
||||
developers: 'پەرەپێدانەران',
|
||||
dir_published_as_website: '%strong% بەشکراوە بۆ:',
|
||||
disable_2fa: 'ناچالاککردنی 2FA',
|
||||
disable_2fa_confirm: 'دڵنیایت کە ئەتەوێت 2FA ناچالاک بکەیت؟',
|
||||
disable_2fa_instructions: 'وشەی تێپەڕی نووسە بۆ ناچالاککردنی 2FA.',
|
||||
@@ -101,7 +98,6 @@ const ku = {
|
||||
dont_allow: 'ڕێگەندان',
|
||||
download: 'داگرتن',
|
||||
download_file: 'داگرتنی فایل',
|
||||
downloading: 'داگرتن',
|
||||
email: 'ئیمەیل',
|
||||
email_change_confirmation_sent:
|
||||
'پەیامی پشتڕاستکردنی ئیمەیل بۆ ناونیشانی ئیمەیلە نوێتەوە نێردرا. تکایە پەیامەکانت بپشکنە و ڕێنیشانەکان پەیڕەو بکە بۆ تەواوبوونەوەی ئەم کارە.',
|
||||
@@ -132,7 +128,6 @@ const ku = {
|
||||
general: 'گشتی',
|
||||
get_a_copy_of_on_puter: 'کۆپیێک وەرگرتنەوەی \'%%\' لە پوتەر.com!',
|
||||
get_copy_link: 'کۆپی بەستەر وەرگرتن',
|
||||
hide_all_windows: 'شاردنەوەی هەموو پەنجەکان',
|
||||
home: 'ماڵەوە',
|
||||
html_document: 'بەڵگەی HTML',
|
||||
hue: 'هیو',
|
||||
@@ -141,7 +136,6 @@ const ku = {
|
||||
item: 'بابەت',
|
||||
items_in_trash_cannot_be_renamed: 'ئەم بابەتە ناتوانرێت ناونوسێت چونکە لە کوڵەدا هەیە. بۆ ناونوسین، سەبارەکەی لە کوڵەکە دەر بکە.',
|
||||
jpeg_image: 'وێنەی JPEG',
|
||||
keep_in_taskbar: 'ڕێپێدان لە تاکسبار',
|
||||
language: 'زمان',
|
||||
license: 'لایسەنس',
|
||||
lightness: 'رووناکی',
|
||||
@@ -192,7 +186,6 @@ const ku = {
|
||||
paste_into_folder: 'دانان لە فۆڵدەرەکە',
|
||||
path: 'ڕێکەوت',
|
||||
personalization: 'بەشداربوون',
|
||||
pick_name_for_website: 'ناوێک دیاربکە بۆ وێبسایتەکەت:',
|
||||
pictures: 'وێنەکان',
|
||||
plural_suffix: 'کان',
|
||||
powered_by_puter_js: 'پێشکەشکراوە لە {{link=docs}}پوتەر.js{{/link}}',
|
||||
@@ -213,7 +206,6 @@ const ku = {
|
||||
'1 گیگابایت بۆ هەر هاوڕێک دەست بەرکە، پەیوەندیدانی هەژمارێک بۆ پوتەر دروست بکە و پشتڕاست بکە. هاوڕێتیش 1 گیگابایت وەرگرتن دەبێت!',
|
||||
refer_friends_social_media_c2a: '1 گیگابایت پارێزگاکانی پوتەر.com بۆ هەر هاوڕێک بکە!',
|
||||
refresh: 'بوژانەوە',
|
||||
remove_from_taskbar: 'لابردن لە تاکسبار',
|
||||
rename: 'ناونانەوە',
|
||||
replace: 'لە نوێکردنەوە',
|
||||
replace_all: 'لە نوێکردنەوەی هەموو',
|
||||
@@ -242,7 +234,6 @@ const ku = {
|
||||
share: 'هاوبەشکردن',
|
||||
share_to: 'هاوبەشکردن بۆ',
|
||||
shortcut_to: 'شارتی بۆ',
|
||||
show_all_windows: 'پیشاندانی هەموو پەنجەکان',
|
||||
show_hidden: 'پیشاندانی شاردراوەکان',
|
||||
sign_in_with_puter: 'چوونەژوورەوە بە پوتەر',
|
||||
sign_up: 'خۆتۆمارکردن',
|
||||
@@ -385,8 +376,6 @@ const ku = {
|
||||
'create_shortcut_s': 'دروستکردنی کورتەڕەوەکان', // In English: "Create Shortcuts"
|
||||
'minimize': 'بچووککردنەوە', // In English: "Minimize"
|
||||
'reload_app': 'ئەپلیکەیشن دووبارە بار بکە', // In English: "Reload App"
|
||||
'new_window': 'پەنجەرەی نوێ', // In English: "New Window"
|
||||
'open_trash': 'زبڵدانە بکەرەوە', // In English: "Open Trash"
|
||||
'publish_as_serverless_worker': 'بڵاوکردنەوە وەک کارمەند', // In English: "Publish as Worker"
|
||||
'toolbar.enter_fullscreen': 'بە تەواوی شاشە بچۆرە', // In English: "Enter Full Screen"
|
||||
'toolbar.github': 'GitHub', // In English: "GitHub"
|
||||
@@ -418,14 +407,8 @@ const ku = {
|
||||
'window_title_publish_worker': 'بڵاوکردنەوەی کارمەند', // In English: "Publish Worker"
|
||||
'window_title_authenticating': 'پەسندکردنەوە', // In English: "Authenticating..."
|
||||
'window_title_refer_friend': 'هاوڕێک بانگهێشت بکە!', // In English: "Refer a friend!"
|
||||
'desktop_show_desktop': 'دێسکتۆپ پیشان بدە', // In English: "Show Desktop"
|
||||
'desktop_show_open_windows': 'پەنجەرە کراوەکان پیشان بدە', // In English: "Show Open Windows"
|
||||
'desktop_exit_full_screen': 'دەرچوون لە شاشەی پڕە', // In English: "Exit Full Screen"
|
||||
'desktop_enter_full_screen': 'چوونە شاشەی پڕە', // In English: "Enter Full Screen"
|
||||
'desktop_position': 'شوێن', // In English: "Position"
|
||||
'desktop_position_left': 'چەپ', // In English: "Left"
|
||||
'desktop_position_bottom': 'خوارەوە', // In English: "Bottom"
|
||||
'desktop_position_right': 'ڕاست', // In English: "Right"
|
||||
'item_shared_with_you': 'بەکارهێنەرێک ئەم شتەیە لەگەڵ تۆ هاوبەش کردووە.', // In English: "A user has shared this item with you."
|
||||
'item_shared_by_you': 'تۆ ئەم شتە بە هەندێک بەکارهێنەرێکی تر هاوبەش کردووە.', // In English: "You have shared this item with at least one other user."
|
||||
'item_shortcut': 'کورتەڕەو', // In English: "Shortcut"
|
||||
|
||||
@@ -31,7 +31,6 @@ const ml = {
|
||||
allow: 'അനുവദിക്കുക',
|
||||
ascending: 'ആരോഹണം',
|
||||
associated_websites: 'ബന്ധപ്പെട്ട വെബ്സൈറ്റുകൾ',
|
||||
auto_arrange: 'സ്വയം ക്രമീകരിക്കുക',
|
||||
cancel: 'റദ്ദാക്കുക',
|
||||
change: 'മാറ്റുക',
|
||||
change_always_open_with: 'ഈ തരത്തിലുള്ള ഫയൽ എപ്പോഴും ഇതുപയോഗിച്ച് തുറക്കണോ',
|
||||
@@ -40,7 +39,6 @@ const ml = {
|
||||
change_password: 'പാസ്വേഡ് മാറ്റുക',
|
||||
change_username: 'ഉപയോക്തൃനാമം മാറ്റുക',
|
||||
close: 'അടയ്ക്കുക',
|
||||
close_all_windows: 'എല്ലാ വിൻഡോകളും അടയ്ക്കുക',
|
||||
close_all_windows_and_log_out: 'വിൻഡോകൾ അടച്ച് ലോഗ്ഔട്ട് ചെയ്യുക',
|
||||
confirm: 'സ്ഥിരീകരിക്കുക',
|
||||
confirm_2fa_setup: 'ഞാൻ കോഡ് എന്റെ ഓതന്റിക്കേറ്റർ ആപ്പിലേക്ക് ചേർത്തു',
|
||||
@@ -87,7 +85,6 @@ const ml = {
|
||||
desktop: 'ഡെസ്ക്ടോപ്പ്',
|
||||
desktop_background_fit: 'ഫിറ്റ്',
|
||||
developers: 'ഡെവലപ്പർമാർ',
|
||||
dir_published_as_website: '%strong% എന്നതിലേക്ക് പ്രസിദ്ധീകരിച്ചിരിക്കുന്നു:',
|
||||
disable_2fa: '2FA പ്രവർത്തനരഹിതമാക്കുക',
|
||||
disable_2fa_confirm: '2FA പ്രവർത്തനരഹിതമാക്കണമെന്ന് തീർച്ചയാണോ?',
|
||||
disable_2fa_instructions: '2FA പ്രവർത്തനരഹിതമാക്കാൻ നിങ്ങളുടെ പാസ്വേഡ് നൽകുക.',
|
||||
@@ -95,7 +92,6 @@ const ml = {
|
||||
dont_allow: 'അനുവദിക്കരുത്',
|
||||
download: 'ഡൗൺലോഡ്',
|
||||
download_file: 'ഫയൽ ഡൗൺലോഡ് ചെയ്യുക',
|
||||
downloading: 'ഡൗൺലോഡ് ചെയ്യുന്നു',
|
||||
email: 'ഇമെയിൽ',
|
||||
email_change_confirmation_sent: 'നിങ്ങളുടെ പുതിയ ഇമെയിൽ വിലാസത്തിലേക്ക് ഒരു സ്ഥിരീകരണ ഇമെയിൽ അയച്ചിട്ടുണ്ട്. ദയവായി നിങ്ങളുടെ ഇൻബോ',
|
||||
email_invalid: 'ഇമെയിൽ അസാധുവാണ്.',
|
||||
@@ -122,7 +118,6 @@ const ml = {
|
||||
general: 'പൊതുവായത്',
|
||||
get_a_copy_of_on_puter: 'Puter.com-ൽ \'%%\' എന്നതിന്റെ ഒരു പകർപ്പ് നേടുക!',
|
||||
get_copy_link: 'പകർപ്പ് ലിങ്ക് നേടുക',
|
||||
hide_all_windows: 'എല്ലാ വിൻഡോകളും മറയ്ക്കുക',
|
||||
home: 'ഹോം',
|
||||
html_document: 'HTML ഡോക്യുമെന്റ്',
|
||||
hue: 'ഹ്യൂ',
|
||||
@@ -131,7 +126,6 @@ const ml = {
|
||||
item: 'ഇനം',
|
||||
items_in_trash_cannot_be_renamed: 'ഈ ഇനം ട്രാഷിലായതിനാൽ പേരുമാറ്റാൻ കഴിയില്ല. ഈ ഇനത്തിന്റെ പേരുമാറ്റാൻ, ആദ്യം ഇത് ട്രാഷിൽ നിന്ന് പുറത്തെടുക്കുക.',
|
||||
jpeg_image: 'JPEG ചിത്രം',
|
||||
keep_in_taskbar: 'ടാസ്ക്ബാറിൽ നിലനിർത്തുക',
|
||||
language: 'ഭാഷ',
|
||||
license: 'ലൈസൻസ്',
|
||||
lightness: 'പ്രകാശം',
|
||||
@@ -177,7 +171,6 @@ const ml = {
|
||||
paste_into_folder: 'ഫോൾഡറിലേക്ക് പേസ്റ്റ് ചെയ്യുക',
|
||||
path: 'പാത',
|
||||
personalization: 'വ്യക്തിഗതമാക്കൽ',
|
||||
pick_name_for_website: 'നിങ്ങളുടെ വെബ്സൈറ്റിന് ഒരു പേര് തിരഞ്ഞെടുക്കുക:',
|
||||
pictures: 'ചിത്രങ്ങൾ',
|
||||
plural_suffix: 'കൾ',
|
||||
powered_by_puter_js: '{{link=docs}}Puter.js{{/link}} ഉപയോഗിച്ച് നിർമ്മിച്ചത്',
|
||||
@@ -198,7 +191,6 @@ const ml = {
|
||||
refer_friends_c2a: 'അക്കൗണ്ട് സൃഷ്ടിച്ച് സ്ഥിരീകരിക്കുന്ന ഓരോ സുഹൃത്തിനും 1 GB നേടുക. നിങ്ങളുടെ സുഹൃത്തിനും 1 GB ലഭിക്കും!',
|
||||
refer_friends_social_media_c2a: 'Puter.com-ൽ 1 GB സൗജന്യ സ്റ്റോറേജ് നേടുക!',
|
||||
refresh: 'പുതുക്കുക',
|
||||
remove_from_taskbar: 'ടാസ്ക്ബാറിൽ നിന്ന് നീക്കം ചെയ്യുക',
|
||||
rename: 'പേരുമാറ്റുക',
|
||||
replace: 'മാറ്റിസ്ഥാപിക്കുക',
|
||||
replace_all: 'എല്ലാം മാറ്റിസ്ഥാപിക്കുക',
|
||||
@@ -226,7 +218,6 @@ const ml = {
|
||||
share: 'പങ്കുവയ്ക്കുക',
|
||||
share_to: 'ഇതിലേക്ക് പങ്കുവയ്ക്കുക',
|
||||
shortcut_to: 'ഷോർട്ട്കട്ട്',
|
||||
show_all_windows: 'എല്ലാ വിൻഡോകളും കാണിക്കുക',
|
||||
show_hidden: 'മറഞ്ഞിരിക്കുന്നവ കാണിക്കുക',
|
||||
sign_in_with_puter: 'പ്യൂട്ടർ ഉപയോഗിച്ച് സൈൻ ഇൻ ചെയ്യുക',
|
||||
sign_up: 'സൈൻ അപ്പ്',
|
||||
|
||||
@@ -38,7 +38,6 @@ const my = {
|
||||
allow: 'Benarkan',
|
||||
ascending: 'Menaik',
|
||||
associated_websites: 'Laman Web Berkaitan',
|
||||
auto_arrange: 'Susunan Automatik',
|
||||
cancel: 'Batal',
|
||||
change: 'Ubah',
|
||||
change_email: 'Tukar Emel',
|
||||
@@ -46,7 +45,6 @@ const my = {
|
||||
change_password: 'Tukar Kata Laluan',
|
||||
change_username: 'Tukar Nama Pengguna',
|
||||
close: 'Tutup',
|
||||
close_all_windows: 'Tutup Semua Tetingkap',
|
||||
close_all_windows_and_log_out: 'Tutup Tetingkap dan Keluar',
|
||||
change_always_open_with: 'Adakah anda mahu sentiasa membuka jenis fail ini dengan',
|
||||
confirm: 'Sahkan',
|
||||
@@ -106,7 +104,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.',
|
||||
@@ -114,7 +111,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.',
|
||||
@@ -142,7 +138,6 @@ const my = {
|
||||
general: 'Umum',
|
||||
get_a_copy_of_on_puter: 'Dapatkan salinan \'%%\' di Puter.com!',
|
||||
get_copy_link: 'Dapatkan Salinan Pautan',
|
||||
hide_all_windows: 'Sorok Semua Tetingkap',
|
||||
home: 'Laman Utama',
|
||||
html_document: 'Dokumen HTML',
|
||||
hue: 'Hue',
|
||||
@@ -151,7 +146,6 @@ const my = {
|
||||
item: 'item',
|
||||
items_in_trash_cannot_be_renamed: 'Item ini tidak boleh dinamakan semula kerana berada dalam tong sampah. Untuk menamakan semula item ini, tariknya keluar dari Tong Sampah terlebih dahulu.',
|
||||
jpeg_image: 'Imej JPEG',
|
||||
keep_in_taskbar: 'Kekal dalam Bar Tugas',
|
||||
language: 'Bahasa',
|
||||
license: 'Lesen',
|
||||
lightness: 'Kecerahan',
|
||||
@@ -181,10 +175,8 @@ const my = {
|
||||
no: 'Tidak',
|
||||
ok: 'OK',
|
||||
open: 'Buka',
|
||||
new_window: 'Tetingkap Baru',
|
||||
open_in_new_tab: 'Buka dalam Tab Baru',
|
||||
open_in_new_window: 'Buka dalam Tetingkap Baru',
|
||||
open_trash: 'Buka Tong Sampah',
|
||||
open_with: 'Buka Dengan',
|
||||
original_name: 'Nama Asal',
|
||||
original_path: 'Laluan Asal',
|
||||
@@ -201,7 +193,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}}',
|
||||
@@ -223,7 +214,6 @@ const my = {
|
||||
refer_friends_c2a: 'Dapatkan 1 GB untuk setiap rakan yang membuat dan mengesahkan akaun di Puter. Rakan anda juga akan mendapat 1 GB!',
|
||||
refer_friends_social_media_c2a: 'Dapatkan 1 GB storan percuma di Puter.com!',
|
||||
refresh: 'Muat Semula',
|
||||
remove_from_taskbar: 'Keluarkan dari Bar Tugas',
|
||||
rename: 'Namakan Semula',
|
||||
replace: 'Ganti',
|
||||
replace_all: 'Ganti Semua',
|
||||
@@ -251,7 +241,6 @@ const my = {
|
||||
share: 'Kongsi',
|
||||
share_to: 'Kongsi kepada',
|
||||
shortcut_to: 'Pintasan ke',
|
||||
show_all_windows: 'Tunjukkan Semua Tetingkap',
|
||||
show_hidden: 'Tunjukkan yang tersembunyi',
|
||||
sign_in_with_puter: 'Daftar masuk dengan Puter',
|
||||
sign_up: 'Daftar',
|
||||
@@ -423,14 +412,8 @@ const my = {
|
||||
'window_title_refer_friend': 'Rujuk rakan!',
|
||||
|
||||
// Desktop UI
|
||||
'desktop_show_desktop': 'Tunjukkan Desktop',
|
||||
'desktop_show_open_windows': 'Tunjukkan Tetingkap Terbuka',
|
||||
'desktop_exit_full_screen': 'Keluar Skrin Penuh',
|
||||
'desktop_enter_full_screen': 'Masuk Skrin Penuh',
|
||||
'desktop_position': 'Kedudukan',
|
||||
'desktop_position_left': 'Kiri',
|
||||
'desktop_position_bottom': 'Bawah',
|
||||
'desktop_position_right': 'Kanan',
|
||||
|
||||
// Item UI
|
||||
'item_shared_with_you': 'Pengguna telah berkongsi item ini dengan anda.',
|
||||
|
||||
Loaded 100 of 123 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user