feat: Rename to Toju and add translation
Some checks failed
Deploy Web Apps / deploy (push) Successful in 5m52s
Build Android APK / build-android-apk (push) Failing after 23m15s
Queue Release Build / prepare (push) Successful in 1m42s
Queue Release Build / build-linux (push) Failing after 9m33s
Queue Release Build / build-windows (push) Successful in 26m5s
Queue Release Build / finalize (push) Has been skipped

This commit is contained in:
2026-06-05 17:13:03 +02:00
parent 8ecfc9a1fe
commit ee293d7daf
301 changed files with 8247 additions and 2218 deletions

View File

@@ -80,7 +80,7 @@ export function getDocsHtml(specUrl: string): string {
http-equiv="Content-Security-Policy"
content="${contentSecurityPolicy}"
/>
<title>MetoYou Local API</title>
<title>Toju Local API</title>
<style>
:root { color-scheme: light dark; }
body {

View File

@@ -18,10 +18,10 @@ export function buildOpenApiDocument(options: OpenApiBuildOptions): unknown {
return {
openapi: '3.1.0',
info: {
title: 'MetoYou Local Desktop API',
title: 'Toju Local Desktop API',
version: appVersion,
description:
'Authenticated local HTTP API exposed by the MetoYou desktop app. '
'Authenticated local HTTP API exposed by the Toju desktop app. '
+ 'Authentication is performed against a configured signaling server. '
+ 'Bearer tokens issued here are scoped to this device only.'
},

View File

@@ -3,21 +3,14 @@ import AutoLaunch from 'auto-launch';
import * as fsp from 'fs/promises';
import * as path from 'path';
import { readDesktopSettings } from '../desktop-settings';
import { DESKTOP_APP_DISPLAY_NAME, patchLinuxAutostartDesktopEntryNameField } from './desktop-branding.rules';
import { resolveLaunchPath } from './launch-path';
let autoLauncher: AutoLaunch | null = null;
let autoLaunchPath = '';
const LINUX_AUTO_START_ARGUMENTS = ['--no-sandbox', '%U'];
function resolveLaunchPath(): string {
// AppImage runs from a temporary mount; APPIMAGE points to the real file path.
const appImagePath = process.platform === 'linux'
? String(process.env['APPIMAGE'] || '').trim()
: '';
return appImagePath || process.execPath;
}
function escapeDesktopEntryExecArgument(argument: string): string {
const escapedArgument = argument.replace(/(["\\$`])/g, '\\$1');
@@ -35,14 +28,12 @@ function buildLinuxAutoStartExecLine(launchPath: string): string {
}
function buildLinuxAutoStartDesktopEntry(launchPath: string): string {
const appName = path.basename(launchPath);
return [
'[Desktop Entry]',
'Type=Application',
'Version=1.0',
`Name=${appName}`,
`Comment=${appName}startup script`,
`Name=${DESKTOP_APP_DISPLAY_NAME}`,
`Comment=${DESKTOP_APP_DISPLAY_NAME} startup script`,
buildLinuxAutoStartExecLine(launchPath),
'StartupNotify=false',
'Terminal=false'
@@ -65,11 +56,13 @@ async function synchronizeLinuxAutoStartDesktopEntry(launchPath: string): Promis
// Create the desktop entry if auto-launch did not leave one behind.
}
const nextDesktopEntry = currentDesktopEntry
? /^Exec=.*$/m.test(currentDesktopEntry)
? currentDesktopEntry.replace(/^Exec=.*$/m, execLine)
: `${currentDesktopEntry.trimEnd()}\n${execLine}\n`
: buildLinuxAutoStartDesktopEntry(launchPath);
const nextDesktopEntry = patchLinuxAutostartDesktopEntryNameField(
currentDesktopEntry
? /^Exec=.*$/m.test(currentDesktopEntry)
? currentDesktopEntry.replace(/^Exec=.*$/m, execLine)
: `${currentDesktopEntry.trimEnd()}\n${execLine}\n`
: buildLinuxAutoStartDesktopEntry(launchPath)
);
if (nextDesktopEntry === currentDesktopEntry) {
return;
@@ -87,7 +80,7 @@ function getAutoLauncher(): AutoLaunch | null {
if (!autoLauncher) {
autoLaunchPath = resolveLaunchPath();
autoLauncher = new AutoLaunch({
name: app.getName(),
name: DESKTOP_APP_DISPLAY_NAME,
path: autoLaunchPath
});
}

View File

@@ -0,0 +1,88 @@
import { app } from 'electron';
import AutoLaunch from 'auto-launch';
import * as fsp from 'fs/promises';
import * as path from 'path';
import {
DESKTOP_APP_DISPLAY_NAME,
isLegacyLinuxAutostartEntry,
LEGACY_APP_REGISTRY_NAMES
} from './desktop-branding.rules';
import { resolveLaunchPath } from './launch-path';
function getLinuxAutoStartDirectory(): string {
return path.join(app.getPath('home'), '.config', 'autostart');
}
export function configureDesktopBranding(): void {
if (!app.isPackaged) {
return;
}
app.setName(DESKTOP_APP_DISPLAY_NAME);
if (process.platform !== 'darwin') {
process.title = DESKTOP_APP_DISPLAY_NAME;
}
}
async function removeLegacyLinuxAutostartEntries(launchPath: string): Promise<void> {
if (process.platform !== 'linux') {
return;
}
const autostartDirectory = getLinuxAutoStartDirectory();
const currentLaunchBaseName = path.basename(launchPath);
let fileNames: string[] = [];
try {
fileNames = await fsp.readdir(autostartDirectory);
} catch {
return;
}
await Promise.all(fileNames.map(async (fileName) => {
if (!fileName.endsWith('.desktop')) {
return;
}
if (!isLegacyLinuxAutostartEntry(fileName, currentLaunchBaseName)) {
return;
}
await fsp.unlink(path.join(autostartDirectory, fileName)).catch(() => {});
}));
}
async function disableLegacyWindowsAutoLaunchEntries(launchPath: string): Promise<void> {
if (process.platform !== 'win32') {
return;
}
await Promise.all(LEGACY_APP_REGISTRY_NAMES.map(async (legacyName) => {
const launcher = new AutoLaunch({
name: legacyName,
path: launchPath
});
try {
if (await launcher.isEnabled()) {
await launcher.disable();
}
} catch {
// Best-effort cleanup for renamed desktop binaries.
}
}));
}
export async function migrateLegacyDesktopBranding(): Promise<void> {
if (!app.isPackaged) {
return;
}
const launchPath = resolveLaunchPath();
await removeLegacyLinuxAutostartEntries(launchPath);
await disableLegacyWindowsAutoLaunchEntries(launchPath);
}

View File

@@ -0,0 +1,45 @@
import {
describe,
expect,
it
} from 'vitest';
import {
DESKTOP_APP_DISPLAY_NAME,
DESKTOP_EXECUTABLE_NAME,
LEGACY_APP_REGISTRY_NAMES,
isLegacyLinuxAutostartEntry,
patchLinuxAutostartDesktopEntryNameField
} from './desktop-branding.rules';
describe('desktop-branding.rules', () => {
it('exposes the Toju desktop branding constants', () => {
expect(DESKTOP_APP_DISPLAY_NAME).toBe('Toju');
expect(DESKTOP_EXECUTABLE_NAME).toBe('toju');
expect(LEGACY_APP_REGISTRY_NAMES).toContain('MetoYou');
expect(LEGACY_APP_REGISTRY_NAMES).toContain('metoyou');
});
it('treats legacy linux autostart entries as removable', () => {
expect(isLegacyLinuxAutostartEntry('metoyou.desktop', 'toju')).toBe(true);
expect(isLegacyLinuxAutostartEntry('MetoYou.desktop', 'toju')).toBe(true);
expect(isLegacyLinuxAutostartEntry('MetoYou-1.0.0-x86_64.AppImage.desktop', 'Toju-1.1.0-x86_64.AppImage')).toBe(true);
});
it('keeps the current launch entry when it already uses the Toju binary', () => {
expect(isLegacyLinuxAutostartEntry('toju.desktop', 'toju')).toBe(false);
expect(isLegacyLinuxAutostartEntry('Toju-1.1.0-x86_64.AppImage.desktop', 'Toju-1.1.0-x86_64.AppImage')).toBe(false);
});
it('rewrites the desktop entry display name to Toju', () => {
const patched = patchLinuxAutostartDesktopEntryNameField([
'[Desktop Entry]',
'Type=Application',
'Name=metoyou',
'Exec=/opt/Toju/toju --no-sandbox %U'
].join('\n'));
expect(patched).toContain('Name=Toju');
expect(patched).not.toContain('Name=metoyou');
});
});

View File

@@ -0,0 +1,43 @@
export const DESKTOP_APP_DISPLAY_NAME = 'Toju';
export const DESKTOP_EXECUTABLE_NAME = 'toju';
export const LEGACY_APP_REGISTRY_NAMES = [
'MetoYou',
'MeToYou',
'metoyou'
] as const;
function normalizeAutostartBaseName(fileName: string): string {
return fileName.replace(/\.desktop$/iu, '').replace(/\.exe$/iu, '');
}
export function isLegacyLinuxAutostartEntry(
fileName: string,
currentLaunchBaseName: string
): boolean {
const entryBaseName = normalizeAutostartBaseName(fileName);
const currentBaseName = normalizeAutostartBaseName(currentLaunchBaseName);
if (entryBaseName === currentBaseName) {
return false;
}
const normalizedEntry = entryBaseName.toLowerCase();
if (LEGACY_APP_REGISTRY_NAMES.some((legacyName) => normalizedEntry === legacyName.toLowerCase())) {
return true;
}
return /^metoyou[-.]/iu.test(entryBaseName);
}
export function patchLinuxAutostartDesktopEntryNameField(
desktopEntry: string,
displayName: string = DESKTOP_APP_DISPLAY_NAME
): string {
if (/^Name=.*$/m.test(desktopEntry)) {
return desktopEntry.replace(/^Name=.*$/m, `Name=${displayName}`);
}
return desktopEntry;
}

View File

@@ -1,7 +1,9 @@
import { app } from 'electron';
import { configureDesktopBranding } from './desktop-branding-migration';
import { readDesktopSettings } from '../desktop-settings';
export function configureAppFlags(): void {
configureDesktopBranding();
linuxSpecificFlags();
networkFlags();
setupGpuEncodingFlags();

View File

@@ -0,0 +1,8 @@
/** Resolves the packaged binary path used for auto-start and updater migration. */
export function resolveLaunchPath(): string {
const appImagePath = process.platform === 'linux'
? String(process.env['APPIMAGE'] || '').trim()
: '';
return appImagePath || process.execPath;
}

View File

@@ -2,6 +2,7 @@ import { app, BrowserWindow } from 'electron';
import { cleanupLinuxScreenShareAudioRouting } from '../audio/linux-screen-share-routing';
import { initializeDesktopUpdater, shutdownDesktopUpdater } from '../update/desktop-updater';
import { synchronizeAutoStartSetting } from './auto-start';
import { migrateLegacyDesktopBranding } from './desktop-branding-migration';
import { applyLocalApiSettings, stopLocalApiServer } from '../api';
import {
initializeDatabase,
@@ -41,6 +42,7 @@ export function registerAppLifecycle(): void {
setupCqrsHandlers();
setupWindowControlHandlers();
setupSystemHandlers();
await migrateLegacyDesktopBranding();
await synchronizeAutoStartSetting();
initializeDesktopUpdater();
await createWindow();

View File

@@ -47,8 +47,8 @@ export async function exportUserData(): Promise<ExportUserDataResult> {
.slice(0, 10)}.dat`;
const { canceled, filePath } = await dialog.showSaveDialog({
defaultPath: path.join(app.getPath('documents'), defaultFileName),
filters: [{ extensions: ['dat'], name: 'MetoYou data archive' }],
title: 'Export MetoYou data'
filters: [{ extensions: ['dat'], name: 'Toju data archive' }],
title: 'Export Toju data'
});
if (canceled || !filePath) {
@@ -87,9 +87,9 @@ export async function exportUserData(): Promise<ExportUserDataResult> {
export async function importUserData(): Promise<ImportUserDataResult> {
const { canceled, filePaths } = await dialog.showOpenDialog({
filters: [{ extensions: ['dat', 'zip'], name: 'MetoYou data archive' }],
filters: [{ extensions: ['dat', 'zip'], name: 'Toju data archive' }],
properties: ['openFile'],
title: 'Import MetoYou data'
title: 'Import Toju data'
});
if (canceled || filePaths.length === 0) {
@@ -235,7 +235,7 @@ function validateArchiveManifest(entries: ZipArchiveEntry[]): void {
const manifest = entries.find((entry) => entry.path === ARCHIVE_MANIFEST_PATH);
if (!manifest) {
throw new Error('The selected file is missing a MetoYou data manifest.');
throw new Error('The selected file is missing a Toju data manifest.');
}
const parsed = JSON.parse(manifest.data.toString('utf8')) as { format?: string; version?: number };

View File

@@ -105,6 +105,7 @@ export const HARDCODED_IGNORED_PROCESSES: ReadonlySet<string> = new Set([
'logitechg',
'login',
'metoyou',
'toju',
'msedge',
'msedgewebview2',
'msteams',

View File

@@ -1,5 +1,6 @@
import { app, net } from 'electron';
import { autoUpdater } from 'electron-updater';
import { migrateLegacyDesktopBranding } from '../app/desktop-branding-migration';
import { readDesktopSettings, type AutoUpdateMode } from '../desktop-settings';
import { getMainWindow } from '../window/create-window';
import {
@@ -500,7 +501,7 @@ async function performUpdateCheck(
setDesktopUpdateState({
lastCheckedAt: Date.now(),
status: 'checking',
statusMessage: `Checking for MetoYou ${targetRelease.version}...`,
statusMessage: `Checking for Toju ${targetRelease.version}...`,
targetVersion: targetRelease.version
});
@@ -641,7 +642,7 @@ async function refreshDesktopUpdater(
setDesktopUpdateState({
status: 'target-older-than-installed',
statusMessage: `MetoYou ${app.getVersion()} is newer than ${selectedRelease.version}. Downgrades are not applied automatically.`,
statusMessage: `Toju ${app.getVersion()} is newer than ${selectedRelease.version}. Downgrades are not applied automatically.`,
targetVersion: selectedRelease.version
});
@@ -698,7 +699,7 @@ export function initializeDesktopUpdater(): void {
setDesktopUpdateState({
lastCheckedAt: Date.now(),
status: 'downloading',
statusMessage: `Downloading MetoYou ${nextVersion ?? 'update'}...`,
statusMessage: `Downloading Toju ${nextVersion ?? 'update'}...`,
targetVersion: nextVersion
});
});
@@ -715,8 +716,8 @@ export function initializeDesktopUpdater(): void {
lastCheckedAt: Date.now(),
status: 'up-to-date',
statusMessage: isPinnedVersion
? `MetoYou ${desktopUpdateState.targetVersion} is already installed.`
: 'MetoYou is up to date.'
? `Toju ${desktopUpdateState.targetVersion} is already installed.`
: 'Toju is up to date.'
});
});
@@ -726,11 +727,13 @@ export function initializeDesktopUpdater(): void {
const nextVersion = normalizeSemanticVersion(updateInfo.version)
?? desktopUpdateState.targetVersion;
void migrateLegacyDesktopBranding();
setDesktopUpdateState({
lastCheckedAt: Date.now(),
restartRequired: true,
status: 'restart-required',
statusMessage: `MetoYou ${nextVersion ?? 'update'} is ready. Restart the app to finish installing it.`,
statusMessage: `Toju ${nextVersion ?? 'update'} is ready. Restart the app to finish installing it.`,
targetVersion: nextVersion
});
});

View File

@@ -9,6 +9,7 @@ import {
} from 'electron';
import * as fs from 'fs';
import * as path from 'path';
import { DESKTOP_APP_DISPLAY_NAME } from '../app/desktop-branding.rules';
import { readDesktopSettings } from '../desktop-settings';
let mainWindow: BrowserWindow | null = null;
@@ -114,11 +115,11 @@ function ensureTray(): void {
}
tray = new Tray(trayIconPath);
tray.setToolTip('MetoYou');
tray.setToolTip('Toju');
tray.setContextMenu(
Menu.buildFromTemplate([
{
label: 'Open MetoYou',
label: 'Open Toju',
click: () => {
void showMainWindow();
}
@@ -127,7 +128,7 @@ function ensureTray(): void {
type: 'separator'
},
{
label: 'Close MetoYou',
label: 'Close Toju',
click: () => {
requestAppQuit();
}
@@ -200,6 +201,7 @@ export async function createWindow(): Promise<void> {
minWidth: 800,
minHeight: 600,
frame: false,
title: DESKTOP_APP_DISPLAY_NAME,
titleBarStyle: 'hidden',
backgroundColor: '#0a0a0f',
...(windowIconPath ? { icon: windowIconPath } : {}),