feat: i18n control, Gitea auto-update CI, license-gated updater, fixes
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import { installIpcRouter, registerHandler, setLicenseAssert } from './ipc/route
|
||||
import { LicenseService } from './license/licenseService';
|
||||
import { ZipProjectStore } from './project/zipStore';
|
||||
import { registerDndAssetProtocol } from './protocol/dndAssetProtocol';
|
||||
import { installAutoUpdater } from './update/installAutoUpdater';
|
||||
import { getAppSemanticVersion, getOptionalBuildNumber } from './versionInfo';
|
||||
import { VideoPlaybackStore } from './video/videoPlaybackStore';
|
||||
import {
|
||||
@@ -527,6 +528,7 @@ async function main() {
|
||||
|
||||
installIpcRouter();
|
||||
applyDockIconIfNeeded();
|
||||
installAutoUpdater(licenseService);
|
||||
await runStartupAfterHandlers(licenseService);
|
||||
|
||||
app.on('activate', () => {
|
||||
|
||||
@@ -17,6 +17,28 @@ type Preferences = {
|
||||
eulaAcceptedVersion?: number;
|
||||
};
|
||||
|
||||
type LicenseChangeListener = () => void;
|
||||
|
||||
const licenseChangeListeners = new Set<LicenseChangeListener>();
|
||||
|
||||
/** Слушатели вызываются после смены состояния лицензии (сохранённый токен, EULA, отзыв). */
|
||||
export function addLicenseChangeListener(fn: LicenseChangeListener): () => void {
|
||||
licenseChangeListeners.add(fn);
|
||||
return () => {
|
||||
licenseChangeListeners.delete(fn);
|
||||
};
|
||||
}
|
||||
|
||||
function notifyLicenseChangeListeners(): void {
|
||||
for (const fn of licenseChangeListeners) {
|
||||
try {
|
||||
fn();
|
||||
} catch (err) {
|
||||
console.error('[license] change listener failed', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function readPreferences(userData: string): Preferences {
|
||||
try {
|
||||
const raw = fs.readFileSync(preferencesPath(userData), 'utf8');
|
||||
@@ -35,6 +57,7 @@ function emitLicenseStatusChanged(): void {
|
||||
for (const win of BrowserWindow.getAllWindows()) {
|
||||
win.webContents.send(ipcChannels.license.statusChanged, {});
|
||||
}
|
||||
notifyLicenseChangeListeners();
|
||||
}
|
||||
|
||||
export class LicenseService {
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { app, dialog } from 'electron';
|
||||
import { autoUpdater } from 'electron-updater';
|
||||
|
||||
import { addLicenseChangeListener } from '../license/licenseService';
|
||||
import type { LicenseService } from '../license/licenseService';
|
||||
|
||||
const STARTUP_CHECK_DELAY_MS = 12_000;
|
||||
/** Не дёргать сервер чаще (смена лицензии / повторные emit). */
|
||||
const RE_CHECK_COOLDOWN_MS = 30_000;
|
||||
|
||||
let lastCheckAt = 0;
|
||||
|
||||
function isLicensedForUpdates(licenseService: LicenseService): boolean {
|
||||
const snap = licenseService.getStatusSync();
|
||||
return snap.active;
|
||||
}
|
||||
|
||||
function maybeCheckForUpdates(licenseService: LicenseService, ignoreCooldown: boolean): void {
|
||||
if (!app.isPackaged) return;
|
||||
if (!isLicensedForUpdates(licenseService)) return;
|
||||
const now = Date.now();
|
||||
if (!ignoreCooldown && now - lastCheckAt < RE_CHECK_COOLDOWN_MS) return;
|
||||
lastCheckAt = now;
|
||||
void autoUpdater.checkForUpdates().catch(() => undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка обновлений: только упакованное приложение, только при активной лицензии.
|
||||
* Канал и URL задаются при сборке (`publish` → `app-update.yml` внутри установки).
|
||||
*/
|
||||
export function installAutoUpdater(licenseService: LicenseService): void {
|
||||
if (!app.isPackaged) return;
|
||||
|
||||
const feedOverride = process.env.DND_UPDATE_FEED_URL?.trim();
|
||||
if (feedOverride) {
|
||||
const url = feedOverride.endsWith('/') ? feedOverride : `${feedOverride}/`;
|
||||
autoUpdater.setFeedURL({ provider: 'generic', url });
|
||||
}
|
||||
|
||||
autoUpdater.autoDownload = true;
|
||||
autoUpdater.autoInstallOnAppQuit = true;
|
||||
|
||||
autoUpdater.on('update-downloaded', (info) => {
|
||||
void dialog
|
||||
.showMessageBox({
|
||||
type: 'info',
|
||||
title: 'DNDGamePlayer',
|
||||
message: `Доступна новая версия ${info.version}. Установить и перезапустить?`,
|
||||
buttons: ['Перезапустить сейчас', 'Позже'],
|
||||
defaultId: 0,
|
||||
cancelId: 1,
|
||||
})
|
||||
.then((r) => {
|
||||
if (r.response === 0) {
|
||||
autoUpdater.quitAndInstall(false, true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
autoUpdater.on('error', () => {
|
||||
/* без console: в production main минифицируется с drop console */
|
||||
});
|
||||
|
||||
addLicenseChangeListener(() => {
|
||||
maybeCheckForUpdates(licenseService, false);
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
maybeCheckForUpdates(licenseService, true);
|
||||
}, STARTUP_CHECK_DELAY_MS);
|
||||
}
|
||||
@@ -13,6 +13,19 @@ const windows = new Map<WindowKind, BrowserWindow>();
|
||||
|
||||
let appQuitting = false;
|
||||
|
||||
/** Учитываем окна, которые уже уничтожены при каскадном закрытии (родитель → дочернее). */
|
||||
function broadcastMultiWindowStateChanged(open: boolean): void {
|
||||
for (const w of BrowserWindow.getAllWindows()) {
|
||||
if (w.isDestroyed()) continue;
|
||||
if (w.webContents.isDestroyed()) continue;
|
||||
try {
|
||||
w.webContents.send(ipcChannels.windows.multiWindowStateChanged, { open });
|
||||
} catch {
|
||||
/* окно могло закрыться между проверкой и send */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Разрешает реальное закрытие окна редактора (выход из приложения). */
|
||||
export function markAppQuitting(): void {
|
||||
appQuitting = true;
|
||||
@@ -212,9 +225,7 @@ function createWindow(kind: WindowKind, opts?: CreateWindowOpts): BrowserWindow
|
||||
win.on('closed', () => {
|
||||
if (kind !== 'presentation' && kind !== 'control') return;
|
||||
const open = windows.has('presentation') || windows.has('control');
|
||||
for (const w of BrowserWindow.getAllWindows()) {
|
||||
w.webContents.send(ipcChannels.windows.multiWindowStateChanged, { open });
|
||||
}
|
||||
broadcastMultiWindowStateChanged(open);
|
||||
});
|
||||
windows.set(kind, win);
|
||||
return win;
|
||||
@@ -292,10 +303,7 @@ export function openMultiWindow() {
|
||||
// Keep control window independent on darwin.
|
||||
createWindow('control', process.platform === 'darwin' ? undefined : { parent: presentation });
|
||||
}
|
||||
const open = true;
|
||||
for (const w of BrowserWindow.getAllWindows()) {
|
||||
w.webContents.send(ipcChannels.windows.multiWindowStateChanged, { open });
|
||||
}
|
||||
broadcastMultiWindowStateChanged(true);
|
||||
}
|
||||
|
||||
export function closeMultiWindow(): void {
|
||||
|
||||
Reference in New Issue
Block a user