feat(editor): побочные сюжетные линии и импорт/экспорт линий

Добавлена полноценная поддержка побочных сюжетных линий в редакторе и на
пульте: визуальное выделение компонент графа, запрет недопустимых связей
между основной и побочными линиями, метки «ПОБОЧНАЯ» и названия линий.

Реализован partial export/import сюжетных линий:
- экспорт выбранных линий в урезанный .ttrpg.zip с manifest в project.json;
- импорт в открытый проект с выбором линий, разрешением конфликтов названий
  сцен (одна модалка на операцию) и отчётом о результате;
- новое окно источника импорта: «Из проекта» (dropdown, без текущего) или
  «Из файла»; на главном экране — только полный импорт из файла.

Исправлены гонки при открытии/закрытии проекта (сериализация open/close в
main, сброс зависших состояний UI), залипание оверлея прогресса экспорта и
старт Electron в dev после готовности Vite.

Добавлены unit-тесты для lineage, export/import и контрактов zipStore.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-07-09 14:47:28 +08:00
parent de9190959c
commit c8ab9dd567
26 changed files with 3507 additions and 266 deletions
+84 -1
View File
@@ -14,6 +14,13 @@ import type {
VideoPlaybackEvent,
VideoPlaybackState,
} from '../types';
import type {
SceneImportResolution,
StorylineImportMergeReport,
StorylineLabels,
StorylineListItem,
StorylineSelection,
} from '../graph/storylineExportImport';
export const ipcChannels = {
app: {
@@ -48,10 +55,20 @@ export const ipcChannels = {
addSceneGraphEdge: 'project.addSceneGraphEdge',
removeSceneGraphEdge: 'project.removeSceneGraphEdge',
setSceneGraphNodeStart: 'project.setSceneGraphNodeStart',
setSceneGraphNodeSideStoryStart: 'project.setSceneGraphNodeSideStoryStart',
updateSideStoryLineTitle: 'project.updateSideStoryLineTitle',
deleteScene: 'project.deleteScene',
rename: 'project.rename',
importZip: 'project.importZip',
exportZip: 'project.exportZip',
getProjectStorylines: 'project.getProjectStorylines',
peekImportZip: 'project.peekImportZip',
pickImportZipFile: 'project.pickImportZipFile',
peekImportZipPath: 'project.peekImportZipPath',
peekImportFromProject: 'project.peekImportFromProject',
mergeImportZip: 'project.mergeImportZip',
mergeImportFromProject: 'project.mergeImportFromProject',
importZipFromPath: 'project.importZipFromPath',
deleteProject: 'project.deleteProject',
importZipProgress: 'project.importZipProgress',
exportZipProgress: 'project.exportZipProgress',
@@ -249,6 +266,14 @@ export type IpcInvokeMap = {
req: { graphNodeId: GraphNodeId | null };
res: { project: Project };
};
[ipcChannels.project.setSceneGraphNodeSideStoryStart]: {
req: { graphNodeId: GraphNodeId };
res: { project: Project };
};
[ipcChannels.project.updateSideStoryLineTitle]: {
req: { graphNodeId: GraphNodeId; title: string };
res: { project: Project };
};
[ipcChannels.project.deleteScene]: {
req: { sceneId: SceneId };
res: { project: Project };
@@ -261,8 +286,66 @@ export type IpcInvokeMap = {
req: Record<string, never>;
res: { canceled: true } | { canceled: false; project: Project };
};
[ipcChannels.project.getProjectStorylines]: {
req: { projectId: ProjectId; labels: StorylineLabels };
res: { storylines: StorylineListItem[] };
};
[ipcChannels.project.peekImportZip]: {
req: { labels: StorylineLabels; targetHasMainStart: boolean };
res:
| { canceled: true }
| {
canceled: false;
filePath: string;
projectName: string;
storylines: StorylineListItem[];
sourceProject: Project;
};
};
[ipcChannels.project.pickImportZipFile]: {
req: Record<string, never>;
res: { canceled: true } | { canceled: false; filePath: string };
};
[ipcChannels.project.peekImportZipPath]: {
req: { filePath: string; labels: StorylineLabels; targetHasMainStart: boolean };
res: {
filePath: string;
projectName: string;
storylines: StorylineListItem[];
sourceProject: Project;
};
};
[ipcChannels.project.peekImportFromProject]: {
req: { sourceProjectId: ProjectId; labels: StorylineLabels; targetHasMainStart: boolean };
res: {
sourceProjectId: ProjectId;
projectName: string;
storylines: StorylineListItem[];
sourceProject: Project;
};
};
[ipcChannels.project.mergeImportZip]: {
req: {
filePath: string;
storylineSelections: StorylineSelection[];
sceneResolutions: SceneImportResolution[];
};
res: { project: Project; report: StorylineImportMergeReport };
};
[ipcChannels.project.mergeImportFromProject]: {
req: {
sourceProjectId: ProjectId;
storylineSelections: StorylineSelection[];
sceneResolutions: SceneImportResolution[];
};
res: { project: Project; report: StorylineImportMergeReport };
};
[ipcChannels.project.importZipFromPath]: {
req: { filePath: string };
res: { project: Project };
};
[ipcChannels.project.exportZip]: {
req: { projectId: ProjectId };
req: { projectId: ProjectId; storylineSelections: StorylineSelection[]; labels: StorylineLabels };
res: { canceled: true } | { canceled: false };
};
[ipcChannels.project.deleteProject]: {