diff --git a/app/main/index.ts b/app/main/index.ts index 2a16b38..99414de 100644 --- a/app/main/index.ts +++ b/app/main/index.ts @@ -1893,25 +1893,29 @@ async function main() { registerHandler(ipcChannels.project.importFoundry, async ({ sourcePath }) => { emitZipProgress({ kind: 'import', stage: 'copy', percent: 0, detail: 'Импорт Foundry…' }); try { - const { project, pendingAppPlaylists } = await projectStore.importProjectFromFoundry(sourcePath, (p) => { - emitZipProgress({ - kind: 'import', - stage: - p.stage === 'unzip' ? 'unzip' : p.stage === 'zip' ? 'zip' : p.stage === 'done' ? 'done' : 'copy', - percent: p.percent, - ...(p.detail ? { detail: p.detail } : null), - }); - }); - if (playlistsStore && pendingAppPlaylists.length > 0) { - for (const pl of pendingAppPlaylists) { - const created = await playlistsStore.createPlaylist(pl.name); - const paths = pl.tracks.map((t) => t.absPath); - if (paths.length > 0) { - await playlistsStore.addAudioFiles(created.id, paths); + const { project } = await projectStore.importProjectFromFoundry( + sourcePath, + (p) => { + emitZipProgress({ + kind: 'import', + stage: + p.stage === 'unzip' ? 'unzip' : p.stage === 'zip' ? 'zip' : p.stage === 'done' ? 'done' : 'copy', + percent: p.percent, + ...(p.detail ? { detail: p.detail } : null), + }); + }, + async (pendingAppPlaylists) => { + if (!playlistsStore || pendingAppPlaylists.length === 0) return; + for (const pl of pendingAppPlaylists) { + const created = await playlistsStore.createPlaylist(pl.name); + const paths = pl.tracks.map((t) => t.absPath); + if (paths.length > 0) { + await playlistsStore.addAudioFiles(created.id, paths); + } } - } - emitPlaylistsState(); - } + emitPlaylistsState(); + }, + ); emitZipProgress({ kind: 'import', stage: 'done', percent: 100, detail: 'Готово' }); emitSessionState(); return { project }; diff --git a/app/main/project/zipStore.legacyContract.test.ts b/app/main/project/zipStore.legacyContract.test.ts index 10ff587..2843b1b 100644 --- a/app/main/project/zipStore.legacyContract.test.ts +++ b/app/main/project/zipStore.legacyContract.test.ts @@ -67,6 +67,15 @@ void test('zipStore: closeOpenProject is serialized with open on projectSwitchCh assert.match(src, /Открытие проекта отменено/); }); +void test('zipStore: Foundry archive playlists are copied before temp unzip cleanup', () => { + const src = fs.readFileSync(path.join(here, 'zipStore.ts'), 'utf8'); + const fn = src.slice(src.indexOf('async importProjectFromFoundry')); + const body = fn.slice(0, fn.indexOf('\n async exportProjectZipToPath')); + const copyAt = body.indexOf('await importPlaylists(pendingAppPlaylists)'); + const cleanupAt = body.indexOf('await loaded.cleanup()'); + assert.ok(copyAt > 0 && cleanupAt > copyAt); +}); + void test('zipStore: exportProjectZipToPath flushes saveNow for currently open project', () => { const src = fs.readFileSync(path.join(here, 'zipStore.ts'), 'utf8'); assert.match(src, /async exportProjectZipToPath/); diff --git a/app/main/project/zipStore.ts b/app/main/project/zipStore.ts index 04a8e48..2a77234 100644 --- a/app/main/project/zipStore.ts +++ b/app/main/project/zipStore.ts @@ -108,6 +108,7 @@ import { buildProjectFromFoundryDocuments, loadFoundryDocumentsForImport, type FoundryImportProgress, + type FoundryPendingAppPlaylist, } from '../foundry/foundryImport'; import { getAppSemanticVersion } from '../versionInfo'; @@ -2129,7 +2130,8 @@ export class ZipProjectStore { async importProjectFromFoundry( sourcePath: string, onProgress?: (p: FoundryImportProgress) => void, - ): Promise<{ project: Project; pendingAppPlaylists: import('../foundry/foundryImport').FoundryPendingAppPlaylist[] }> { + importPlaylists?: (lists: FoundryPendingAppPlaylist[]) => Promise, + ): Promise<{ project: Project; pendingAppPlaylists: FoundryPendingAppPlaylist[] }> { await this.ensureRoots(); const loaded = await loadFoundryDocumentsForImport(sourcePath, onProgress); try { @@ -2159,6 +2161,10 @@ export class ZipProjectStore { await this.writeCacheProject(cacheDir, project); onProgress?.({ stage: 'zip', percent: 90, detail: 'Сборка проекта…' }); await this.enqueuePack(cacheDir, zipPath); + // Архив распакован во временную папку. Плейлисты читают её по absPath — копировать до cleanup. + if (importPlaylists && pendingAppPlaylists.length > 0) { + await importPlaylists(pendingAppPlaylists); + } onProgress?.({ stage: 'done', percent: 100, detail: 'Готово' }); return { project: this.openProject.project, pendingAppPlaylists }; } finally { diff --git a/app/renderer/editor/i18n/editorMessages.ts b/app/renderer/editor/i18n/editorMessages.ts index d5fd6dd..b6799a8 100644 --- a/app/renderer/editor/i18n/editorMessages.ts +++ b/app/renderer/editor/i18n/editorMessages.ts @@ -355,7 +355,7 @@ export const EDITOR_MESSAGES: Record> = { 'foundryImport.title': 'Импорт из Foundry', 'foundryImport.hint': - 'Выберите папку или архив мира (.world) либо модуля Foundry VTT (версии 11+). Будет создан новый проект.', + 'Выберите папку мира или модуля Foundry VTT (версия 11+) либо архив (.zip / .fvtt). Будет создан новый проект.', 'foundryImport.sourceType': 'ТИП ИСТОЧНИКА', 'foundryImport.folder': 'Папка', 'foundryImport.archive': 'Архив (.zip / .fvtt)', @@ -1182,7 +1182,7 @@ export const EDITOR_MESSAGES: Record> = { 'foundryImport.title': 'Import from Foundry', 'foundryImport.hint': - 'Choose a Foundry VTT world or module folder or archive (version 11+). A new project will be created.', + 'Choose a Foundry VTT world or module folder, or an archive (.zip / .fvtt), version 11+. A new project will be created.', 'foundryImport.sourceType': 'SOURCE TYPE', 'foundryImport.folder': 'Folder', 'foundryImport.archive': 'Archive (.zip / .fvtt)',