From 2cfcb4c4a4c72f55e13ed2f7bb47ed73f7fc128c Mon Sep 17 00:00:00 2001 From: Luke-Bilhorn Date: Sat, 28 Feb 2026 19:43:53 -0600 Subject: [PATCH 01/14] feat(export): add wrap-in-folder and zip output options. --- src/exportHandler/exportHandler.ts | 58 +++++++++++++++++----- src/exportHandler/utils/zipUtils.ts | 17 +++++++ src/projectManager/projectExportView.ts | 64 +++++++++++++++++++++++-- 3 files changed, 123 insertions(+), 16 deletions(-) create mode 100644 src/exportHandler/utils/zipUtils.ts diff --git a/src/exportHandler/exportHandler.ts b/src/exportHandler/exportHandler.ts index aa429f2e0..42d83b455 100644 --- a/src/exportHandler/exportHandler.ts +++ b/src/exportHandler/exportHandler.ts @@ -8,6 +8,7 @@ import { exec } from "child_process"; import { promisify } from "util"; import { removeHtmlTags, generateSrtData } from "./subtitleUtils"; import { generateVttData } from "./vttUtils"; +import { zipDirectory } from "./utils/zipUtils"; // import { exportRtfWithPandoc } from "../../webviews/codex-webviews/src/NewSourceUploader/importers/rtf/pandocNodeBridge"; const execAsync = promisify(exec); @@ -215,6 +216,8 @@ export enum CodexExportFormat { export interface ExportOptions { skipValidation?: boolean; removeIds?: boolean; + wrapInFolder?: boolean; + zipOutput?: boolean; } // IDML Round-trip export: Uses idmlExporter or biblicaExporter based on filename @@ -1608,6 +1611,27 @@ export async function exportCodexContent( filesToExport: string[], options?: ExportOptions ) { + const shouldWrap = options?.wrapInFolder ?? false; + const shouldZip = options?.zipOutput ?? false; + + const selfManagedFormats: CodexExportFormat[] = [ + CodexExportFormat.HTML, + CodexExportFormat.AUDIO, + CodexExportFormat.REBUILD_EXPORT, + ]; + const isSelfManaged = selfManagedFormats.includes(format); + + let effectivePath = userSelectedPath; + + if ((shouldWrap || shouldZip) && !isSelfManaged) { + const projectConfig = vscode.workspace.getConfiguration("codex-project-manager"); + const projectName = projectConfig.get("projectName", "") || + basename(vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? "export"); + const dateStamp = new Date().toISOString().slice(0, 10); + const subfolderName = `${projectName}-${format}-${dateStamp}`; + effectivePath = path.join(userSelectedPath, subfolderName); + } + // Check if audio export should also be included alongside the text format export const includeAudio = (options as any)?.includeAudio === true && format !== CodexExportFormat.AUDIO; @@ -1617,42 +1641,42 @@ export async function exportCodexContent( // Add text format export switch (format) { case CodexExportFormat.PLAINTEXT: - exportPromises.push(exportCodexContentAsPlaintext(userSelectedPath, filesToExport, options)); + exportPromises.push(exportCodexContentAsPlaintext(effectivePath, filesToExport, options)); break; case CodexExportFormat.USFM: - exportPromises.push(exportCodexContentAsUsfm(userSelectedPath, filesToExport, options)); + exportPromises.push(exportCodexContentAsUsfm(effectivePath, filesToExport, options)); break; case CodexExportFormat.HTML: - exportPromises.push(exportCodexContentAsHtml(userSelectedPath, filesToExport, options)); + exportPromises.push(exportCodexContentAsHtml(effectivePath, filesToExport, options)); break; case CodexExportFormat.AUDIO: { const { exportAudioAttachments } = await import("./audioExporter"); - exportPromises.push(exportAudioAttachments(userSelectedPath, filesToExport, { includeTimestamps: (options as any)?.includeTimestamps })); + exportPromises.push(exportAudioAttachments(effectivePath, filesToExport, { includeTimestamps: (options as any)?.includeTimestamps })); break; } case CodexExportFormat.SUBTITLES_VTT_WITH_STYLES: - exportPromises.push(exportCodexContentAsSubtitlesVtt(userSelectedPath, filesToExport, options, true)); + exportPromises.push(exportCodexContentAsSubtitlesVtt(effectivePath, filesToExport, options, true)); break; case CodexExportFormat.SUBTITLES_VTT_WITHOUT_STYLES: - exportPromises.push(exportCodexContentAsSubtitlesVtt(userSelectedPath, filesToExport, options, false)); + exportPromises.push(exportCodexContentAsSubtitlesVtt(effectivePath, filesToExport, options, false)); break; case CodexExportFormat.SUBTITLES_SRT: - exportPromises.push(exportCodexContentAsSubtitlesSrt(userSelectedPath, filesToExport, options)); + exportPromises.push(exportCodexContentAsSubtitlesSrt(effectivePath, filesToExport, options)); break; case CodexExportFormat.XLIFF: - exportPromises.push(exportCodexContentAsXliff(userSelectedPath, filesToExport, options)); + exportPromises.push(exportCodexContentAsXliff(effectivePath, filesToExport, options)); break; case CodexExportFormat.CSV: - exportPromises.push(exportCodexContentAsCsv(userSelectedPath, filesToExport, options)); + exportPromises.push(exportCodexContentAsCsv(effectivePath, filesToExport, options)); break; case CodexExportFormat.TSV: - exportPromises.push(exportCodexContentAsTsv(userSelectedPath, filesToExport, options)); + exportPromises.push(exportCodexContentAsTsv(effectivePath, filesToExport, options)); break; case CodexExportFormat.REBUILD_EXPORT: - exportPromises.push(exportCodexContentAsRebuild(userSelectedPath, filesToExport, options)); + exportPromises.push(exportCodexContentAsRebuild(effectivePath, filesToExport, options)); break; case CodexExportFormat.BACKTRANSLATIONS: - exportPromises.push(exportCodexContentAsBacktranslations(userSelectedPath, filesToExport, options)); + exportPromises.push(exportCodexContentAsBacktranslations(effectivePath, filesToExport, options)); break; } @@ -1660,7 +1684,7 @@ export async function exportCodexContent( if (includeAudio) { const { exportAudioAttachments } = await import("./audioExporter"); exportPromises.push( - exportAudioAttachments(userSelectedPath, filesToExport, { + exportAudioAttachments(effectivePath, filesToExport, { includeTimestamps: (options as any)?.includeTimestamps }) ); @@ -1668,6 +1692,14 @@ export async function exportCodexContent( // Execute all exports in parallel await Promise.all(exportPromises); + + // Zip the subfolder if requested, then remove it + if (shouldZip && !isSelfManaged && effectivePath !== userSelectedPath) { + const zipPath = `${effectivePath}.zip`; + await zipDirectory(effectivePath, zipPath); + fs.rmSync(effectivePath, { recursive: true, force: true }); + vscode.window.showInformationMessage(`Exported to ${zipPath}`); + } } // Compact helpers for id handling and lookups diff --git a/src/exportHandler/utils/zipUtils.ts b/src/exportHandler/utils/zipUtils.ts new file mode 100644 index 000000000..10730f95e --- /dev/null +++ b/src/exportHandler/utils/zipUtils.ts @@ -0,0 +1,17 @@ +import archiver from "archiver"; +import * as fs from "fs"; +import { basename } from "path"; + +export const zipDirectory = (sourceDir: string, destZipPath: string): Promise => { + return new Promise((resolve, reject) => { + const output = fs.createWriteStream(destZipPath); + const archive = archiver("zip", { zlib: { level: 9 } }); + + output.on("close", resolve); + archive.on("error", reject); + + archive.pipe(output); + archive.directory(sourceDir, basename(sourceDir)); + archive.finalize(); + }); +}; diff --git a/src/projectManager/projectExportView.ts b/src/projectManager/projectExportView.ts index b2347afd0..20df280e4 100644 --- a/src/projectManager/projectExportView.ts +++ b/src/projectManager/projectExportView.ts @@ -505,6 +505,16 @@ function getWebviewContent( Select Location +
+
+ + +
+
+ + +
+
` @@ -575,6 +585,7 @@ function getWebviewContent( if (headerCb) headerCb.checked = !allChecked; updateSelectedGroup(); updateStep1Button(); + autoCheckWrapIfNeeded(); } function onGroupCheckboxChange(groupKey) { @@ -591,6 +602,7 @@ function getWebviewContent( }); updateSelectedGroup(); updateStep1Button(); + autoCheckWrapIfNeeded(); } function onFileCheckboxChange() { @@ -607,6 +619,7 @@ function getWebviewContent( }); updateSelectedGroup(); updateStep1Button(); + autoCheckWrapIfNeeded(); } function updateSelectedGroup() { @@ -756,8 +769,7 @@ function getWebviewContent( option.style.borderColor = ''; } selectedAudio = willSelect; - // Refresh button states immediately when audio toggled - try { updateStep2Button(); updateExportButton(); } catch (e) {} + try { updateStep2Button(); updateExportButton(); updateOutputOptionsVisibility(); autoCheckWrapIfNeeded(); } catch (e) {} return; } @@ -765,10 +777,11 @@ function getWebviewContent( if (option.classList.contains('selected')) { option.classList.remove('selected'); selectedFormat = null; - // hide any USFM-specific options const usfmOptions = document.getElementById('usfmOptions'); if (usfmOptions) usfmOptions.style.display = 'none'; updateStep2Button(); + updateOutputOptionsVisibility(); + autoCheckWrapIfNeeded(); return; } @@ -784,6 +797,8 @@ function getWebviewContent( const usfmOptions = document.getElementById('usfmOptions'); if (usfmOptions) usfmOptions.style.display = selectedFormat === 'usfm' ? 'block' : 'none'; updateStep2Button(); + updateOutputOptionsVisibility(); + autoCheckWrapIfNeeded(); }); }); @@ -793,6 +808,47 @@ function getWebviewContent( }); + const selfManagedFormats = ['html', 'audio', 'rebuild-export']; + + function isSelfManagedFormat() { + const fmt = selectedFormat || (selectedAudio ? 'audio' : null); + return fmt && selfManagedFormats.includes(fmt); + } + + function updateOutputOptionsVisibility() { + const container = document.getElementById('exportOutputOptions'); + if (container) { + container.style.display = isSelfManagedFormat() ? 'none' : ''; + } + } + + function autoCheckWrapIfNeeded() { + if (isSelfManagedFormat()) return; + const wrapCb = document.getElementById('wrapInFolder'); + const zipCb = document.getElementById('zipOutput'); + if (!wrapCb || zipCb?.checked) return; + wrapCb.checked = selectedFiles.size >= 3; + } + + function onWrapInFolderChange() { + const wrapCb = document.getElementById('wrapInFolder'); + const zipCb = document.getElementById('zipOutput'); + if (!wrapCb?.checked && zipCb?.checked) { + zipCb.checked = false; + } + } + + function onZipOutputChange() { + const wrapCb = document.getElementById('wrapInFolder'); + const zipCb = document.getElementById('zipOutput'); + if (zipCb?.checked && wrapCb) { + wrapCb.checked = true; + wrapCb.disabled = true; + } else if (wrapCb) { + wrapCb.disabled = false; + } + } + function exportProject() { const formatToSend = selectedFormat || (selectedAudio ? 'audio' : null); if (!formatToSend || !exportPath || selectedFiles.size === 0) return; @@ -801,6 +857,8 @@ function getWebviewContent( // Audio is now a separate toggle that may be combined with other export formats if (selectedAudio) options.includeAudio = true; if (selectedAudio) options.includeTimestamps = document.getElementById('audioIncludeTimestamps')?.checked; + if (document.getElementById('wrapInFolder')?.checked) options.wrapInFolder = true; + if (document.getElementById('zipOutput')?.checked) options.zipOutput = true; vscode.postMessage({ command: 'export', format: formatToSend, From d5e457e236083c474befa8b50fdec4ea2fec7949 Mon Sep 17 00:00:00 2001 From: Luke-Bilhorn Date: Sat, 28 Feb 2026 21:28:29 -0600 Subject: [PATCH 02/14] Include HTML/audio in wrap/zip, auto-wrap on 3+ files, avoid name collisions --- src/exportHandler/exportHandler.ts | 12 ++++++++---- src/projectManager/projectExportView.ts | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/exportHandler/exportHandler.ts b/src/exportHandler/exportHandler.ts index 42d83b455..090d3192b 100644 --- a/src/exportHandler/exportHandler.ts +++ b/src/exportHandler/exportHandler.ts @@ -1615,8 +1615,6 @@ export async function exportCodexContent( const shouldZip = options?.zipOutput ?? false; const selfManagedFormats: CodexExportFormat[] = [ - CodexExportFormat.HTML, - CodexExportFormat.AUDIO, CodexExportFormat.REBUILD_EXPORT, ]; const isSelfManaged = selfManagedFormats.includes(format); @@ -1628,8 +1626,14 @@ export async function exportCodexContent( const projectName = projectConfig.get("projectName", "") || basename(vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? "export"); const dateStamp = new Date().toISOString().slice(0, 10); - const subfolderName = `${projectName}-${format}-${dateStamp}`; - effectivePath = path.join(userSelectedPath, subfolderName); + const baseName = `${projectName}-${format}-${dateStamp}`; + let candidate = path.join(userSelectedPath, baseName); + let suffix = 1; + while (fs.existsSync(candidate) || fs.existsSync(`${candidate}.zip`)) { + candidate = path.join(userSelectedPath, `${baseName}-${suffix}`); + suffix++; + } + effectivePath = candidate; } // Check if audio export should also be included alongside the text format export diff --git a/src/projectManager/projectExportView.ts b/src/projectManager/projectExportView.ts index 20df280e4..cf7f331b5 100644 --- a/src/projectManager/projectExportView.ts +++ b/src/projectManager/projectExportView.ts @@ -808,7 +808,7 @@ function getWebviewContent( }); - const selfManagedFormats = ['html', 'audio', 'rebuild-export']; + const selfManagedFormats = ['rebuild-export']; function isSelfManagedFormat() { const fmt = selectedFormat || (selectedAudio ? 'audio' : null); From 340552b4ff61ce25db8a3efac1b769d8ea30099a Mon Sep 17 00:00:00 2001 From: Luke-Bilhorn Date: Mon, 2 Mar 2026 17:17:02 -0600 Subject: [PATCH 03/14] - Updated to use JSzip and - Removed uses of Archiver, - Updated to include audiofiles. --- .../contextAware/commands.ts | 4 +-- src/exportHandler/exportHandler.ts | 9 ++--- src/exportHandler/utils/zipUtils.ts | 35 ++++++++++++------- 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/activationHelpers/contextAware/commands.ts b/src/activationHelpers/contextAware/commands.ts index a12c95a3a..990b6241f 100644 --- a/src/activationHelpers/contextAware/commands.ts +++ b/src/activationHelpers/contextAware/commands.ts @@ -10,7 +10,7 @@ import { } from "../../utils/codexNotebookUtils"; import { jumpToCellInNotebook } from "../../utils"; import { setTargetFont } from "../../projectManager/projectInitializers"; -import { CodexExportFormat, exportCodexContent } from "../../exportHandler/exportHandler"; +import { CodexExportFormat, exportCodexContent, type ExportOptions } from "../../exportHandler/exportHandler"; import { createEditAnalysisProvider } from "../../providers/EditAnalysisView/EditAnalysisViewProvider"; @@ -158,7 +158,7 @@ export async function registerCommands(context: vscode.ExtensionContext) { format: CodexExportFormat; userSelectedPath: string; filesToExport: string[]; - options?: { skipValidation?: boolean; removeIds?: boolean; }; + options?: ExportOptions; }) => { await exportCodexContent(format, userSelectedPath, filesToExport, options); } diff --git a/src/exportHandler/exportHandler.ts b/src/exportHandler/exportHandler.ts index 090d3192b..d19667683 100644 --- a/src/exportHandler/exportHandler.ts +++ b/src/exportHandler/exportHandler.ts @@ -1,5 +1,4 @@ import * as vscode from "vscode"; -import JSZip from "jszip"; import { CodexCellTypes } from "../../types/enums"; import { basename } from "path"; import * as path from "path"; @@ -218,6 +217,8 @@ export interface ExportOptions { removeIds?: boolean; wrapInFolder?: boolean; zipOutput?: boolean; + includeAudio?: boolean; + includeTimestamps?: boolean; } // IDML Round-trip export: Uses idmlExporter or biblicaExporter based on filename @@ -1637,7 +1638,7 @@ export async function exportCodexContent( } // Check if audio export should also be included alongside the text format export - const includeAudio = (options as any)?.includeAudio === true && format !== CodexExportFormat.AUDIO; + const includeAudio = options?.includeAudio === true && format !== CodexExportFormat.AUDIO; // Prepare export promises const exportPromises: Promise[] = []; @@ -1655,7 +1656,7 @@ export async function exportCodexContent( break; case CodexExportFormat.AUDIO: { const { exportAudioAttachments } = await import("./audioExporter"); - exportPromises.push(exportAudioAttachments(effectivePath, filesToExport, { includeTimestamps: (options as any)?.includeTimestamps })); + exportPromises.push(exportAudioAttachments(effectivePath, filesToExport, { includeTimestamps: options?.includeTimestamps })); break; } case CodexExportFormat.SUBTITLES_VTT_WITH_STYLES: @@ -1689,7 +1690,7 @@ export async function exportCodexContent( const { exportAudioAttachments } = await import("./audioExporter"); exportPromises.push( exportAudioAttachments(effectivePath, filesToExport, { - includeTimestamps: (options as any)?.includeTimestamps + includeTimestamps: options?.includeTimestamps }) ); } diff --git a/src/exportHandler/utils/zipUtils.ts b/src/exportHandler/utils/zipUtils.ts index 10730f95e..9f48c7422 100644 --- a/src/exportHandler/utils/zipUtils.ts +++ b/src/exportHandler/utils/zipUtils.ts @@ -1,17 +1,28 @@ -import archiver from "archiver"; +import JSZip from "jszip"; import * as fs from "fs"; -import { basename } from "path"; +import * as path from "path"; -export const zipDirectory = (sourceDir: string, destZipPath: string): Promise => { - return new Promise((resolve, reject) => { - const output = fs.createWriteStream(destZipPath); - const archive = archiver("zip", { zlib: { level: 9 } }); +function addDirToZip(zip: JSZip, dirPath: string, zipRoot: string): void { + const entries = fs.readdirSync(dirPath, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dirPath, entry.name); + const zipPath = path.join(zipRoot, entry.name).replace(/\\/g, "/"); + if (entry.isDirectory()) { + addDirToZip(zip, fullPath, zipPath); + } else { + zip.file(zipPath, fs.readFileSync(fullPath)); + } + } +} - output.on("close", resolve); - archive.on("error", reject); - - archive.pipe(output); - archive.directory(sourceDir, basename(sourceDir)); - archive.finalize(); +export const zipDirectory = async (sourceDir: string, destZipPath: string): Promise => { + const zip = new JSZip(); + const rootName = path.basename(sourceDir); + addDirToZip(zip, sourceDir, rootName); + const buffer = await zip.generateAsync({ + type: "nodebuffer", + compression: "DEFLATE", + compressionOptions: { level: 9 }, }); + await fs.promises.writeFile(destZipPath, buffer); }; From 85995df737e0c61f42319679adee78c1ee0c94be Mon Sep 17 00:00:00 2001 From: Luke-Bilhorn Date: Mon, 23 Mar 2026 15:18:32 -0500 Subject: [PATCH 04/14] Revert "- Updated to use JSzip and - Removed uses of Archiver, - Updated to include audiofiles." This reverts commit 3038f560f4f1881be400f3c0596259670abc5f90. --- .../contextAware/commands.ts | 4 +-- src/exportHandler/exportHandler.ts | 9 +++-- src/exportHandler/utils/zipUtils.ts | 35 +++++++------------ 3 files changed, 18 insertions(+), 30 deletions(-) diff --git a/src/activationHelpers/contextAware/commands.ts b/src/activationHelpers/contextAware/commands.ts index 990b6241f..a12c95a3a 100644 --- a/src/activationHelpers/contextAware/commands.ts +++ b/src/activationHelpers/contextAware/commands.ts @@ -10,7 +10,7 @@ import { } from "../../utils/codexNotebookUtils"; import { jumpToCellInNotebook } from "../../utils"; import { setTargetFont } from "../../projectManager/projectInitializers"; -import { CodexExportFormat, exportCodexContent, type ExportOptions } from "../../exportHandler/exportHandler"; +import { CodexExportFormat, exportCodexContent } from "../../exportHandler/exportHandler"; import { createEditAnalysisProvider } from "../../providers/EditAnalysisView/EditAnalysisViewProvider"; @@ -158,7 +158,7 @@ export async function registerCommands(context: vscode.ExtensionContext) { format: CodexExportFormat; userSelectedPath: string; filesToExport: string[]; - options?: ExportOptions; + options?: { skipValidation?: boolean; removeIds?: boolean; }; }) => { await exportCodexContent(format, userSelectedPath, filesToExport, options); } diff --git a/src/exportHandler/exportHandler.ts b/src/exportHandler/exportHandler.ts index d19667683..090d3192b 100644 --- a/src/exportHandler/exportHandler.ts +++ b/src/exportHandler/exportHandler.ts @@ -1,4 +1,5 @@ import * as vscode from "vscode"; +import JSZip from "jszip"; import { CodexCellTypes } from "../../types/enums"; import { basename } from "path"; import * as path from "path"; @@ -217,8 +218,6 @@ export interface ExportOptions { removeIds?: boolean; wrapInFolder?: boolean; zipOutput?: boolean; - includeAudio?: boolean; - includeTimestamps?: boolean; } // IDML Round-trip export: Uses idmlExporter or biblicaExporter based on filename @@ -1638,7 +1637,7 @@ export async function exportCodexContent( } // Check if audio export should also be included alongside the text format export - const includeAudio = options?.includeAudio === true && format !== CodexExportFormat.AUDIO; + const includeAudio = (options as any)?.includeAudio === true && format !== CodexExportFormat.AUDIO; // Prepare export promises const exportPromises: Promise[] = []; @@ -1656,7 +1655,7 @@ export async function exportCodexContent( break; case CodexExportFormat.AUDIO: { const { exportAudioAttachments } = await import("./audioExporter"); - exportPromises.push(exportAudioAttachments(effectivePath, filesToExport, { includeTimestamps: options?.includeTimestamps })); + exportPromises.push(exportAudioAttachments(effectivePath, filesToExport, { includeTimestamps: (options as any)?.includeTimestamps })); break; } case CodexExportFormat.SUBTITLES_VTT_WITH_STYLES: @@ -1690,7 +1689,7 @@ export async function exportCodexContent( const { exportAudioAttachments } = await import("./audioExporter"); exportPromises.push( exportAudioAttachments(effectivePath, filesToExport, { - includeTimestamps: options?.includeTimestamps + includeTimestamps: (options as any)?.includeTimestamps }) ); } diff --git a/src/exportHandler/utils/zipUtils.ts b/src/exportHandler/utils/zipUtils.ts index 9f48c7422..10730f95e 100644 --- a/src/exportHandler/utils/zipUtils.ts +++ b/src/exportHandler/utils/zipUtils.ts @@ -1,28 +1,17 @@ -import JSZip from "jszip"; +import archiver from "archiver"; import * as fs from "fs"; -import * as path from "path"; +import { basename } from "path"; -function addDirToZip(zip: JSZip, dirPath: string, zipRoot: string): void { - const entries = fs.readdirSync(dirPath, { withFileTypes: true }); - for (const entry of entries) { - const fullPath = path.join(dirPath, entry.name); - const zipPath = path.join(zipRoot, entry.name).replace(/\\/g, "/"); - if (entry.isDirectory()) { - addDirToZip(zip, fullPath, zipPath); - } else { - zip.file(zipPath, fs.readFileSync(fullPath)); - } - } -} +export const zipDirectory = (sourceDir: string, destZipPath: string): Promise => { + return new Promise((resolve, reject) => { + const output = fs.createWriteStream(destZipPath); + const archive = archiver("zip", { zlib: { level: 9 } }); -export const zipDirectory = async (sourceDir: string, destZipPath: string): Promise => { - const zip = new JSZip(); - const rootName = path.basename(sourceDir); - addDirToZip(zip, sourceDir, rootName); - const buffer = await zip.generateAsync({ - type: "nodebuffer", - compression: "DEFLATE", - compressionOptions: { level: 9 }, + output.on("close", resolve); + archive.on("error", reject); + + archive.pipe(output); + archive.directory(sourceDir, basename(sourceDir)); + archive.finalize(); }); - await fs.promises.writeFile(destZipPath, buffer); }; From e687addd9d3721a64a32b06bbcc1b93048dd7d71 Mon Sep 17 00:00:00 2001 From: Luke-Bilhorn Date: Tue, 24 Mar 2026 12:50:08 -0500 Subject: [PATCH 05/14] Audio is included in exports. --- src/activationHelpers/contextAware/commands.ts | 4 ++-- src/exportHandler/exportHandler.ts | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/activationHelpers/contextAware/commands.ts b/src/activationHelpers/contextAware/commands.ts index a12c95a3a..990b6241f 100644 --- a/src/activationHelpers/contextAware/commands.ts +++ b/src/activationHelpers/contextAware/commands.ts @@ -10,7 +10,7 @@ import { } from "../../utils/codexNotebookUtils"; import { jumpToCellInNotebook } from "../../utils"; import { setTargetFont } from "../../projectManager/projectInitializers"; -import { CodexExportFormat, exportCodexContent } from "../../exportHandler/exportHandler"; +import { CodexExportFormat, exportCodexContent, type ExportOptions } from "../../exportHandler/exportHandler"; import { createEditAnalysisProvider } from "../../providers/EditAnalysisView/EditAnalysisViewProvider"; @@ -158,7 +158,7 @@ export async function registerCommands(context: vscode.ExtensionContext) { format: CodexExportFormat; userSelectedPath: string; filesToExport: string[]; - options?: { skipValidation?: boolean; removeIds?: boolean; }; + options?: ExportOptions; }) => { await exportCodexContent(format, userSelectedPath, filesToExport, options); } diff --git a/src/exportHandler/exportHandler.ts b/src/exportHandler/exportHandler.ts index 090d3192b..d19667683 100644 --- a/src/exportHandler/exportHandler.ts +++ b/src/exportHandler/exportHandler.ts @@ -1,5 +1,4 @@ import * as vscode from "vscode"; -import JSZip from "jszip"; import { CodexCellTypes } from "../../types/enums"; import { basename } from "path"; import * as path from "path"; @@ -218,6 +217,8 @@ export interface ExportOptions { removeIds?: boolean; wrapInFolder?: boolean; zipOutput?: boolean; + includeAudio?: boolean; + includeTimestamps?: boolean; } // IDML Round-trip export: Uses idmlExporter or biblicaExporter based on filename @@ -1637,7 +1638,7 @@ export async function exportCodexContent( } // Check if audio export should also be included alongside the text format export - const includeAudio = (options as any)?.includeAudio === true && format !== CodexExportFormat.AUDIO; + const includeAudio = options?.includeAudio === true && format !== CodexExportFormat.AUDIO; // Prepare export promises const exportPromises: Promise[] = []; @@ -1655,7 +1656,7 @@ export async function exportCodexContent( break; case CodexExportFormat.AUDIO: { const { exportAudioAttachments } = await import("./audioExporter"); - exportPromises.push(exportAudioAttachments(effectivePath, filesToExport, { includeTimestamps: (options as any)?.includeTimestamps })); + exportPromises.push(exportAudioAttachments(effectivePath, filesToExport, { includeTimestamps: options?.includeTimestamps })); break; } case CodexExportFormat.SUBTITLES_VTT_WITH_STYLES: @@ -1689,7 +1690,7 @@ export async function exportCodexContent( const { exportAudioAttachments } = await import("./audioExporter"); exportPromises.push( exportAudioAttachments(effectivePath, filesToExport, { - includeTimestamps: (options as any)?.includeTimestamps + includeTimestamps: options?.includeTimestamps }) ); } From 4c8545309091fabf7139d0a56b246c2496c07ff8 Mon Sep 17 00:00:00 2001 From: Luke-Bilhorn Date: Tue, 24 Mar 2026 14:09:22 -0500 Subject: [PATCH 06/14] feat(export): always wrap output; simplify output folder structure. Now, when downloading audio with text, it has type "multi" instead of "HTML" or "plaintext" or "wav", and it has "audio" and "html" folders inside, for example. Now, downloads are always wrapped in a new folder but the option to zip remains. --- src/exportHandler/audioExporter.ts | 22 ++----- src/exportHandler/exportHandler.ts | 83 +++++++++++-------------- src/projectManager/projectExportView.ts | 57 +---------------- 3 files changed, 42 insertions(+), 120 deletions(-) diff --git a/src/exportHandler/audioExporter.ts b/src/exportHandler/audioExporter.ts index ef56e7850..72a9d23bf 100644 --- a/src/exportHandler/audioExporter.ts +++ b/src/exportHandler/audioExporter.ts @@ -28,11 +28,6 @@ function sanitizeFileComponent(input: string): string { .replace(/_+/g, "_"); } -function formatDateForFolder(d: Date): string { - const pad = (n: number, w = 2) => String(n).padStart(w, "0"); - return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`; -} - // REMOVE: This doesn't seem to be used anywhere /** * Parses a cell reference ID (from globalReferences) to extract book, chapter, and verse. @@ -481,17 +476,8 @@ export async function exportAudioAttachments( } const workspaceFolder = workspaceFolders[0]; - // Resolve project name - const projectConfig = vscode.workspace.getConfiguration("codex-project-manager"); - let projectName = projectConfig.get("projectName", ""); - if (!projectName) { - projectName = basename(workspaceFolder.uri.fsPath); - } - - const dateStamp = formatDateForFolder(new Date()); - const exportRoot = vscode.Uri.file(userSelectedPath); - const finalExportDir = vscode.Uri.joinPath(exportRoot, "export", `${sanitizeFileComponent(projectName)}-${dateStamp}`); - await vscode.workspace.fs.createDirectory(finalExportDir); + const exportDir = vscode.Uri.file(userSelectedPath); + await vscode.workspace.fs.createDirectory(exportDir); const includeTimestamps = !!options?.includeTimestamps; const selectedFiles = filesToExport.map((p) => vscode.Uri.file(p)); @@ -516,7 +502,7 @@ export async function exportAudioAttachments( progress.report({ message: `Processing ${basename(file.fsPath)} (${index + 1}/${selectedFiles.length})`, increment }); const bookCode = basename(file.fsPath).split(".")[0] || "BOOK"; - const bookFolder = vscode.Uri.joinPath(finalExportDir, sanitizeFileComponent(bookCode)); + const bookFolder = vscode.Uri.joinPath(exportDir, sanitizeFileComponent(bookCode)); await vscode.workspace.fs.createDirectory(bookFolder); let notebook: CodexNotebookAsJSONData; @@ -629,7 +615,7 @@ export async function exportAudioAttachments( } debug(`Export summary: ${copiedCount} files copied, ${missingCount} skipped`); - vscode.window.showInformationMessage(`Audio export completed: ${copiedCount} files copied${missingCount ? `, ${missingCount} skipped` : ""}. Output: ${finalExportDir.fsPath}`); + vscode.window.showInformationMessage(`Audio export completed: ${copiedCount} files copied${missingCount ? `, ${missingCount} skipped` : ""}. Output: ${exportDir.fsPath}`); } ); } diff --git a/src/exportHandler/exportHandler.ts b/src/exportHandler/exportHandler.ts index d19667683..0a791bbc1 100644 --- a/src/exportHandler/exportHandler.ts +++ b/src/exportHandler/exportHandler.ts @@ -215,7 +215,6 @@ export enum CodexExportFormat { export interface ExportOptions { skipValidation?: boolean; removeIds?: boolean; - wrapInFolder?: boolean; zipOutput?: boolean; includeAudio?: boolean; includeTimestamps?: boolean; @@ -1612,97 +1611,87 @@ export async function exportCodexContent( filesToExport: string[], options?: ExportOptions ) { - const shouldWrap = options?.wrapInFolder ?? false; const shouldZip = options?.zipOutput ?? false; - - const selfManagedFormats: CodexExportFormat[] = [ - CodexExportFormat.REBUILD_EXPORT, - ]; - const isSelfManaged = selfManagedFormats.includes(format); - - let effectivePath = userSelectedPath; - - if ((shouldWrap || shouldZip) && !isSelfManaged) { - const projectConfig = vscode.workspace.getConfiguration("codex-project-manager"); - const projectName = projectConfig.get("projectName", "") || - basename(vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? "export"); - const dateStamp = new Date().toISOString().slice(0, 10); - const baseName = `${projectName}-${format}-${dateStamp}`; - let candidate = path.join(userSelectedPath, baseName); - let suffix = 1; - while (fs.existsSync(candidate) || fs.existsSync(`${candidate}.zip`)) { - candidate = path.join(userSelectedPath, `${baseName}-${suffix}`); - suffix++; - } - effectivePath = candidate; + const includeAudio = options?.includeAudio === true && format !== CodexExportFormat.AUDIO; + const isMulti = includeAudio; + + // Always create a wrapper folder + const projectConfig = vscode.workspace.getConfiguration("codex-project-manager"); + const projectName = projectConfig.get("projectName", "") || + basename(vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? "export"); + const dateStamp = new Date().toISOString().slice(0, 10); + const formatLabel = isMulti ? "multi" : format; + const baseName = `${projectName}-${formatLabel}-${dateStamp}`; + let candidate = path.join(userSelectedPath, baseName); + let suffix = 1; + while (fs.existsSync(candidate) || fs.existsSync(`${candidate}.zip`)) { + candidate = path.join(userSelectedPath, `${baseName}-${suffix}`); + suffix++; } + const wrapperPath = candidate; - // Check if audio export should also be included alongside the text format export - const includeAudio = options?.includeAudio === true && format !== CodexExportFormat.AUDIO; + // In multi mode, each export type gets its own subfolder + const formatPath = isMulti ? path.join(wrapperPath, format) : wrapperPath; + const audioPath = isMulti ? path.join(wrapperPath, "audio") : wrapperPath; - // Prepare export promises const exportPromises: Promise[] = []; - // Add text format export switch (format) { case CodexExportFormat.PLAINTEXT: - exportPromises.push(exportCodexContentAsPlaintext(effectivePath, filesToExport, options)); + exportPromises.push(exportCodexContentAsPlaintext(formatPath, filesToExport, options)); break; case CodexExportFormat.USFM: - exportPromises.push(exportCodexContentAsUsfm(effectivePath, filesToExport, options)); + exportPromises.push(exportCodexContentAsUsfm(formatPath, filesToExport, options)); break; case CodexExportFormat.HTML: - exportPromises.push(exportCodexContentAsHtml(effectivePath, filesToExport, options)); + exportPromises.push(exportCodexContentAsHtml(formatPath, filesToExport, options)); break; case CodexExportFormat.AUDIO: { const { exportAudioAttachments } = await import("./audioExporter"); - exportPromises.push(exportAudioAttachments(effectivePath, filesToExport, { includeTimestamps: options?.includeTimestamps })); + exportPromises.push(exportAudioAttachments(wrapperPath, filesToExport, { includeTimestamps: options?.includeTimestamps })); break; } case CodexExportFormat.SUBTITLES_VTT_WITH_STYLES: - exportPromises.push(exportCodexContentAsSubtitlesVtt(effectivePath, filesToExport, options, true)); + exportPromises.push(exportCodexContentAsSubtitlesVtt(formatPath, filesToExport, options, true)); break; case CodexExportFormat.SUBTITLES_VTT_WITHOUT_STYLES: - exportPromises.push(exportCodexContentAsSubtitlesVtt(effectivePath, filesToExport, options, false)); + exportPromises.push(exportCodexContentAsSubtitlesVtt(formatPath, filesToExport, options, false)); break; case CodexExportFormat.SUBTITLES_SRT: - exportPromises.push(exportCodexContentAsSubtitlesSrt(effectivePath, filesToExport, options)); + exportPromises.push(exportCodexContentAsSubtitlesSrt(formatPath, filesToExport, options)); break; case CodexExportFormat.XLIFF: - exportPromises.push(exportCodexContentAsXliff(effectivePath, filesToExport, options)); + exportPromises.push(exportCodexContentAsXliff(formatPath, filesToExport, options)); break; case CodexExportFormat.CSV: - exportPromises.push(exportCodexContentAsCsv(effectivePath, filesToExport, options)); + exportPromises.push(exportCodexContentAsCsv(formatPath, filesToExport, options)); break; case CodexExportFormat.TSV: - exportPromises.push(exportCodexContentAsTsv(effectivePath, filesToExport, options)); + exportPromises.push(exportCodexContentAsTsv(formatPath, filesToExport, options)); break; case CodexExportFormat.REBUILD_EXPORT: - exportPromises.push(exportCodexContentAsRebuild(effectivePath, filesToExport, options)); + exportPromises.push(exportCodexContentAsRebuild(formatPath, filesToExport, options)); break; case CodexExportFormat.BACKTRANSLATIONS: - exportPromises.push(exportCodexContentAsBacktranslations(effectivePath, filesToExport, options)); + exportPromises.push(exportCodexContentAsBacktranslations(formatPath, filesToExport, options)); break; } - // Add audio export if requested alongside text format if (includeAudio) { const { exportAudioAttachments } = await import("./audioExporter"); exportPromises.push( - exportAudioAttachments(effectivePath, filesToExport, { + exportAudioAttachments(audioPath, filesToExport, { includeTimestamps: options?.includeTimestamps }) ); } - // Execute all exports in parallel await Promise.all(exportPromises); - // Zip the subfolder if requested, then remove it - if (shouldZip && !isSelfManaged && effectivePath !== userSelectedPath) { - const zipPath = `${effectivePath}.zip`; - await zipDirectory(effectivePath, zipPath); - fs.rmSync(effectivePath, { recursive: true, force: true }); + if (shouldZip) { + const zipPath = `${wrapperPath}.zip`; + await zipDirectory(wrapperPath, zipPath); + fs.rmSync(wrapperPath, { recursive: true, force: true }); vscode.window.showInformationMessage(`Exported to ${zipPath}`); } } diff --git a/src/projectManager/projectExportView.ts b/src/projectManager/projectExportView.ts index cf7f331b5..1e40da7ec 100644 --- a/src/projectManager/projectExportView.ts +++ b/src/projectManager/projectExportView.ts @@ -506,12 +506,8 @@ function getWebviewContent(
-
- - -
- +
@@ -585,7 +581,6 @@ function getWebviewContent( if (headerCb) headerCb.checked = !allChecked; updateSelectedGroup(); updateStep1Button(); - autoCheckWrapIfNeeded(); } function onGroupCheckboxChange(groupKey) { @@ -602,7 +597,6 @@ function getWebviewContent( }); updateSelectedGroup(); updateStep1Button(); - autoCheckWrapIfNeeded(); } function onFileCheckboxChange() { @@ -619,7 +613,6 @@ function getWebviewContent( }); updateSelectedGroup(); updateStep1Button(); - autoCheckWrapIfNeeded(); } function updateSelectedGroup() { @@ -769,7 +762,7 @@ function getWebviewContent( option.style.borderColor = ''; } selectedAudio = willSelect; - try { updateStep2Button(); updateExportButton(); updateOutputOptionsVisibility(); autoCheckWrapIfNeeded(); } catch (e) {} + try { updateStep2Button(); updateExportButton(); } catch (e) {} return; } @@ -780,8 +773,6 @@ function getWebviewContent( const usfmOptions = document.getElementById('usfmOptions'); if (usfmOptions) usfmOptions.style.display = 'none'; updateStep2Button(); - updateOutputOptionsVisibility(); - autoCheckWrapIfNeeded(); return; } @@ -797,8 +788,6 @@ function getWebviewContent( const usfmOptions = document.getElementById('usfmOptions'); if (usfmOptions) usfmOptions.style.display = selectedFormat === 'usfm' ? 'block' : 'none'; updateStep2Button(); - updateOutputOptionsVisibility(); - autoCheckWrapIfNeeded(); }); }); @@ -808,47 +797,6 @@ function getWebviewContent( }); - const selfManagedFormats = ['rebuild-export']; - - function isSelfManagedFormat() { - const fmt = selectedFormat || (selectedAudio ? 'audio' : null); - return fmt && selfManagedFormats.includes(fmt); - } - - function updateOutputOptionsVisibility() { - const container = document.getElementById('exportOutputOptions'); - if (container) { - container.style.display = isSelfManagedFormat() ? 'none' : ''; - } - } - - function autoCheckWrapIfNeeded() { - if (isSelfManagedFormat()) return; - const wrapCb = document.getElementById('wrapInFolder'); - const zipCb = document.getElementById('zipOutput'); - if (!wrapCb || zipCb?.checked) return; - wrapCb.checked = selectedFiles.size >= 3; - } - - function onWrapInFolderChange() { - const wrapCb = document.getElementById('wrapInFolder'); - const zipCb = document.getElementById('zipOutput'); - if (!wrapCb?.checked && zipCb?.checked) { - zipCb.checked = false; - } - } - - function onZipOutputChange() { - const wrapCb = document.getElementById('wrapInFolder'); - const zipCb = document.getElementById('zipOutput'); - if (zipCb?.checked && wrapCb) { - wrapCb.checked = true; - wrapCb.disabled = true; - } else if (wrapCb) { - wrapCb.disabled = false; - } - } - function exportProject() { const formatToSend = selectedFormat || (selectedAudio ? 'audio' : null); if (!formatToSend || !exportPath || selectedFiles.size === 0) return; @@ -857,7 +805,6 @@ function getWebviewContent( // Audio is now a separate toggle that may be combined with other export formats if (selectedAudio) options.includeAudio = true; if (selectedAudio) options.includeTimestamps = document.getElementById('audioIncludeTimestamps')?.checked; - if (document.getElementById('wrapInFolder')?.checked) options.wrapInFolder = true; if (document.getElementById('zipOutput')?.checked) options.zipOutput = true; vscode.postMessage({ command: 'export', From c9592b2e1777607ed8e1b81ec504649a4cd2ccc2 Mon Sep 17 00:00:00 2001 From: Luke-Bilhorn Date: Tue, 24 Mar 2026 14:33:02 -0500 Subject: [PATCH 07/14] persist last folder used for downlading exports --- src/projectManager/projectExportView.ts | 41 ++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/src/projectManager/projectExportView.ts b/src/projectManager/projectExportView.ts index 1e40da7ec..5db3e4de8 100644 --- a/src/projectManager/projectExportView.ts +++ b/src/projectManager/projectExportView.ts @@ -1,4 +1,5 @@ import { CodexExportFormat } from "../exportHandler/exportHandler"; +import * as fs from "fs"; import * as vscode from "vscode"; import { safePostMessageToPanel } from "../utils/webviewUtils"; import { @@ -7,6 +8,26 @@ import { type FileGroup, } from "./utils/exportViewUtils"; +const LAST_EXPORT_FOLDER_KEY = "projectExport.lastFolder"; + +function getLastExportFolderUri(context: vscode.ExtensionContext): vscode.Uri | undefined { + const lastPath = context.workspaceState.get(LAST_EXPORT_FOLDER_KEY); + if (!lastPath) { + return undefined; + } + try { + if (!fs.existsSync(lastPath)) { + return undefined; + } + if (!fs.statSync(lastPath).isDirectory()) { + return undefined; + } + return vscode.Uri.file(lastPath); + } catch { + return undefined; + } +} + export async function openProjectExportView(context: vscode.ExtensionContext) { const panel = vscode.window.createWebviewPanel( "projectExportView", @@ -35,27 +56,32 @@ export async function openProjectExportView(context: vscode.ExtensionContext) { const codexFiles = await vscode.workspace.findFiles("**/*.codex"); const fileGroups = await groupCodexFilesByImporterType(codexFiles); + const initialExportFolder = getLastExportFolderUri(context)?.fsPath ?? null; panel.webview.html = getWebviewContent( sourceLanguage, targetLanguage, codiconsUri, - fileGroups + fileGroups, + initialExportFolder ); panel.webview.onDidReceiveMessage(async (message) => { let result: vscode.Uri[] | undefined; switch (message.command) { - case "selectExportPath": + case "selectExportPath": { + const defaultUri = getLastExportFolderUri(context); result = await vscode.window.showOpenDialog({ canSelectFiles: false, canSelectFolders: true, canSelectMany: false, title: "Select Export Location", openLabel: "Select Folder", + ...(defaultUri ? { defaultUri } : {}), }); if (result && result[0]) { + await context.workspaceState.update(LAST_EXPORT_FOLDER_KEY, result[0].fsPath); safePostMessageToPanel( panel, { @@ -66,6 +92,7 @@ export async function openProjectExportView(context: vscode.ExtensionContext) { ); } break; + } case "openProjectSettings": await vscode.commands.executeCommand( "codex-project-manager.openProjectSettings" @@ -107,12 +134,14 @@ function getWebviewContent( sourceLanguage: unknown, targetLanguage: unknown, codiconsUri: vscode.Uri, - fileGroups: FileGroup[] + fileGroups: FileGroup[], + initialExportFolder: string | null ) { const hasLanguages = sourceLanguage && targetLanguage; const groupsJson = JSON.stringify(fileGroups); const exportOptionsConfigJson = JSON.stringify(EXPORT_OPTIONS_BY_FILE_TYPE); + const initialExportFolderJson = JSON.stringify(initialExportFolder); return ` @@ -532,7 +561,7 @@ function getWebviewContent( let currentStep = 1; let selectedFormat = null; let selectedAudio = false; - let exportPath = null; + let exportPath = ${initialExportFolderJson}; let selectedFiles = new Set(); let selectedGroupKey = null; @@ -740,6 +769,10 @@ function getWebviewContent( document.addEventListener('DOMContentLoaded', () => { renderFileGroups(); updateStep1Button(); + if (exportPath) { + const pathEl = document.getElementById('exportPath'); + if (pathEl) pathEl.textContent = exportPath; + } document.querySelectorAll('#step2 .format-option').forEach(option => { option.addEventListener('click', (e) => { From 087876a0c97da6ce92c7fd47a69a156bcc48e40f Mon Sep 17 00:00:00 2001 From: Luke-Bilhorn Date: Tue, 24 Mar 2026 23:39:57 -0500 Subject: [PATCH 08/14] Separated the include timestamps checkbox from the audio export button, making it its own button. I just generaly sat down and cleaned up the export options page and gave it a unified, consistent structure and spacing that should hopefully be less confusing to the user. --- src/projectManager/projectExportView.ts | 189 ++++++++++++------------ 1 file changed, 98 insertions(+), 91 deletions(-) diff --git a/src/projectManager/projectExportView.ts b/src/projectManager/projectExportView.ts index 5db3e4de8..34d0ea65a 100644 --- a/src/projectManager/projectExportView.ts +++ b/src/projectManager/projectExportView.ts @@ -214,7 +214,7 @@ function getWebviewContent( border-radius: 4px; cursor: pointer; display: flex; - align-items: center; + align-items: flex-start; gap: 12px; } .format-option:hover { background-color: var(--vscode-list-hoverBackground); } @@ -298,12 +298,17 @@ function getWebviewContent( background-color: var(--vscode-editor-background); border-top: 1px solid var(--vscode-input-border); } - .format-section-content .format-option { margin-bottom: 8px; padding: 12px; } - .format-section-content .format-option:last-child { margin-bottom: 0; } + .format-section-content { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; + } + .format-section-content .format-option { padding: 12px; } .format-option-row { display: flex; gap: 1rem; } .format-option[data-option].hidden { display: none !important; } .format-section[data-option].hidden { display: none !important; } .format-option-row[data-option].hidden { display: none !important; } + .format-option p, .format-option-content p { line-height: 1.45; margin: 4px 0 0 0; } .format-option-content { display: flex; flex-direction: column; gap: 4px; } .format-tag { display: inline-block; @@ -371,23 +376,43 @@ function getWebviewContent(
-

Audio Export

-
-
- -
- Audio -

Export per-cell audio attachments alongside the selected export format

-
- - +

Select Export Format

+
+ +
+
+ +

Text and Markup Export Options

+
+
+
+
+ Generate Plaintext +

Export as plain text files with minimal formatting

+
+
+
+
+ Generate XLIFF +

Export in XML Localization Interchange File Format (XLIFF) for translation workflows

+ Translation Ready +
+
+
+
+ Generate USFM +

Export in Universal Standard Format Markers

+
+
+
+
+ Generate HTML +

Export as web pages with chapter navigation

+
-
-

Select Export Format

-
- +
@@ -442,43 +467,6 @@ function getWebviewContent(
- -
-
- -
- Generate Plaintext -

Export as plain text files with minimal formatting

-
-
-
- -
- Generate XLIFF -

Export in XML Localization Interchange File Format (XLIFF) for translation workflows

- Translation Ready -
-
-
- -
-
- -
- Generate USFM -

Export in Universal Standard Format Markers

-
-
-
- -
- Generate HTML -

Export as web pages with chapter navigation

-
-
-
- -
@@ -510,6 +498,29 @@ function getWebviewContent(
+

Select Audio Export Format

+
+
+
+ +

Audio Export Options

+
+
+
+
+ Include Audio +

Export per-cell audio attachments alongside the selected export format

+
+
+
+
+ Include Audio with Timestamps +

Export per-cell audio attachments alongside the selected export format, and embed timestamps in audio metadata (WAV, WebM, M4A)

+
+
+
+
+
+
@@ -522,13 +531,6 @@ function getWebviewContent(
-
@@ -829,7 +831,8 @@ function getWebviewContent( option.classList.add('selected'); selectedFormat = option.dataset.format; const usfmOptions = document.getElementById('usfmOptions'); - if (usfmOptions) usfmOptions.style.display = selectedFormat === 'usfm' ? 'block' : 'none'; + const isUsfmVariant = selectedFormat === 'usfm' || selectedFormat === 'usfm-no-validate'; + if (usfmOptions) usfmOptions.style.display = isUsfmVariant ? 'block' : 'none'; updateStep2Button(); }); }); @@ -837,10 +840,13 @@ function getWebviewContent( }); function exportProject() { - const formatToSend = selectedFormat || (selectedAudioMode ? 'audio' : null); + let formatToSend = selectedFormat || (selectedAudioMode ? 'audio' : null); if (!formatToSend || !exportPath || selectedFiles.size === 0) return; const options = {}; - if (formatToSend === 'usfm') options.skipValidation = document.getElementById('skipValidation')?.checked; + if (formatToSend === 'usfm-no-validate') { + formatToSend = 'usfm'; + options.skipValidation = true; + } if (selectedAudioMode) { options.includeAudio = true; options.includeTimestamps = selectedAudioMode === 'audio-timestamps'; From 5206e2b27182db1998d89c87c1b008d1b953a634 Mon Sep 17 00:00:00 2001 From: Luke-Bilhorn Date: Sun, 29 Mar 2026 23:50:28 -0500 Subject: [PATCH 10/14] Persist text export option selection if going steps 2-> 3-> 2 --- src/projectManager/projectExportView.ts | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/projectManager/projectExportView.ts b/src/projectManager/projectExportView.ts index 50b1ca950..c40deb496 100644 --- a/src/projectManager/projectExportView.ts +++ b/src/projectManager/projectExportView.ts @@ -675,7 +675,7 @@ function getWebviewContent( if (btn) btn.disabled = selectedFiles.size === 0; } - function initStep2Options() { + function initStep2Options(resetFormatSelection) { const key = selectedGroupKey || 'unknown'; const show = (option) => { const allowed = exportOptionsConfig[option]; @@ -687,14 +687,18 @@ function getWebviewContent( const visible = show(opt); el.classList.toggle('hidden', !visible); }); - // Visibility for HTML/USFM is controlled by data-option and exportOptionsConfig - selectedFormat = null; - // Clear previous selected state for format options but keep audio selection intact - document.querySelectorAll('#step2 .format-option:not(.audio-option)').forEach(opt => { - opt.classList.remove('selected'); - opt.style.backgroundColor = ''; - opt.style.borderColor = ''; - }); + // Only clear text format when entering step 2 from step 1 (file group may have changed). + // When returning from step 3, keep the user's format choice; audio already behaved this way. + if (resetFormatSelection) { + selectedFormat = null; + document.querySelectorAll('#step2 .format-option:not(.audio-option)').forEach(opt => { + opt.classList.remove('selected'); + opt.style.backgroundColor = ''; + opt.style.borderColor = ''; + }); + const usfmOptionsEl = document.getElementById('usfmOptions'); + if (usfmOptionsEl) usfmOptionsEl.style.display = 'none'; + } updateStep2Button(); } @@ -722,6 +726,7 @@ function getWebviewContent( } function goToStep(n) { + const prevStep = currentStep; document.querySelectorAll('.step-panel').forEach(p => p.classList.remove('active')); document.getElementById('step' + n).classList.add('active'); document.querySelectorAll('.step-dot').forEach((dot, i) => { @@ -732,7 +737,7 @@ function getWebviewContent( currentStep = n; updateButtonVisibility(); if (n === 2) { - initStep2Options(); + initStep2Options(prevStep === 1); } else if (n === 3) { updateExportButton(); } From c842852ea7246c045bebd3f1feeb611915ff922f Mon Sep 17 00:00:00 2001 From: TimRl Date: Mon, 30 Mar 2026 13:22:25 -0600 Subject: [PATCH 11/14] Implemented audio exporter initialization and update FFmpeg path retrieval - Added `initializeAudioExporter` function to set the extension context for audio exporting. - Updated the FFmpeg path retrieval in the audio export process to utilize the context, enhancing the functionality of the audio exporter. --- src/exportHandler/audioExporter.ts | 8 +++++- src/extension.ts | 2 ++ types/index.d.ts | 44 +++++++++++++++--------------- 3 files changed, 31 insertions(+), 23 deletions(-) diff --git a/src/exportHandler/audioExporter.ts b/src/exportHandler/audioExporter.ts index 6e74a8899..c3385f552 100644 --- a/src/exportHandler/audioExporter.ts +++ b/src/exportHandler/audioExporter.ts @@ -9,6 +9,12 @@ import { getFFmpegPath } from "../utils/ffmpegManager"; const execAsync = promisify(exec); +let extensionContext: vscode.ExtensionContext | undefined; + +export const initializeAudioExporter = (context: vscode.ExtensionContext): void => { + extensionContext = context; +}; + // Debug logging for audio export diagnostics const DEBUG = false; function debug(...args: any[]) { @@ -353,7 +359,7 @@ async function convertToWav( originalExt: string, sampleRate: number = 48000 ): Promise { - const ffmpegBinaryPath = await getFFmpegPath(); + const ffmpegBinaryPath = await getFFmpegPath(extensionContext); if (!ffmpegBinaryPath) { throw new Error("FFmpeg not available"); } diff --git a/src/extension.ts b/src/extension.ts index 716bfc949..6b8b7a6fe 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -62,6 +62,7 @@ import { import { initializeAudioProcessor } from "./utils/audioProcessor"; import { initializeAudioMerger } from "./utils/audioMerger"; import { initializeAudioExtractor } from "./utils/audioExtractor"; +import { initializeAudioExporter } from "./exportHandler/audioExporter"; import { checkTools, getUnavailableTools } from "./utils/toolsManager"; import { initToolPreferences, setNativeGitAvailable, getGitToolMode, getSqliteToolMode } from "./utils/toolPreferences"; import { downloadFFmpeg } from "./utils/ffmpegManager"; @@ -322,6 +323,7 @@ export async function activate(context: vscode.ExtensionContext) { initializeAudioProcessor(context); initializeAudioMerger(context); initializeAudioExtractor(context); + initializeAudioExporter(context); // Register and show splash screen immediately before anything else try { diff --git a/types/index.d.ts b/types/index.d.ts index c0250bf17..9174737ee 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -2242,27 +2242,27 @@ export type GitToolMode = "auto" | "builtin" | "force-builtin"; export type SqliteToolMode = "auto" | "builtin" | "force-builtin"; export type MessagesToMissingToolsWarning = - | { command: "showWarnings"; git: boolean; nativeGitAvailable?: boolean; sqlite: boolean; nativeSqliteAvailable?: boolean; ffmpeg: boolean } - | { command: "updateWarnings"; git: boolean; nativeGitAvailable?: boolean; sqlite: boolean; nativeSqliteAvailable?: boolean; ffmpeg: boolean } - | { command: "showToolsStatus"; git: boolean; nativeGitAvailable?: boolean; sqlite: boolean; nativeSqliteAvailable?: boolean; ffmpeg: boolean; audioToolMode: AudioToolMode; gitToolMode: GitToolMode; sqliteToolMode: SqliteToolMode; syncInProgress?: boolean; audioProcessingInProgress?: boolean } - | { command: "toolDownloadResult"; tool: "sqlite" | "git" | "ffmpeg"; success: boolean; git: boolean; nativeGitAvailable?: boolean; sqlite: boolean; nativeSqliteAvailable?: boolean; ffmpeg: boolean; audioToolMode: AudioToolMode; gitToolMode: GitToolMode; sqliteToolMode: SqliteToolMode } - | { command: "audioModeChanged"; audioToolMode: AudioToolMode; ffmpeg: boolean } - | { command: "gitModeChanged"; gitToolMode: GitToolMode; git: boolean; nativeGitAvailable?: boolean } - | { command: "sqliteModeChanged"; sqliteToolMode: SqliteToolMode; sqlite: boolean; nativeSqliteAvailable?: boolean } - | { command: "operationStatusChanged"; syncInProgress: boolean; audioProcessingInProgress: boolean } - | { command: "showDeleteButtons" } - | { command: "showForceBuiltinButtons" } - | { command: "toolDeleted"; tool: "sqlite" | "git" | "ffmpeg" }; + | { command: "showWarnings"; git: boolean; nativeGitAvailable?: boolean; sqlite: boolean; nativeSqliteAvailable?: boolean; ffmpeg: boolean; } + | { command: "updateWarnings"; git: boolean; nativeGitAvailable?: boolean; sqlite: boolean; nativeSqliteAvailable?: boolean; ffmpeg: boolean; } + | { command: "showToolsStatus"; git: boolean; nativeGitAvailable?: boolean; sqlite: boolean; nativeSqliteAvailable?: boolean; ffmpeg: boolean; audioToolMode: AudioToolMode; gitToolMode: GitToolMode; sqliteToolMode: SqliteToolMode; syncInProgress?: boolean; audioProcessingInProgress?: boolean; } + | { command: "toolDownloadResult"; tool: "sqlite" | "git" | "ffmpeg"; success: boolean; git: boolean; nativeGitAvailable?: boolean; sqlite: boolean; nativeSqliteAvailable?: boolean; ffmpeg: boolean; audioToolMode: AudioToolMode; gitToolMode: GitToolMode; sqliteToolMode: SqliteToolMode; } + | { command: "audioModeChanged"; audioToolMode: AudioToolMode; ffmpeg: boolean; } + | { command: "gitModeChanged"; gitToolMode: GitToolMode; git: boolean; nativeGitAvailable?: boolean; } + | { command: "sqliteModeChanged"; sqliteToolMode: SqliteToolMode; sqlite: boolean; nativeSqliteAvailable?: boolean; } + | { command: "operationStatusChanged"; syncInProgress: boolean; audioProcessingInProgress: boolean; } + | { command: "showDeleteButtons"; } + | { command: "showForceBuiltinButtons"; } + | { command: "toolDeleted"; tool: "sqlite" | "git" | "ffmpeg"; }; export type MessagesFromMissingToolsWarning = - | { command: "retry" } - | { command: "continue" } - | { command: "openDownloadPage" } - | { command: "close" } - | { command: "downloadTool"; tool: "sqlite" | "git" | "ffmpeg" } - | { command: "toggleAudioMode" } - | { command: "toggleGitMode" } - | { command: "toggleSqliteMode" } - | { command: "deleteTool"; tool: "sqlite" | "git" | "ffmpeg" } - | { command: "forceBuiltinTool"; tool: "sqlite" | "git" | "ffmpeg" } - | { command: "reloadWindow" }; + | { command: "retry"; } + | { command: "continue"; } + | { command: "openDownloadPage"; } + | { command: "close"; } + | { command: "downloadTool"; tool: "sqlite" | "git" | "ffmpeg"; } + | { command: "toggleAudioMode"; } + | { command: "toggleGitMode"; } + | { command: "toggleSqliteMode"; } + | { command: "deleteTool"; tool: "sqlite" | "git" | "ffmpeg"; } + | { command: "forceBuiltinTool"; tool: "sqlite" | "git" | "ffmpeg"; } + | { command: "reloadWindow"; }; From 7629b2ce67f3db1864810ecef7797f682ac5a195 Mon Sep 17 00:00:00 2001 From: TimRl Date: Mon, 30 Mar 2026 13:29:21 -0600 Subject: [PATCH 12/14] Enhanced metadata synchronization in project configuration - Implemented syncing of sourceLanguage and targetLanguage from metadata to configuration, ensuring updates only occur when necessary. - Improved type definitions for languages in ProjectMetadata to utilize LanguageMetadata, enhancing clarity and type safety. --- src/projectManager/utils/projectUtils.ts | 36 ++++++++++++++++- types/index.d.ts | 51 +++++++++++------------- 2 files changed, 58 insertions(+), 29 deletions(-) diff --git a/src/projectManager/utils/projectUtils.ts b/src/projectManager/utils/projectUtils.ts index 1f1aef625..878bc82bb 100644 --- a/src/projectManager/utils/projectUtils.ts +++ b/src/projectManager/utils/projectUtils.ts @@ -1074,7 +1074,41 @@ export async function syncMetadataToConfiguration() { debug("No valid validationCountAudio found in metadata"); } - // Add other metadata properties sync here as needed + // Sync sourceLanguage and targetLanguage from metadata to config + if (Array.isArray(metadata.languages) && metadata.languages.length > 0) { + const metadataSource = metadata.languages.find( + (lang: LanguageMetadata) => lang.projectStatus === LanguageProjectStatus.SOURCE + ); + const metadataTarget = metadata.languages.find( + (lang: LanguageMetadata) => lang.projectStatus === LanguageProjectStatus.TARGET + ); + + if (metadataSource) { + const currentSource = config.get("sourceLanguage") as LanguageMetadata | undefined; + const needsUpdate = !currentSource + || !currentSource.tag + || currentSource.tag !== metadataSource.tag + || currentSource.refName !== metadataSource.refName; + + if (needsUpdate) { + debug(`Syncing sourceLanguage from metadata (${metadataSource.refName}) to configuration`); + await config.update("sourceLanguage", metadataSource, vscode.ConfigurationTarget.Workspace); + } + } + + if (metadataTarget) { + const currentTarget = config.get("targetLanguage") as LanguageMetadata | undefined; + const needsUpdate = !currentTarget + || !currentTarget.tag + || currentTarget.tag !== metadataTarget.tag + || currentTarget.refName !== metadataTarget.refName; + + if (needsUpdate) { + debug(`Syncing targetLanguage from metadata (${metadataTarget.refName}) to configuration`); + await config.update("targetLanguage", metadataTarget, vscode.ConfigurationTarget.Workspace); + } + } + } } catch (error) { console.error("Error syncing metadata to configuration:", error); } diff --git a/types/index.d.ts b/types/index.d.ts index 9174737ee..3519a13b4 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1024,12 +1024,7 @@ type ProjectMetadata = { [lang: string]: string; }; }; - languages: Array<{ - tag: string; - name: { - [lang: string]: string; - }; - }>; + languages: Array; type: { flavorType: { name: string; @@ -2242,27 +2237,27 @@ export type GitToolMode = "auto" | "builtin" | "force-builtin"; export type SqliteToolMode = "auto" | "builtin" | "force-builtin"; export type MessagesToMissingToolsWarning = - | { command: "showWarnings"; git: boolean; nativeGitAvailable?: boolean; sqlite: boolean; nativeSqliteAvailable?: boolean; ffmpeg: boolean; } - | { command: "updateWarnings"; git: boolean; nativeGitAvailable?: boolean; sqlite: boolean; nativeSqliteAvailable?: boolean; ffmpeg: boolean; } - | { command: "showToolsStatus"; git: boolean; nativeGitAvailable?: boolean; sqlite: boolean; nativeSqliteAvailable?: boolean; ffmpeg: boolean; audioToolMode: AudioToolMode; gitToolMode: GitToolMode; sqliteToolMode: SqliteToolMode; syncInProgress?: boolean; audioProcessingInProgress?: boolean; } - | { command: "toolDownloadResult"; tool: "sqlite" | "git" | "ffmpeg"; success: boolean; git: boolean; nativeGitAvailable?: boolean; sqlite: boolean; nativeSqliteAvailable?: boolean; ffmpeg: boolean; audioToolMode: AudioToolMode; gitToolMode: GitToolMode; sqliteToolMode: SqliteToolMode; } - | { command: "audioModeChanged"; audioToolMode: AudioToolMode; ffmpeg: boolean; } - | { command: "gitModeChanged"; gitToolMode: GitToolMode; git: boolean; nativeGitAvailable?: boolean; } - | { command: "sqliteModeChanged"; sqliteToolMode: SqliteToolMode; sqlite: boolean; nativeSqliteAvailable?: boolean; } - | { command: "operationStatusChanged"; syncInProgress: boolean; audioProcessingInProgress: boolean; } - | { command: "showDeleteButtons"; } - | { command: "showForceBuiltinButtons"; } - | { command: "toolDeleted"; tool: "sqlite" | "git" | "ffmpeg"; }; + | { command: "showWarnings"; git: boolean; nativeGitAvailable?: boolean; sqlite: boolean; nativeSqliteAvailable?: boolean; ffmpeg: boolean } + | { command: "updateWarnings"; git: boolean; nativeGitAvailable?: boolean; sqlite: boolean; nativeSqliteAvailable?: boolean; ffmpeg: boolean } + | { command: "showToolsStatus"; git: boolean; nativeGitAvailable?: boolean; sqlite: boolean; nativeSqliteAvailable?: boolean; ffmpeg: boolean; audioToolMode: AudioToolMode; gitToolMode: GitToolMode; sqliteToolMode: SqliteToolMode; syncInProgress?: boolean; audioProcessingInProgress?: boolean } + | { command: "toolDownloadResult"; tool: "sqlite" | "git" | "ffmpeg"; success: boolean; git: boolean; nativeGitAvailable?: boolean; sqlite: boolean; nativeSqliteAvailable?: boolean; ffmpeg: boolean; audioToolMode: AudioToolMode; gitToolMode: GitToolMode; sqliteToolMode: SqliteToolMode } + | { command: "audioModeChanged"; audioToolMode: AudioToolMode; ffmpeg: boolean } + | { command: "gitModeChanged"; gitToolMode: GitToolMode; git: boolean; nativeGitAvailable?: boolean } + | { command: "sqliteModeChanged"; sqliteToolMode: SqliteToolMode; sqlite: boolean; nativeSqliteAvailable?: boolean } + | { command: "operationStatusChanged"; syncInProgress: boolean; audioProcessingInProgress: boolean } + | { command: "showDeleteButtons" } + | { command: "showForceBuiltinButtons" } + | { command: "toolDeleted"; tool: "sqlite" | "git" | "ffmpeg" }; export type MessagesFromMissingToolsWarning = - | { command: "retry"; } - | { command: "continue"; } - | { command: "openDownloadPage"; } - | { command: "close"; } - | { command: "downloadTool"; tool: "sqlite" | "git" | "ffmpeg"; } - | { command: "toggleAudioMode"; } - | { command: "toggleGitMode"; } - | { command: "toggleSqliteMode"; } - | { command: "deleteTool"; tool: "sqlite" | "git" | "ffmpeg"; } - | { command: "forceBuiltinTool"; tool: "sqlite" | "git" | "ffmpeg"; } - | { command: "reloadWindow"; }; + | { command: "retry" } + | { command: "continue" } + | { command: "openDownloadPage" } + | { command: "close" } + | { command: "downloadTool"; tool: "sqlite" | "git" | "ffmpeg" } + | { command: "toggleAudioMode" } + | { command: "toggleGitMode" } + | { command: "toggleSqliteMode" } + | { command: "deleteTool"; tool: "sqlite" | "git" | "ffmpeg" } + | { command: "forceBuiltinTool"; tool: "sqlite" | "git" | "ffmpeg" } + | { command: "reloadWindow" }; From bdbb605144b5407b0689efd31478f87a2f986de2 Mon Sep 17 00:00:00 2001 From: TimRl Date: Tue, 31 Mar 2026 08:33:17 -0600 Subject: [PATCH 13/14] Refactored USFM export options in projectExportView.ts. Removed hidden usfmOptions section and integrated USFM no-validation option directly into the export format selection. --- src/projectManager/projectExportView.ts | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/src/projectManager/projectExportView.ts b/src/projectManager/projectExportView.ts index c40deb496..2c880a318 100644 --- a/src/projectManager/projectExportView.ts +++ b/src/projectManager/projectExportView.ts @@ -404,6 +404,13 @@ function getWebviewContent(

Export in Universal Standard Format Markers

+
+
+ Generate USFM Without Validation +

Skip USFM validation for a faster export

+ May produce invalid USFM +
+
Generate HTML @@ -412,15 +419,6 @@ function getWebviewContent(
-
@@ -696,8 +694,7 @@ function getWebviewContent( opt.style.backgroundColor = ''; opt.style.borderColor = ''; }); - const usfmOptionsEl = document.getElementById('usfmOptions'); - if (usfmOptionsEl) usfmOptionsEl.style.display = 'none'; + } updateStep2Button(); } @@ -821,8 +818,6 @@ function getWebviewContent( if (option.classList.contains('selected')) { option.classList.remove('selected'); selectedFormat = null; - const usfmOptions = document.getElementById('usfmOptions'); - if (usfmOptions) usfmOptions.style.display = 'none'; updateStep2Button(); return; } @@ -835,9 +830,6 @@ function getWebviewContent( }); option.classList.add('selected'); selectedFormat = option.dataset.format; - const usfmOptions = document.getElementById('usfmOptions'); - const isUsfmVariant = selectedFormat === 'usfm' || selectedFormat === 'usfm-no-validate'; - if (usfmOptions) usfmOptions.style.display = isUsfmVariant ? 'block' : 'none'; updateStep2Button(); }); }); From 19640c539c14db25f656c069226eb96093559782 Mon Sep 17 00:00:00 2001 From: TimRl Date: Tue, 31 Mar 2026 08:42:51 -0600 Subject: [PATCH 14/14] Removed zip output functionality from export options and related UI elements. Cleaned up exportCodexContent function by eliminating zipDirectory dependency and associated logic. --- src/exportHandler/exportHandler.ts | 13 ++----------- src/exportHandler/utils/zipUtils.ts | 17 ----------------- src/projectManager/projectExportView.ts | 8 +------- 3 files changed, 3 insertions(+), 35 deletions(-) delete mode 100644 src/exportHandler/utils/zipUtils.ts diff --git a/src/exportHandler/exportHandler.ts b/src/exportHandler/exportHandler.ts index 0a791bbc1..278dbf649 100644 --- a/src/exportHandler/exportHandler.ts +++ b/src/exportHandler/exportHandler.ts @@ -7,7 +7,7 @@ import { exec } from "child_process"; import { promisify } from "util"; import { removeHtmlTags, generateSrtData } from "./subtitleUtils"; import { generateVttData } from "./vttUtils"; -import { zipDirectory } from "./utils/zipUtils"; + // import { exportRtfWithPandoc } from "../../webviews/codex-webviews/src/NewSourceUploader/importers/rtf/pandocNodeBridge"; const execAsync = promisify(exec); @@ -215,7 +215,6 @@ export enum CodexExportFormat { export interface ExportOptions { skipValidation?: boolean; removeIds?: boolean; - zipOutput?: boolean; includeAudio?: boolean; includeTimestamps?: boolean; } @@ -1611,7 +1610,6 @@ export async function exportCodexContent( filesToExport: string[], options?: ExportOptions ) { - const shouldZip = options?.zipOutput ?? false; const includeAudio = options?.includeAudio === true && format !== CodexExportFormat.AUDIO; const isMulti = includeAudio; @@ -1624,7 +1622,7 @@ export async function exportCodexContent( const baseName = `${projectName}-${formatLabel}-${dateStamp}`; let candidate = path.join(userSelectedPath, baseName); let suffix = 1; - while (fs.existsSync(candidate) || fs.existsSync(`${candidate}.zip`)) { + while (fs.existsSync(candidate)) { candidate = path.join(userSelectedPath, `${baseName}-${suffix}`); suffix++; } @@ -1687,13 +1685,6 @@ export async function exportCodexContent( } await Promise.all(exportPromises); - - if (shouldZip) { - const zipPath = `${wrapperPath}.zip`; - await zipDirectory(wrapperPath, zipPath); - fs.rmSync(wrapperPath, { recursive: true, force: true }); - vscode.window.showInformationMessage(`Exported to ${zipPath}`); - } } // Compact helpers for id handling and lookups diff --git a/src/exportHandler/utils/zipUtils.ts b/src/exportHandler/utils/zipUtils.ts deleted file mode 100644 index 10730f95e..000000000 --- a/src/exportHandler/utils/zipUtils.ts +++ /dev/null @@ -1,17 +0,0 @@ -import archiver from "archiver"; -import * as fs from "fs"; -import { basename } from "path"; - -export const zipDirectory = (sourceDir: string, destZipPath: string): Promise => { - return new Promise((resolve, reject) => { - const output = fs.createWriteStream(destZipPath); - const archive = archiver("zip", { zlib: { level: 9 } }); - - output.on("close", resolve); - archive.on("error", reject); - - archive.pipe(output); - archive.directory(sourceDir, basename(sourceDir)); - archive.finalize(); - }); -}; diff --git a/src/projectManager/projectExportView.ts b/src/projectManager/projectExportView.ts index 2c880a318..db6aa375b 100644 --- a/src/projectManager/projectExportView.ts +++ b/src/projectManager/projectExportView.ts @@ -545,12 +545,7 @@ function getWebviewContent( Select Location
-
-
- - -
-
+
` @@ -848,7 +843,6 @@ function getWebviewContent( options.includeAudio = true; options.includeTimestamps = selectedAudioMode === 'audio-timestamps'; } - if (document.getElementById('zipOutput')?.checked) options.zipOutput = true; vscode.postMessage({ command: 'export', format: formatToSend,