feat(materials): add campaign materials overlay for sessions

Let GMs manage and show images over the scene from the editor and control panel, with zoom tools, help docs, and full ru/en i18n.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-07-17 11:05:38 +08:00
parent 195d4be086
commit 61875be857
39 changed files with 2521 additions and 22 deletions
+63 -1
View File
@@ -8,7 +8,15 @@ import type {
StorylineListItem,
StorylineSelection,
} from '../../../shared/graph/storylineExportImport';
import type { AssetId, GraphNodeId, Project, ProjectId, Scene, SceneId } from '../../../shared/types';
import type {
AssetId,
GraphNodeId,
MaterialId,
Project,
ProjectId,
Scene,
SceneId,
} from '../../../shared/types';
import { getDndApi } from '../../shared/dndApi';
type ProjectSummary = { id: ProjectId; name: string; updatedAt: string; fileName: string };
@@ -40,6 +48,15 @@ type Actions = {
importCampaignAudio: () => Promise<void>;
importCampaignAudioFromPaths: (filePaths: string[]) => Promise<void>;
updateCampaignAudios: (next: Project['campaignAudios']) => Promise<void>;
upsertMaterial: (input: {
materialId?: MaterialId;
name: string;
filePath?: string;
}) => Promise<void>;
deleteMaterial: (materialId: MaterialId) => Promise<void>;
setMaterialsOrder: (materialIds: MaterialId[]) => Promise<void>;
setMaterialRotation: (materialId: MaterialId, rotationDeg: 0 | 90 | 180 | 270) => Promise<void>;
pickMaterialImage: () => Promise<{ filePath: string; previewDataUrl: string } | null>;
updateScene: (
sceneId: SceneId,
patch: {
@@ -469,6 +486,46 @@ export function useProjectState(licenseActive: boolean, opts?: ProjectStateOpts)
await refreshProjects();
};
const upsertMaterial = async (input: {
materialId?: MaterialId;
name: string;
filePath?: string;
}) => {
const res = await api.invoke(ipcChannels.project.upsertMaterial, input);
setState((s) => ({ ...s, project: res.project }));
await refreshProjects();
};
const deleteMaterial = async (materialId: MaterialId) => {
const res = await api.invoke(ipcChannels.project.deleteMaterial, { materialId });
setState((s) => ({ ...s, project: res.project }));
await refreshProjects();
};
const setMaterialsOrder = async (materialIds: MaterialId[]) => {
const res = await api.invoke(ipcChannels.project.setMaterialsOrder, { materialIds });
setState((s) => ({ ...s, project: res.project }));
await refreshProjects();
};
const setMaterialRotation = async (
materialId: MaterialId,
rotationDeg: 0 | 90 | 180 | 270,
) => {
const res = await api.invoke(ipcChannels.project.setMaterialRotation, {
materialId,
rotationDeg,
});
setState((s) => ({ ...s, project: res.project }));
await refreshProjects();
};
const pickMaterialImage = async () => {
const res = await api.invoke(ipcChannels.project.pickMaterialImage, {});
if (res.canceled) return null;
return { filePath: res.filePath, previewDataUrl: res.previewDataUrl };
};
const updateScene = async (
sceneId: SceneId,
patch: {
@@ -802,6 +859,11 @@ export function useProjectState(licenseActive: boolean, opts?: ProjectStateOpts)
importCampaignAudio,
importCampaignAudioFromPaths,
updateCampaignAudios,
upsertMaterial,
deleteMaterial,
setMaterialsOrder,
setMaterialRotation,
pickMaterialImage,
updateScene,
updateConnections,
importMediaToScene,