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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user