feat(players): app Players library and circular NPC tokens on scenes
Add userData players/teams, scene npcTokens with hex-inscribed sizing, session scale synced to presentation, and Playwright e2e coverage. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+237
-113
@@ -29,6 +29,8 @@ import { NpcsOverlayStore } from './npcs/npcsOverlayStore';
|
||||
import { ZipProjectStore } from './project/zipStore';
|
||||
import { SceneViewStore } from './sceneView/sceneViewStore';
|
||||
import { registerDndAssetProtocol } from './protocol/dndAssetProtocol';
|
||||
import { PlayersStore } from './players/playersStore';
|
||||
import { SceneNpcTokensSessionStore } from './players/sceneNpcTokensSessionStore';
|
||||
import { SceneTokensSessionStore } from './tokens/sceneTokensSessionStore';
|
||||
import { TokensStore } from './tokens/tokensStore';
|
||||
import { installAutoUpdater } from './update/installAutoUpdater';
|
||||
@@ -86,21 +88,13 @@ function emitScenePreviewImportProgress(evt: ScenePreviewImportEvent): void {
|
||||
}
|
||||
}
|
||||
|
||||
function emitMaterialUpsertProgress(evt: {
|
||||
percent: number;
|
||||
stage: string;
|
||||
detail?: string;
|
||||
}): void {
|
||||
function emitMaterialUpsertProgress(evt: { percent: number; stage: string; detail?: string }): void {
|
||||
for (const win of BrowserWindow.getAllWindows()) {
|
||||
win.webContents.send(ipcChannels.project.materialUpsertProgress, evt);
|
||||
}
|
||||
}
|
||||
|
||||
function emitNpcUpsertProgress(evt: {
|
||||
percent: number;
|
||||
stage: string;
|
||||
detail?: string;
|
||||
}): void {
|
||||
function emitNpcUpsertProgress(evt: { percent: number; stage: string; detail?: string }): void {
|
||||
for (const win of BrowserWindow.getAllWindows()) {
|
||||
win.webContents.send(ipcChannels.project.npcUpsertProgress, evt);
|
||||
}
|
||||
@@ -179,7 +173,9 @@ const videoStore = new VideoPlaybackStore();
|
||||
const materialsOverlayStore = new MaterialsOverlayStore();
|
||||
const npcsOverlayStore = new NpcsOverlayStore();
|
||||
const sceneTokensSessionStore = new SceneTokensSessionStore();
|
||||
const sceneNpcTokensSessionStore = new SceneNpcTokensSessionStore();
|
||||
let tokensStore: TokensStore | null = null;
|
||||
let playersStore: PlayersStore | null = null;
|
||||
|
||||
function emitEffectsState(): void {
|
||||
const state = effectsStore.getState();
|
||||
@@ -248,6 +244,27 @@ function emitSceneTokensSessionState(): void {
|
||||
}
|
||||
}
|
||||
|
||||
function emitPlayersState(): void {
|
||||
const players = playersStore?.listPlayers() ?? [];
|
||||
const teams = playersStore?.listTeams() ?? [];
|
||||
for (const win of BrowserWindow.getAllWindows()) {
|
||||
win.webContents.send(ipcChannels.players.stateChanged, { players, teams });
|
||||
}
|
||||
}
|
||||
|
||||
function emitPlayerUpsertProgress(p: { percent: number; stage: string; detail?: string }): void {
|
||||
for (const win of BrowserWindow.getAllWindows()) {
|
||||
win.webContents.send(ipcChannels.players.upsertProgress, p);
|
||||
}
|
||||
}
|
||||
|
||||
function emitSceneNpcTokensSessionState(): void {
|
||||
const state = sceneNpcTokensSessionStore.getState();
|
||||
for (const win of BrowserWindow.getAllWindows()) {
|
||||
win.webContents.send(ipcChannels.sceneNpcTokensSession.stateChanged, { state });
|
||||
}
|
||||
}
|
||||
|
||||
function syncSceneDarknessForProject(project: Project): void {
|
||||
const cacheKey = project.currentGraphNodeId ?? project.currentSceneId ?? null;
|
||||
const scene = project.currentSceneId ? project.scenes[project.currentSceneId] : undefined;
|
||||
@@ -397,11 +414,13 @@ async function main() {
|
||||
const licenseService = new LicenseService(app.getPath('userData'));
|
||||
tokensStore = new TokensStore(app.getPath('userData'));
|
||||
await tokensStore.ensureLoaded();
|
||||
playersStore = new PlayersStore(app.getPath('userData'));
|
||||
await playersStore.ensureLoaded();
|
||||
setLicenseAssert(() => {
|
||||
licenseService.assertForIpc();
|
||||
});
|
||||
installAppMenuForSession();
|
||||
registerDndAssetProtocol(projectStore, tokensStore);
|
||||
registerDndAssetProtocol(projectStore, tokensStore, playersStore);
|
||||
registerHandler(ipcChannels.app.quit, () => {
|
||||
markAppQuitting();
|
||||
app.quit();
|
||||
@@ -420,6 +439,7 @@ async function main() {
|
||||
sceneDarknessStore.resetSession();
|
||||
sceneTrapsStore.resetSession();
|
||||
sceneTokensSessionStore.reset();
|
||||
sceneNpcTokensSessionStore.reset();
|
||||
effectsStore.dispatch({ kind: 'tool.set', tool: effectsDefaultTool() });
|
||||
openMultiWindow();
|
||||
const project = projectStore.getOpenProject();
|
||||
@@ -430,6 +450,7 @@ async function main() {
|
||||
emitSceneDarknessState();
|
||||
emitSceneTrapsState();
|
||||
emitSceneTokensSessionState();
|
||||
emitSceneNpcTokensSessionState();
|
||||
emitEffectsState();
|
||||
return { ok: true };
|
||||
});
|
||||
@@ -526,8 +547,10 @@ async function main() {
|
||||
const project = await projectStore.openProjectById(projectId);
|
||||
sceneViewStore.reset();
|
||||
sceneTokensSessionStore.reset();
|
||||
sceneNpcTokensSessionStore.reset();
|
||||
emitSceneViewState();
|
||||
emitSceneTokensSessionState();
|
||||
emitSceneNpcTokensSessionState();
|
||||
emitSessionState();
|
||||
warmNpcsEditorWindow();
|
||||
return { project };
|
||||
@@ -542,6 +565,7 @@ async function main() {
|
||||
sceneTrapsStore.resetSession();
|
||||
sceneViewStore.reset();
|
||||
sceneTokensSessionStore.reset();
|
||||
sceneNpcTokensSessionStore.reset();
|
||||
emitEffectsState();
|
||||
emitMaterialsOverlayState();
|
||||
emitNpcsOverlayState();
|
||||
@@ -549,6 +573,7 @@ async function main() {
|
||||
emitSceneTrapsState();
|
||||
emitSceneViewState();
|
||||
emitSceneTokensSessionState();
|
||||
emitSceneNpcTokensSessionState();
|
||||
emitSessionState();
|
||||
return { ok: true };
|
||||
});
|
||||
@@ -684,36 +709,39 @@ async function main() {
|
||||
emitSessionState();
|
||||
return { project };
|
||||
});
|
||||
registerHandler(ipcChannels.project.upsertMaterial, async ({ materialId, name, filePath: pathFromDrop }) => {
|
||||
let filePath = pathFromDrop;
|
||||
if (!filePath && !materialId) {
|
||||
const { canceled, filePaths } = await dialog.showOpenDialog({
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
{
|
||||
name: openDialogFilterLabel('images', app.getLocale()),
|
||||
extensions: ['png', 'jpg', 'jpeg', 'webp'],
|
||||
},
|
||||
],
|
||||
});
|
||||
if (canceled || filePaths.length === 0) {
|
||||
throw new Error('Material image is required');
|
||||
registerHandler(
|
||||
ipcChannels.project.upsertMaterial,
|
||||
async ({ materialId, name, filePath: pathFromDrop }) => {
|
||||
let filePath = pathFromDrop;
|
||||
if (!filePath && !materialId) {
|
||||
const { canceled, filePaths } = await dialog.showOpenDialog({
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
{
|
||||
name: openDialogFilterLabel('images', app.getLocale()),
|
||||
extensions: ['png', 'jpg', 'jpeg', 'webp'],
|
||||
},
|
||||
],
|
||||
});
|
||||
if (canceled || filePaths.length === 0) {
|
||||
throw new Error('Material image is required');
|
||||
}
|
||||
filePath = filePaths[0];
|
||||
}
|
||||
filePath = filePaths[0];
|
||||
}
|
||||
const project = await projectStore.upsertMaterial(
|
||||
{
|
||||
...(materialId ? { materialId } : {}),
|
||||
name,
|
||||
...(filePath ? { filePath } : {}),
|
||||
},
|
||||
(p) => emitMaterialUpsertProgress(p),
|
||||
);
|
||||
syncMaterialsOverlayWithProject(project);
|
||||
emitMaterialsOverlayState();
|
||||
emitSessionState();
|
||||
return { project };
|
||||
});
|
||||
const project = await projectStore.upsertMaterial(
|
||||
{
|
||||
...(materialId ? { materialId } : {}),
|
||||
name,
|
||||
...(filePath ? { filePath } : {}),
|
||||
},
|
||||
(p) => emitMaterialUpsertProgress(p),
|
||||
);
|
||||
syncMaterialsOverlayWithProject(project);
|
||||
emitMaterialsOverlayState();
|
||||
emitSessionState();
|
||||
return { project };
|
||||
},
|
||||
);
|
||||
registerHandler(ipcChannels.project.deleteMaterial, async ({ materialId }) => {
|
||||
const project = await projectStore.deleteMaterial(materialId);
|
||||
syncMaterialsOverlayWithProject(project);
|
||||
@@ -750,14 +778,13 @@ async function main() {
|
||||
const filePath = filePaths[0]!;
|
||||
const buf = await fs.readFile(filePath);
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
const mime =
|
||||
ext === '.png' ? 'image/png' : ext === '.webp' ? 'image/webp' : 'image/jpeg';
|
||||
const mime = ext === '.png' ? 'image/png' : ext === '.webp' ? 'image/webp' : 'image/jpeg';
|
||||
const previewDataUrl = `data:${mime};base64,${buf.toString('base64')}`;
|
||||
return { canceled: false as const, filePath, previewDataUrl };
|
||||
});
|
||||
registerHandler(
|
||||
ipcChannels.project.upsertNpc,
|
||||
async ({ npcId, name, description, filePath: pathFromDrop, groupId }) => {
|
||||
async ({ npcId, name, description, filePath: pathFromDrop, groupId, ringColor, imageOffset }) => {
|
||||
let filePath = pathFromDrop;
|
||||
if (!filePath && !npcId) {
|
||||
const { canceled, filePaths } = await dialog.showOpenDialog({
|
||||
@@ -781,6 +808,8 @@ async function main() {
|
||||
...(typeof description === 'string' ? { description } : {}),
|
||||
...(filePath ? { filePath } : {}),
|
||||
...(groupId !== undefined ? { groupId } : {}),
|
||||
...(ringColor !== undefined ? { ringColor } : {}),
|
||||
...(imageOffset !== undefined ? { imageOffset } : {}),
|
||||
},
|
||||
(p) => emitNpcUpsertProgress(p),
|
||||
);
|
||||
@@ -792,11 +821,13 @@ async function main() {
|
||||
);
|
||||
registerHandler(
|
||||
ipcChannels.project.updateNpcFields,
|
||||
async ({ npcId, name, description, groupId }) => {
|
||||
async ({ npcId, name, description, groupId, ringColor, imageOffset }) => {
|
||||
const project = await projectStore.updateNpcFields(npcId, {
|
||||
...(typeof name === 'string' ? { name } : {}),
|
||||
...(typeof description === 'string' ? { description } : {}),
|
||||
...(groupId !== undefined ? { groupId } : {}),
|
||||
...(ringColor !== undefined ? { ringColor } : {}),
|
||||
...(imageOffset !== undefined ? { imageOffset } : {}),
|
||||
});
|
||||
emitSessionState();
|
||||
return { project };
|
||||
@@ -833,8 +864,7 @@ async function main() {
|
||||
const filePath = filePaths[0]!;
|
||||
const buf = await fs.readFile(filePath);
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
const mime =
|
||||
ext === '.png' ? 'image/png' : ext === '.webp' ? 'image/webp' : 'image/jpeg';
|
||||
const mime = ext === '.png' ? 'image/png' : ext === '.webp' ? 'image/webp' : 'image/jpeg';
|
||||
const previewDataUrl = `data:${mime};base64,${buf.toString('base64')}`;
|
||||
return { canceled: false as const, filePath, previewDataUrl };
|
||||
});
|
||||
@@ -856,19 +886,16 @@ async function main() {
|
||||
emitSessionState();
|
||||
return { project };
|
||||
});
|
||||
registerHandler(
|
||||
ipcChannels.project.upsertNpcGroup,
|
||||
async ({ groupId, name, color, parentId }) => {
|
||||
const project = await projectStore.upsertNpcGroup({
|
||||
...(groupId ? { groupId } : {}),
|
||||
name,
|
||||
...(typeof color === 'string' ? { color } : {}),
|
||||
...(parentId !== undefined ? { parentId } : {}),
|
||||
});
|
||||
emitSessionState();
|
||||
return { project };
|
||||
},
|
||||
);
|
||||
registerHandler(ipcChannels.project.upsertNpcGroup, async ({ groupId, name, color, parentId }) => {
|
||||
const project = await projectStore.upsertNpcGroup({
|
||||
...(groupId ? { groupId } : {}),
|
||||
name,
|
||||
...(typeof color === 'string' ? { color } : {}),
|
||||
...(parentId !== undefined ? { parentId } : {}),
|
||||
});
|
||||
emitSessionState();
|
||||
return { project };
|
||||
});
|
||||
registerHandler(ipcChannels.project.deleteNpcGroup, async ({ groupId }) => {
|
||||
const project = await projectStore.deleteNpcGroup(groupId);
|
||||
syncNpcsOverlayWithProject(project);
|
||||
@@ -1042,9 +1069,12 @@ async function main() {
|
||||
registerHandler(ipcChannels.project.peekImportZipPath, async ({ filePath, labels, targetHasMainStart }) => {
|
||||
return projectStore.peekImportFromZipPath(filePath, labels, targetHasMainStart);
|
||||
});
|
||||
registerHandler(ipcChannels.project.peekImportFromProject, async ({ sourceProjectId, labels, targetHasMainStart }) => {
|
||||
return projectStore.peekImportFromProjectId(sourceProjectId, labels, targetHasMainStart);
|
||||
});
|
||||
registerHandler(
|
||||
ipcChannels.project.peekImportFromProject,
|
||||
async ({ sourceProjectId, labels, targetHasMainStart }) => {
|
||||
return projectStore.peekImportFromProjectId(sourceProjectId, labels, targetHasMainStart);
|
||||
},
|
||||
);
|
||||
registerHandler(
|
||||
ipcChannels.project.mergeImportZip,
|
||||
async ({ filePath, storylineSelections, sceneResolutions, npcResolutions }) => {
|
||||
@@ -1143,7 +1173,8 @@ async function main() {
|
||||
const project = await projectStore.importProjectFromFoundry(sourcePath, (p) => {
|
||||
emitZipProgress({
|
||||
kind: 'import',
|
||||
stage: p.stage === 'unzip' ? 'unzip' : p.stage === 'zip' ? 'zip' : p.stage === 'done' ? 'done' : 'copy',
|
||||
stage:
|
||||
p.stage === 'unzip' ? 'unzip' : p.stage === 'zip' ? 'zip' : p.stage === 'done' ? 'done' : 'copy',
|
||||
percent: p.percent,
|
||||
...(p.detail ? { detail: p.detail } : null),
|
||||
});
|
||||
@@ -1156,53 +1187,56 @@ async function main() {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
registerHandler(ipcChannels.project.exportZip, async ({ projectId, storylineSelections, npcIds, labels }) => {
|
||||
const list = await projectStore.listProjects();
|
||||
const entry = list.find((p) => p.id === projectId);
|
||||
if (!entry) {
|
||||
throw new Error('Проект не найден');
|
||||
}
|
||||
const defaultName = isProjectZipFileName(entry.fileName)
|
||||
? entry.fileName.toLowerCase().endsWith('.ttrpg.zip')
|
||||
? entry.fileName
|
||||
: projectZipFileNameFromBase(stripProjectZipExtension(entry.fileName))
|
||||
: projectZipFileNameFromBase(stripProjectZipExtension(entry.fileName));
|
||||
const { canceled, filePath } = await dialog.showSaveDialog({
|
||||
defaultPath: defaultName,
|
||||
filters: [PROJECT_ZIP_SAVE_DIALOG_FILTER],
|
||||
});
|
||||
if (canceled || !filePath) {
|
||||
return { canceled: true as const };
|
||||
}
|
||||
const dest = normalizeSaveProjectZipPath(filePath);
|
||||
try {
|
||||
emitZipProgress({ kind: 'export', stage: 'zip', percent: 0, detail: 'Экспорт…' });
|
||||
await projectStore.exportStorylinesZipToPath(
|
||||
projectId,
|
||||
storylineSelections,
|
||||
npcIds ?? [],
|
||||
dest,
|
||||
labels,
|
||||
(p) => {
|
||||
emitZipProgress({
|
||||
kind: 'export',
|
||||
stage: p.stage,
|
||||
percent: p.percent,
|
||||
...(p.detail ? { detail: p.detail } : null),
|
||||
});
|
||||
},
|
||||
async (tokenIds, exportRoot) => {
|
||||
await tokensStore!.packForExport(tokenIds, exportRoot);
|
||||
},
|
||||
);
|
||||
emitZipProgress({ kind: 'export', stage: 'done', percent: 100, detail: 'Готово' });
|
||||
return { canceled: false as const };
|
||||
} catch (err) {
|
||||
const detail = err instanceof Error ? err.message : 'Ошибка экспорта';
|
||||
emitZipProgress({ kind: 'export', stage: 'error', percent: 0, detail });
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
registerHandler(
|
||||
ipcChannels.project.exportZip,
|
||||
async ({ projectId, storylineSelections, npcIds, labels }) => {
|
||||
const list = await projectStore.listProjects();
|
||||
const entry = list.find((p) => p.id === projectId);
|
||||
if (!entry) {
|
||||
throw new Error('Проект не найден');
|
||||
}
|
||||
const defaultName = isProjectZipFileName(entry.fileName)
|
||||
? entry.fileName.toLowerCase().endsWith('.ttrpg.zip')
|
||||
? entry.fileName
|
||||
: projectZipFileNameFromBase(stripProjectZipExtension(entry.fileName))
|
||||
: projectZipFileNameFromBase(stripProjectZipExtension(entry.fileName));
|
||||
const { canceled, filePath } = await dialog.showSaveDialog({
|
||||
defaultPath: defaultName,
|
||||
filters: [PROJECT_ZIP_SAVE_DIALOG_FILTER],
|
||||
});
|
||||
if (canceled || !filePath) {
|
||||
return { canceled: true as const };
|
||||
}
|
||||
const dest = normalizeSaveProjectZipPath(filePath);
|
||||
try {
|
||||
emitZipProgress({ kind: 'export', stage: 'zip', percent: 0, detail: 'Экспорт…' });
|
||||
await projectStore.exportStorylinesZipToPath(
|
||||
projectId,
|
||||
storylineSelections,
|
||||
npcIds ?? [],
|
||||
dest,
|
||||
labels,
|
||||
(p) => {
|
||||
emitZipProgress({
|
||||
kind: 'export',
|
||||
stage: p.stage,
|
||||
percent: p.percent,
|
||||
...(p.detail ? { detail: p.detail } : null),
|
||||
});
|
||||
},
|
||||
async (tokenIds, exportRoot) => {
|
||||
await tokensStore!.packForExport(tokenIds, exportRoot);
|
||||
},
|
||||
);
|
||||
emitZipProgress({ kind: 'export', stage: 'done', percent: 100, detail: 'Готово' });
|
||||
return { canceled: false as const };
|
||||
} catch (err) {
|
||||
const detail = err instanceof Error ? err.message : 'Ошибка экспорта';
|
||||
emitZipProgress({ kind: 'export', stage: 'error', percent: 0, detail });
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
);
|
||||
registerHandler(ipcChannels.project.deleteProject, async ({ projectId }) => {
|
||||
await projectStore.deleteProjectById(projectId);
|
||||
emitSessionState();
|
||||
@@ -1253,7 +1287,11 @@ async function main() {
|
||||
return { tokens: tokensStore!.list() };
|
||||
});
|
||||
registerHandler(ipcChannels.tokens.upsert, async ({ id, name, filePath }) => {
|
||||
const token = await tokensStore!.upsert({ id, name, filePath });
|
||||
const token = await tokensStore!.upsert({
|
||||
name,
|
||||
...(id !== undefined ? { id } : {}),
|
||||
...(filePath !== undefined ? { filePath } : {}),
|
||||
});
|
||||
emitTokensState();
|
||||
return { token };
|
||||
});
|
||||
@@ -1288,8 +1326,7 @@ async function main() {
|
||||
const filePath = filePaths[0]!;
|
||||
const buf = await fs.readFile(filePath);
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
const mime =
|
||||
ext === '.png' ? 'image/png' : ext === '.webp' ? 'image/webp' : 'image/jpeg';
|
||||
const mime = ext === '.png' ? 'image/png' : ext === '.webp' ? 'image/webp' : 'image/jpeg';
|
||||
const previewDataUrl = `data:${mime};base64,${buf.toString('base64')}`;
|
||||
return { canceled: false as const, filePath, previewDataUrl };
|
||||
});
|
||||
@@ -1305,6 +1342,93 @@ async function main() {
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
registerHandler(ipcChannels.players.list, async () => {
|
||||
await playersStore!.ensureLoaded();
|
||||
return { players: playersStore!.listPlayers(), teams: playersStore!.listTeams() };
|
||||
});
|
||||
registerHandler(
|
||||
ipcChannels.players.upsert,
|
||||
async ({ id, name, filePath, teamId, ringColor, imageOffset, imageScale }) => {
|
||||
const player = await playersStore!.upsert(
|
||||
{
|
||||
name,
|
||||
...(id !== undefined ? { id } : {}),
|
||||
...(filePath !== undefined ? { filePath } : {}),
|
||||
...(teamId !== undefined ? { teamId } : {}),
|
||||
...(ringColor !== undefined ? { ringColor } : {}),
|
||||
...(imageOffset !== undefined ? { imageOffset } : {}),
|
||||
...(imageScale !== undefined ? { imageScale } : {}),
|
||||
},
|
||||
(p) => emitPlayerUpsertProgress(p),
|
||||
);
|
||||
emitPlayersState();
|
||||
return { player };
|
||||
},
|
||||
);
|
||||
registerHandler(ipcChannels.players.delete, async ({ id }) => {
|
||||
await playersStore!.delete(id);
|
||||
emitPlayersState();
|
||||
return { ok: true };
|
||||
});
|
||||
registerHandler(ipcChannels.players.setOrder, async ({ playerIds }) => {
|
||||
const players = await playersStore!.setPlayersOrder(playerIds);
|
||||
emitPlayersState();
|
||||
return { players };
|
||||
});
|
||||
registerHandler(ipcChannels.players.upsertTeam, async ({ id, name, color }) => {
|
||||
const team = await playersStore!.upsertTeam({
|
||||
name,
|
||||
...(id !== undefined ? { id } : {}),
|
||||
...(color !== undefined ? { color } : {}),
|
||||
});
|
||||
emitPlayersState();
|
||||
return { team };
|
||||
});
|
||||
registerHandler(ipcChannels.players.deleteTeam, async ({ id }) => {
|
||||
await playersStore!.deleteTeam(id);
|
||||
emitPlayersState();
|
||||
return { ok: true };
|
||||
});
|
||||
registerHandler(ipcChannels.players.setTeamsOrder, async ({ teamIds }) => {
|
||||
const teams = await playersStore!.setTeamsOrder(teamIds);
|
||||
emitPlayersState();
|
||||
return { teams };
|
||||
});
|
||||
registerHandler(ipcChannels.players.assignTeam, async ({ playerId, teamId }) => {
|
||||
const player = await playersStore!.assignPlayerTeam(playerId, teamId);
|
||||
emitPlayersState();
|
||||
return { player };
|
||||
});
|
||||
registerHandler(ipcChannels.players.pickImage, async () => {
|
||||
const { canceled, filePaths } = await dialog.showOpenDialog({
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
{
|
||||
name: openDialogFilterLabel('images', app.getLocale()),
|
||||
extensions: ['png', 'jpg', 'jpeg', 'webp'],
|
||||
},
|
||||
],
|
||||
});
|
||||
if (canceled || filePaths.length === 0) return { canceled: true as const };
|
||||
const filePath = filePaths[0]!;
|
||||
const buf = await fs.readFile(filePath);
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
const mime = ext === '.png' ? 'image/png' : ext === '.webp' ? 'image/webp' : 'image/jpeg';
|
||||
const previewDataUrl = `data:${mime};base64,${buf.toString('base64')}`;
|
||||
return { canceled: false as const, filePath, previewDataUrl };
|
||||
});
|
||||
registerHandler(ipcChannels.players.imageUrl, ({ id }) => {
|
||||
return { url: playersStore!.getImageUrl(id) };
|
||||
});
|
||||
registerHandler(ipcChannels.sceneNpcTokensSession.getState, () => {
|
||||
return { state: sceneNpcTokensSessionStore.getState() };
|
||||
});
|
||||
registerHandler(ipcChannels.sceneNpcTokensSession.dispatch, ({ event }) => {
|
||||
sceneNpcTokensSessionStore.dispatch(event);
|
||||
emitSceneNpcTokensSessionState();
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
registerHandler(ipcChannels.video.getState, () => {
|
||||
return { state: videoStore.getState() };
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user