Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 21 additions & 18 deletions app/capture.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const {
const { physicalToDip, physicalRectToDip, displayForDipPoint, cropGeometry } = require('./coords');
const { WINDOW_BOUNDS_CS, parseWindowBounds, readActiveWindow } = require('./platform/windows/window-bounds');
const { defaultCaptureTitle, mouseButtonKey } = require('../core/text-intel');
const { t, tn, uiLanguage } = require('../core/ui-text');

/** True when two presses (DIP points) are close enough to be one double-click. */
function withinDoubleClickRadius(a, b) {
Expand Down Expand Up @@ -71,10 +72,10 @@ const MIN_DOUBLE_CLICK_GAP_MS = 20;
// something else that the debounce happened to drop.
const DOUBLE_CLICK_RADIUS = 8;
// Shown before and during a recording whose Window setting can't be honoured.
const WINDOW_FALLBACK_NOTE = 'Window capture is not available on this desktop, so StepForge records the full screen.';
const windowFallbackNote = () => t('Window capture is not available on this desktop, so StepForge records the full screen.');
// Window mode on Windows, when a capture has no window to show (a click on
// the desktop or taskbar, or the active window could not be read).
const WINDOW_NOT_FOUND_NOTE = 'StepForge couldn’t tell which window to capture, so it captured the full screen.';
const windowNotFoundNote = () => t('StepForge couldn’t tell which window to capture, so it captured the full screen.');
// How long a Linux raw button event waits for its regular twin (the
// representation that carries root coordinates) before firing without them.
const LINUX_CLICK_TWIN_MS = 25;
Expand Down Expand Up @@ -333,7 +334,7 @@ class CaptureService {

state() {
// Said up front, so the recording bar shows it before anything is captured.
const modeNote = this.windowModeFallsBack() ? WINDOW_FALLBACK_NOTE : '';
const modeNote = this.windowModeFallsBack() ? windowFallbackNote() : '';
return this.session
? {
active: true,
Expand Down Expand Up @@ -487,19 +488,19 @@ class CaptureService {
const img = raster.createImage(16, 16, [0, 0, 0, 0]);
raster.fillOval(img, 2, 2, 12, 12, [229, 72, 77, 255]);
this.tray = new Tray(nativeImage.createFromBuffer(encodePng(img)));
this.tray.setToolTip('StepForge — capture session running');
this.tray.setToolTip(t('StepForge — capture session running'));
const rebuild = () => {
if (!this.tray || this.tray.isDestroyed()) return;
this.tray.setContextMenu(Menu.buildFromTemplate([
{ label: `Captured ${this.session ? this.session.count : 0} steps`, enabled: false },
{ label: tn(this.session ? this.session.count : 0, 'Captured {count} step', 'Captured {count} steps'), enabled: false },
{ type: 'separator' },
{ label: 'Capture now', click: () => this.sessionCapture('manual').then(rebuild).catch(() => {}) },
{ label: t('Capture now'), click: () => this.sessionCapture('manual').then(rebuild).catch(() => {}) },
{
label: this.session && this.session.paused ? 'Resume capturing' : 'Pause capturing',
label: this.session && this.session.paused ? t('Resume capturing') : t('Pause capturing'),
click: () => { this.noteUiStopGesture(); this.togglePause(); rebuild(); },
},
{
label: 'Open StepForge (pauses capture)',
label: t('Open StepForge (pauses capture)'),
click: () => {
this.noteUiStopGesture();
this.togglePause(true);
Expand All @@ -508,7 +509,7 @@ class CaptureService {
},
},
{ type: 'separator' },
{ label: 'Finish session', click: () => { this.noteUiStopGesture(); this.finishSession(); } },
{ label: t('Finish session'), click: () => { this.noteUiStopGesture(); this.finishSession(); } },
]));
};
rebuild();
Expand Down Expand Up @@ -800,7 +801,7 @@ class CaptureService {
this.notify('capture:diagnostic', {
kind: 'strict-click-skipped',
guideId,
reason: 'No pre-click frame was ready for this click. In strict timing mode StepForge skips the shot rather than capturing the screen after the click.',
reason: t('No pre-click frame was ready for this click. In strict timing mode StepForge skips the shot rather than capturing the screen after the click.'),
});
return { ok: false, reason: 'strict mode: no pre-click frame available for this click' };
}
Expand Down Expand Up @@ -853,7 +854,7 @@ class CaptureService {
return { ok: false, reason: 'waiting for the screen-share stream' };
}

if (this.shooting) return { ok: false, reason: 'capture already in progress' };
if (this.shooting) return { ok: false, reason: t('capture already in progress') };
this.shooting = true;
try {
const mode = this.settings.get('capture.mode') || 'fullscreen';
Expand Down Expand Up @@ -1005,7 +1006,7 @@ class CaptureService {
this.notify('capture:diagnostic', {
kind: 'window-mode-unavailable',
guideId: this.session.guideId,
reason: WINDOW_FALLBACK_NOTE,
reason: windowFallbackNote(),
});
}
grabMode = 'fullscreen';
Expand Down Expand Up @@ -2319,7 +2320,7 @@ ${WINDOW_BOUNDS_CS}
this.notify('capture:diagnostic', {
kind: 'window-not-found',
guideId: this.session ? this.session.guideId : null,
reason: WINDOW_NOT_FOUND_NOTE,
reason: windowNotFoundNote(),
});
}

Expand Down Expand Up @@ -2563,11 +2564,11 @@ ${WINDOW_BOUNDS_CS}
}
const { image, display } = grabbed;
const rect = await this.pickRegion(display, image);
if (!rect) return { ok: false, reason: 'selection cancelled' };
if (!rect) return { ok: false, reason: t('selection cancelled') };

const cropped = image.crop(rect);
const size = cropped.getSize();
if (!size.width || !size.height) return { ok: false, reason: 'empty selection' };
if (!size.width || !size.height) return { ok: false, reason: t('empty selection') };
return { ok: true, frame: { image: cropped, size, display, cursor: null } };
}

Expand All @@ -2578,8 +2579,8 @@ ${WINDOW_BOUNDS_CS}
* is running, since it owns the window, the hotkey and the click capture.
*/
async retakeFrame({ mode = 'fullscreen', delayMs = 3000 } = {}) {
if (this.session) return { ok: false, reason: 'Stop the recording session before retaking a screenshot.' };
if (this.shooting) return { ok: false, reason: 'capture already in progress' };
if (this.session) return { ok: false, reason: t('Stop the recording session before retaking a screenshot.') };
if (this.shooting) return { ok: false, reason: t('capture already in progress') };
this.shooting = true;
try {
let frame;
Expand Down Expand Up @@ -2684,7 +2685,9 @@ ${WINDOW_BOUNDS_CS}
ipcMain.on('region:picked', onPick);
overlay.on('closed', () => finish(null));
overlay.webContents.on('did-fail-load', () => finish(null));
overlay.loadFile(path.join(__dirname, 'renderer', 'region.html')).catch(() => finish(null));
overlay.loadFile(path.join(__dirname, 'renderer', 'region.html'), {
query: { hint: t('Drag to select a region — Esc to cancel'), title: t('Select region'), lang: uiLanguage() },
}).catch(() => finish(null));
});
}
}
Expand Down
38 changes: 22 additions & 16 deletions app/cloud-account.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const path = require('node:path');
const { atomicWriteFileSync } = require('../core/util');
const { progressBody } = require('../core/transfer-meter');
const { FolderStorage } = require('../core/folder-storage');
const { t } = require('../core/ui-text');

/*
* What OneDrive, Dropbox and Nextcloud/WebDAV sync have in common: sign-in
Expand Down Expand Up @@ -54,7 +55,7 @@ class CloudAccount {
this.credentials = stored;
this.loadError = null;
} else {
this.loadError = 'Please sign in again to connect this version of StepForge.';
this.loadError = t('Please sign in again to connect this version of StepForge.');
}
} catch {
this.loadError = `Stored ${this.label} sign-in could not be unlocked. Disconnect and sign in again.`;
Expand Down Expand Up @@ -199,7 +200,7 @@ class CloudAccount {
if (response.body) {
for await (const chunk of response.body) {
length += chunk.length;
if (length > MAX_TRANSFER_BYTES) { controller.abort(); throw new Error('Cloud archive exceeds the 256 MB transfer limit.'); }
if (length > MAX_TRANSFER_BYTES) { controller.abort(); throw new Error(t('Cloud archive exceeds the 256 MB transfer limit.')); }
chunks.push(Buffer.from(chunk));
onProgress?.(length);
}
Expand All @@ -221,7 +222,7 @@ class CloudAccount {

async authorized(url, options = {}) {
const generation = this.generation;
if (!this.credentials) throw new Error(this.loadError || `Sign in to ${this.label} first.`);
if (!this.credentials) throw new Error(this.loadError || t('Sign in to {service} first.', { service: this.label }));
const headers = await this.authHeaders();
if (generation !== this.generation) throw new Error(`${this.label} request cancelled.`);
try {
Expand Down Expand Up @@ -259,7 +260,7 @@ class CloudAccount {
this.onCallback = (req, res) => {
const url = new URL(req.url, 'http://localhost');
if (req.method !== 'GET' || url.pathname !== '/' || url.searchParams.get('state') !== state) {
res.writeHead(400).end('Invalid sign-in callback.');
res.writeHead(400).end(t('Invalid sign-in callback.'));
return;
}
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
Expand All @@ -268,7 +269,7 @@ class CloudAccount {
res.end(`${this.label} sign-in was not completed. Return to StepForge.`);
reject(new Error(`${this.label} sign-in was denied or cancelled.`));
} else {
res.end(`Signed in to ${this.label}. You can close this tab and go back to StepForge.`);
res.end(t('Signed in to {service}. You can close this tab and go back to StepForge.', { service: this.label }));
resolve(url.searchParams.get('code'));
}
};
Expand Down Expand Up @@ -320,7 +321,7 @@ class CloudAccount {
/** A current OAuth access token, refreshed when it's about to expire. */
async accessToken(force = false) {
const credentials = this.credentials;
if (!credentials?.refresh_token) throw new Error(this.loadError || `Sign in to ${this.label} first.`);
if (!credentials?.refresh_token) throw new Error(this.loadError || t('Sign in to {service} first.', { service: this.label }));
if (!force && credentials.access_token && credentials.expiresAt > Date.now() + 60000) return credentials.access_token;
this.refreshing ||= (async () => {
let response;
Expand Down Expand Up @@ -358,37 +359,42 @@ class CloudAccount {
quota() { return this.storage.quota(); }

async account() {
if (!this.credentials) throw new Error(this.loadError || `Sign in to ${this.label} first.`);
if (!this.credentials) throw new Error(this.loadError || t('Sign in to {service} first.', { service: this.label }));
if (!this.credentials.accountId) await this.loadProfile();
return this.credentials.accountId;
}

/**
* Sign in, list, upload, download and delete a test file. Each check is
* { text, ok }: its text is in the interface language, so pass or fail is
* never read from it.
*/
async test() {
const checks = [];
let probe;
try {
await this.authHeaders(true);
checks.push('Sign-in: passed');
checks.push({ text: t('Sign-in: passed'), ok: true });
await this.storage.objects();
checks.push(`${this.label} storage access: passed`);
checks.push({ text: t('{service} storage access: passed', { service: this.label }), ok: true });
const bytes = crypto.randomBytes(8 * 1024);
probe = await this.upload({ data: bytes, name: 'StepForge connection test', properties: { stepforge: 'connection-test' } });
checks.push('Test upload: passed');
checks.push({ text: t('Test upload: passed'), ok: true });
const downloaded = await this.download(probe.id);
if (!bytes.equals(downloaded)) throw new Error('The test download did not match the uploaded data.');
checks.push('Test download and integrity: passed');
if (!bytes.equals(downloaded)) throw new Error(t('The test download did not match the uploaded data.'));
checks.push({ text: t('Test download and integrity: passed'), ok: true });
} catch (err) {
checks.push(`Failed: ${err.message}`);
checks.push({ text: t('Failed: {message}', { message: err.message }), ok: false });
return { ok: false, checks };
} finally {
if (probe) {
try {
await this.deleteFile(probe.id);
checks.push('Test file cleanup: passed');
} catch { checks.push('Test file cleanup failed; a small test file remains in StepForge’s folder.'); }
checks.push({ text: t('Test file cleanup: passed'), ok: true });
} catch { checks.push({ text: t('Test file cleanup failed; a small test file remains in StepForge’s folder.'), ok: false }); }
}
}
return { ok: checks.every((line) => !line.includes('failed')), checks };
return { ok: checks.every((check) => check.ok), checks };
}
}

Expand Down
3 changes: 2 additions & 1 deletion app/confluence-session.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

const crypto = require('node:crypto');
const { t } = require('../core/ui-text');

/*
* The Electron side of Confluence (app/confluence.js): a browser session of
Expand Down Expand Up @@ -39,7 +40,7 @@ function createConfluenceSession({ session, BrowserWindow, getParent = () => nul
// It never gets StepForge's preload or IPC.
const makeWindow = (show) => {
const win = new BrowserWindow({
show, width: 1000, height: 760, title: 'Sign in to Confluence', autoHideMenuBar: true,
show, width: 1000, height: 760, title: t('Sign in to Confluence'), autoHideMenuBar: true,
parent: show ? getParent() || undefined : undefined,
webPreferences: { session: ses, sandbox: true, contextIsolation: true, nodeIntegration: false },
});
Expand Down
Loading
Loading