feat: кубики/таймер/кисть/спонсоры и задел игровых систем
Показ анимации кубиков игрокам, закрытие оверлеев с окнами, ручной ввод количества, меньший радиус кисти, блок спонсоров в «О программе»; плюс черновик game systems / character sheet. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+89
-1
@@ -1,13 +1,14 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
import { app, BrowserWindow, dialog, Menu, protocol } from 'electron';
|
||||
import { app, BrowserWindow, dialog, Menu, protocol, shell } from 'electron';
|
||||
|
||||
import { installStdoutEpipeGuards } from './safeConsole';
|
||||
|
||||
installStdoutEpipeGuards();
|
||||
|
||||
import { openDialogFilterLabel } from '../shared/appBranding';
|
||||
import type { SystemDefinition } from '../shared/characterSheet/types';
|
||||
import { USERS_BRANCH_FEATURES_ENABLED } from '../shared/features/usersBranchFeatures';
|
||||
import { ipcChannels, type ScenePreviewImportEvent, type SessionState } from '../shared/ipc/contracts';
|
||||
import {
|
||||
@@ -43,6 +44,7 @@ import { ZipProjectStore } from './project/zipStore';
|
||||
import { SceneViewStore } from './sceneView/sceneViewStore';
|
||||
import { registerDndAssetProtocol } from './protocol/dndAssetProtocol';
|
||||
import { PlayersStore } from './players/playersStore';
|
||||
import { GameSystemsStore } from './gameSystems/gameSystemsStore';
|
||||
import { PlaylistsStore } from './playlists/playlistsStore';
|
||||
import { SceneNpcTokensSessionStore } from './players/sceneNpcTokensSessionStore';
|
||||
import { ScenePlayerTokensSessionStore } from './players/scenePlayerTokensSessionStore';
|
||||
@@ -91,6 +93,8 @@ import {
|
||||
closeTokenPathEditorWindow,
|
||||
closeNpcsWindow,
|
||||
sendToAppWindows,
|
||||
setDiceWindowClosedListener,
|
||||
setTimerWindowClosedListener,
|
||||
syncAllWindowChromeTitles,
|
||||
togglePresentationFullscreen,
|
||||
waitForEditorWindowReady,
|
||||
@@ -199,6 +203,7 @@ const scenePlayerTokensSessionStore = new ScenePlayerTokensSessionStore();
|
||||
const tokenGridSnapSessionStore = new TokenGridSnapSessionStore();
|
||||
let tokensStore: TokensStore | null = null;
|
||||
let playersStore: PlayersStore | null = null;
|
||||
let gameSystemsStore: GameSystemsStore | null = null;
|
||||
let playlistsStore: PlaylistsStore | null = null;
|
||||
|
||||
function emitTokenPathSessionState(): void {
|
||||
@@ -293,6 +298,22 @@ function emitSessionTimerState(): void {
|
||||
}
|
||||
}
|
||||
|
||||
function dismissSessionTimerWhenWindowCloses(): void {
|
||||
const st = timerSessionStore.getState();
|
||||
let changed = false;
|
||||
if (st.running) {
|
||||
timerSessionStore.dispatch({ kind: 'pause' });
|
||||
changed = true;
|
||||
}
|
||||
if (st.visibleToPlayers) {
|
||||
timerSessionStore.dispatch({ kind: 'setVisibleToPlayers', visible: false });
|
||||
changed = true;
|
||||
}
|
||||
if (changed) emitSessionTimerState();
|
||||
}
|
||||
|
||||
setTimerWindowClosedListener(dismissSessionTimerWhenWindowCloses);
|
||||
|
||||
function emitInitiativeState(): void {
|
||||
const state = initiativeSessionStore.getState();
|
||||
for (const win of BrowserWindow.getAllWindows()) {
|
||||
@@ -317,6 +338,15 @@ function emitDiceRollState(): void {
|
||||
}
|
||||
}
|
||||
|
||||
function hideDiceOverlayWhenWindowCloses(): void {
|
||||
const st = diceRollSessionStore.getState();
|
||||
if (!st.visibleToPlayers || !st.active) return;
|
||||
diceRollSessionStore.dispatch({ kind: 'hideOverlay' });
|
||||
emitDiceRollState();
|
||||
}
|
||||
|
||||
setDiceWindowClosedListener(hideDiceOverlayWhenWindowCloses);
|
||||
|
||||
function syncInitiativePresenceFromSession(): void {
|
||||
const project = projectStore.getOpenProject();
|
||||
const sceneId = project?.currentSceneId ?? null;
|
||||
@@ -633,6 +663,8 @@ async function main() {
|
||||
playersStore = new PlayersStore(app.getPath('userData'));
|
||||
await playersStore.ensureLoaded();
|
||||
}
|
||||
gameSystemsStore = new GameSystemsStore(app.getPath('userData'));
|
||||
await gameSystemsStore.ensureLoaded();
|
||||
playlistsStore = new PlaylistsStore(app.getPath('userData'));
|
||||
await playlistsStore.ensureLoaded();
|
||||
setLicenseAssert(() => {
|
||||
@@ -650,6 +682,18 @@ async function main() {
|
||||
buildNumber: getOptionalBuildNumber(),
|
||||
packaged: app.isPackaged,
|
||||
}));
|
||||
registerHandler(ipcChannels.app.openExternal, async ({ url }) => {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
||||
return { ok: false as const, error: 'Only http(s) URLs are allowed' };
|
||||
}
|
||||
await shell.openExternal(parsed.toString());
|
||||
return { ok: true as const };
|
||||
} catch (err) {
|
||||
return { ok: false as const, error: err instanceof Error ? err.message : String(err) };
|
||||
}
|
||||
});
|
||||
registerHandler(ipcChannels.license.getStatus, () => licenseService.getStatus());
|
||||
registerHandler(ipcChannels.license.setToken, async ({ token }) => licenseService.setToken(token));
|
||||
registerHandler(ipcChannels.license.clearToken, () => licenseService.clearToken());
|
||||
@@ -1945,6 +1989,50 @@ async function main() {
|
||||
return { url: playersStore.getImageUrl(id) };
|
||||
});
|
||||
|
||||
registerHandler(ipcChannels.gameSystems.list, async () => {
|
||||
if (!gameSystemsStore) return { systems: [] };
|
||||
return { systems: await gameSystemsStore.list() };
|
||||
});
|
||||
registerHandler(ipcChannels.gameSystems.importFromFile, async () => {
|
||||
if (!gameSystemsStore) throw new Error('Game systems store unavailable');
|
||||
const { canceled, filePaths } = await dialog.showOpenDialog({
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
{
|
||||
name: app.getLocale().toLowerCase().startsWith('ru') ? 'Системы (JSON)' : 'Systems (JSON)',
|
||||
extensions: ['json'],
|
||||
},
|
||||
],
|
||||
});
|
||||
if (canceled || filePaths.length === 0) return { canceled: true as const };
|
||||
const filePath = filePaths[0]!;
|
||||
const raw = await fs.readFile(filePath, 'utf8');
|
||||
const parsed = JSON.parse(raw) as SystemDefinition;
|
||||
const system = await gameSystemsStore.importSystem(parsed);
|
||||
return { canceled: false as const, system };
|
||||
});
|
||||
registerHandler(ipcChannels.gameSystems.delete, async ({ id }) => {
|
||||
if (!gameSystemsStore) return { ok: true as const };
|
||||
await gameSystemsStore.delete(id);
|
||||
if (playersStore) {
|
||||
const cleared = await playersStore.clearSystemBindings(id);
|
||||
if (cleared > 0) emitPlayersState();
|
||||
}
|
||||
return { ok: true as const };
|
||||
});
|
||||
registerHandler(ipcChannels.gameSystems.get, async ({ id }) => {
|
||||
if (!gameSystemsStore) return { system: null };
|
||||
return { system: await gameSystemsStore.get(id) };
|
||||
});
|
||||
registerHandler(ipcChannels.gameSystems.bindSnapshot, async ({ systemId }) => {
|
||||
if (!gameSystemsStore) throw new Error('Game systems store unavailable');
|
||||
return gameSystemsStore.bindSnapshot(systemId);
|
||||
});
|
||||
registerHandler(ipcChannels.gameSystems.getSnapshot, async ({ hash }) => {
|
||||
if (!gameSystemsStore) return { system: null };
|
||||
return { system: await gameSystemsStore.getSnapshot(hash) };
|
||||
});
|
||||
|
||||
registerHandler(ipcChannels.playlists.getState, async () => {
|
||||
if (!playlistsStore) return { playlists: [], audios: [] };
|
||||
await playlistsStore.ensureLoaded();
|
||||
|
||||
Reference in New Issue
Block a user