Skip to content

Commit 1e7c707

Browse files
inkeysoftwaredhivehimediaCopilot
authored
Save to idml.txt (#83)
* autosave to idml.txt * USFM is now only to select template * Update src/MainApp.js Co-authored-by: Copilot <[email protected]> * Update src/MainApp.js Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: dhivehimedia <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent cedcb00 commit 1e7c707

File tree

8 files changed

+540
-119
lines changed

8 files changed

+540
-119
lines changed
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
{
22
"name": "Paratext Diagram Labeler",
3-
"version": "0.1.3",
4-
"date": "2025-11-11",
3+
"version": "0.1.4",
4+
"date": "2025-11-12",
55
"platform": "win",
66
"platform_version": "",
77
"architecture": "",
88
"stability": "stable",
9-
"file": "Paratext.Diagram.Labeler.Setup.0.1.3.exe",
10-
"md5": "1f39069d5f8b74eb86ed8f53b6b02953",
9+
"file": "Paratext.Diagram.Labeler.Setup.0.1.4.exe",
10+
"md5": "",
1111
"type": "exe",
1212
"build": ""
1313
}

electron-main.js

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,101 @@ ipcMain.handle('save-term-renderings', async (event, projectFolder, saveToDemo,
583583
}
584584
});
585585

586+
// Load labels from .IDML.TXT file in shared/labeler folder
587+
ipcMain.handle('load-labels-from-idml-txt', async (event, projectFolder, templateName) => {
588+
await loadSettings(projectFolder);
589+
try {
590+
const projectName = settings.name;
591+
const filename = `${templateName} @${projectName}.idml.txt`;
592+
const sharedLabelerPath = path.join(projectFolder, 'shared', 'labeler');
593+
const filePath = path.join(sharedLabelerPath, filename);
594+
595+
if (!fs.existsSync(filePath)) {
596+
console.log(`.IDML.TXT file not found: ${filePath}`);
597+
return { success: true, labels: null };
598+
}
599+
600+
// Read and decode the file
601+
const fileBuffer = fs.readFileSync(filePath);
602+
const uint8 = new Uint8Array(fileBuffer);
603+
604+
let fileText;
605+
// UTF-16LE BOM: FF FE
606+
if (uint8[0] === 0xff && uint8[1] === 0xfe) {
607+
fileText = new TextDecoder('utf-16le').decode(uint8.subarray(2));
608+
} else if (uint8[0] === 0xef && uint8[1] === 0xbb && uint8[2] === 0xbf) {
609+
// UTF-8 BOM
610+
fileText = new TextDecoder('utf-8').decode(uint8.subarray(3));
611+
} else {
612+
// Default: utf-8
613+
fileText = new TextDecoder('utf-8').decode(uint8);
614+
}
615+
616+
// Parse IDML data merge format
617+
const lines = fileText.split('\n').map(line => line.trim()).filter(line => line.length > 0);
618+
if (lines.length < 2) {
619+
console.log('Invalid .IDML.TXT file format');
620+
return { success: true, labels: null };
621+
}
622+
623+
const mergeKeys = lines[0].split('\t');
624+
const verns = lines[1].split('\t');
625+
626+
const labels = {};
627+
if (verns.length === mergeKeys.length) {
628+
for (let i = 0; i < mergeKeys.length; i++) {
629+
labels[mergeKeys[i]] = verns[i];
630+
}
631+
console.log(`Loaded labels from ${filename}:`, labels);
632+
return { success: true, labels };
633+
} else {
634+
console.log('Mismatch between merge keys and vernacular labels');
635+
return { success: true, labels: null };
636+
}
637+
} catch (e) {
638+
console.error('Error loading .IDML.TXT file:', e);
639+
return { success: false, error: e.message };
640+
}
641+
});
642+
643+
// Save labels to .IDML.TXT file in shared/labeler folder
644+
ipcMain.handle('save-labels-to-idml-txt', async (event, projectFolder, templateName, labels) => {
645+
await loadSettings(projectFolder);
646+
try {
647+
const projectName = settings.name;
648+
const filename = `${templateName} @${projectName}.idml.txt`;
649+
const sharedPath = path.join(projectFolder, 'shared');
650+
const sharedLabelerPath = path.join(sharedPath, 'labeler');
651+
652+
// Create folders if they don't exist
653+
if (!fs.existsSync(sharedPath)) {
654+
fs.mkdirSync(sharedPath, { recursive: true });
655+
}
656+
if (!fs.existsSync(sharedLabelerPath)) {
657+
fs.mkdirSync(sharedLabelerPath, { recursive: true });
658+
}
659+
660+
const filePath = path.join(sharedLabelerPath, filename);
661+
662+
// Build IDML data merge format
663+
const mergeKeys = Object.keys(labels);
664+
const vernLabels = mergeKeys.map(key => labels[key] || '');
665+
666+
const dataMergeHeader = mergeKeys.join('\t');
667+
const dataMergeContent = vernLabels.join('\t');
668+
const data = dataMergeHeader + '\n' + dataMergeContent + '\n';
669+
670+
// Write with BOM and UTF-16 LE encoding
671+
await fs.promises.writeFile(filePath, '\uFEFF' + data, { encoding: 'utf16le' });
672+
673+
console.log(`Labels saved to ${filename}`);
674+
return { success: true, filePath };
675+
} catch (e) {
676+
console.error('Error saving .IDML.TXT file:', e);
677+
return { success: false, error: e.message };
678+
}
679+
});
680+
586681
// Paratext project discovery functions
587682
function getParatextDirectories() {
588683
const directories = [];

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "paratext-diagram-labeler",
3-
"version": "0.1.4",
3+
"version": "0.1.5",
44
"private": true,
55
"author": "SIL Scripture Publishing Services <[email protected]>",
66
"description": "Paratext Diagram Labeler",

preload.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ contextBridge.exposeInMainWorld('electronAPI', {
4343
discoverCollections: templateFolderPath =>
4444
ipcRenderer.invoke('discover-collections', templateFolderPath),
4545

46+
loadLabelsFromIdmlTxt: (projectFolder, templateName) =>
47+
ipcRenderer.invoke('load-labels-from-idml-txt', projectFolder, templateName),
48+
49+
saveLabelsToIdmlTxt: (projectFolder, templateName, labels) =>
50+
ipcRenderer.invoke('save-labels-to-idml-txt', projectFolder, templateName, labels),
51+
4652
// Path utilities
4753
path: {
4854
join: (...paths) => ipcRenderer.invoke('path-join', ...paths),

src/DetailsPane.js

Lines changed: 119 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -44,23 +44,40 @@ export default function DetailsPane({
4444
onVariantChange,
4545
mapxPath,
4646
idmlPath,
47+
hasUnsavedChanges = false,
48+
onSaveLabels,
49+
onRevertLabels,
4750
}) {
4851
const [localIsApproved, setLocalIsApproved] = useState(isApproved);
4952
const [localRenderings, setLocalRenderings] = useState(renderings);
5053
const [showTemplateInfo, setShowTemplateInfo] = useState(false);
5154
const [templateData, setTemplateData] = useState({});
5255
const [showExportDialog, setShowExportDialog] = useState(false);
53-
const [selectedExportFormat, setSelectedExportFormat] = useState('idml-txt');
56+
const [selectedExportFormat, setSelectedExportFormat] = useState('idml-full');
57+
const [lastUsedExportFormat, setLastUsedExportFormat] = useState(null);
5458

55-
// Reset export format if selected format is unavailable
59+
// Set export format based on last-used or precedence order when dialog opens
5660
useEffect(() => {
57-
if (selectedExportFormat === 'mapx-full' && !mapxPath) {
58-
setSelectedExportFormat('idml-txt');
59-
}
60-
if (selectedExportFormat === 'idml-full' && !idmlPath) {
61-
setSelectedExportFormat('idml-txt');
61+
if (!showExportDialog) return;
62+
63+
// Determine available options
64+
const availableFormats = [];
65+
if (idmlPath) availableFormats.push('idml-full');
66+
if (mapxPath) availableFormats.push('mapx-full');
67+
if (templateData.formats && templateData.formats.includes('mapx')) availableFormats.push('mapx-txt');
68+
69+
// If last-used format is available, use it
70+
if (lastUsedExportFormat && availableFormats.includes(lastUsedExportFormat)) {
71+
setSelectedExportFormat(lastUsedExportFormat);
72+
} else {
73+
// Otherwise, use precedence order: idml-full > mapx-full > mapx-txt
74+
if (availableFormats.length > 0) {
75+
setSelectedExportFormat(availableFormats[0]);
76+
} else {
77+
setSelectedExportFormat(null);
78+
}
6279
}
63-
}, [mapxPath, idmlPath, selectedExportFormat]);
80+
}, [showExportDialog, mapxPath, idmlPath, templateData.formats, lastUsedExportFormat]);
6481

6582
// Load template data when mapDef.template changes
6683
useEffect(() => {
@@ -204,30 +221,18 @@ export default function DetailsPane({
204221
}
205222
};
206223

207-
// Helper function to generate USFM from the current map state // TODO: compare with usfmFromMap(). Could probably be consolidated.
224+
// Helper function to generate USFM from the current map state
225+
// USFM now only contains the \fig field - labels are stored in .idml.txt files
208226
const generateUsfm = () => {
209-
console.log('Converting map to USFM:', mapDef);
210-
// Reconstruct USFM string from current map state
211-
let usfm = `\\zdiagram-s |template="${mapDef.template}"\\*\n`;
212-
213-
// Always include the \fig line if present, and ensure it is in correct USFM format
227+
console.log('Converting map to USFM (only \\fig field):', mapDef);
228+
// Only return the \fig...\fig* field
229+
let usfm = '';
214230
if (mapDef.fig && !/^\\fig/.test(mapDef.fig)) {
215-
usfm += `\\fig ${mapDef.fig}\\fig*\n`;
231+
usfm = `\\fig ${mapDef.fig}\\fig*`;
216232
} else if (mapDef.fig) {
217-
usfm += `${mapDef.fig}\n`;
233+
usfm = mapDef.fig;
218234
}
219-
220-
// Add each label as a \zlabel entry
221-
locations.forEach(label => {
222-
usfm += `\\zlabel-s |key="${label.mergeKey}" termid="${label.termId}" gloss="${inLang(
223-
label.gloss,
224-
lang
225-
)}"\\*${label.vernLabel || ''}\\zlabel-e\\*\n`;
226-
});
227-
228-
usfm += '\\zdiagram-e \\*';
229-
// Remove unnecessary escaping for output
230-
return usfm.replace(/\\/g, '\\');
235+
return usfm;
231236
};
232237

233238
const handleOk = () => {
@@ -579,6 +584,85 @@ export default function DetailsPane({
579584
📂
580585
</span>
581586
</button>
587+
<button
588+
onClick={onRevertLabels}
589+
disabled={!hasUnsavedChanges}
590+
style={{
591+
background: 'none',
592+
border: 'none',
593+
cursor: hasUnsavedChanges ? 'pointer' : 'not-allowed',
594+
padding: 0,
595+
marginLeft: 1,
596+
opacity: hasUnsavedChanges ? 1 : 0.3,
597+
}}
598+
title={inLang(uiStr.revertChanges, lang)}
599+
>
600+
{/* Revert icon: curved arrow going left */}
601+
<svg
602+
width="22"
603+
height="22"
604+
viewBox="0 0 22 22"
605+
fill="none"
606+
xmlns="http://www.w3.org/2000/svg"
607+
>
608+
<path
609+
d="M8 11h8M8 11l3 3m-3-3l3-3M16 11a5 5 0 1 1-5-5"
610+
stroke={hasUnsavedChanges ? '#ff9800' : '#999'}
611+
strokeWidth="1.8"
612+
strokeLinecap="round"
613+
strokeLinejoin="round"
614+
/>
615+
</svg>
616+
</button>
617+
<button
618+
onClick={onSaveLabels}
619+
disabled={!hasUnsavedChanges}
620+
style={{
621+
background: 'none',
622+
border: 'none',
623+
cursor: hasUnsavedChanges ? 'pointer' : 'not-allowed',
624+
padding: 0,
625+
marginLeft: 1,
626+
opacity: hasUnsavedChanges ? 1 : 0.3,
627+
}}
628+
title={inLang(uiStr.saveChanges, lang)}
629+
>
630+
{/* Save icon: floppy disk */}
631+
<svg
632+
width="22"
633+
height="22"
634+
viewBox="0 0 22 22"
635+
fill="none"
636+
xmlns="http://www.w3.org/2000/svg"
637+
>
638+
<rect
639+
x="4"
640+
y="3"
641+
width="14"
642+
height="16"
643+
rx="2"
644+
stroke={hasUnsavedChanges ? '#4caf50' : '#999'}
645+
strokeWidth="1.5"
646+
fill="none"
647+
/>
648+
<rect
649+
x="7"
650+
y="3"
651+
width="8"
652+
height="5"
653+
fill={hasUnsavedChanges ? '#4caf50' : '#999'}
654+
/>
655+
<rect
656+
x="6"
657+
y="12"
658+
width="10"
659+
height="7"
660+
fill="none"
661+
stroke={hasUnsavedChanges ? '#4caf50' : '#999'}
662+
strokeWidth="1.2"
663+
/>
664+
</svg>
665+
</button>
582666
<button
583667
onClick={handleExportDataMerge}
584668
style={{
@@ -774,19 +858,6 @@ export default function DetailsPane({
774858
</div>
775859
)}
776860

777-
{/* IDML Data Merge Export */}
778-
<label style={{ display: 'block', marginBottom: 8, cursor: 'pointer' }}>
779-
<input
780-
type="radio"
781-
name="exportFormat"
782-
value="idml-txt"
783-
checked={selectedExportFormat === 'idml-txt'}
784-
onChange={e => setSelectedExportFormat(e.target.value)}
785-
style={{ marginRight: 8 }}
786-
/>
787-
InDesign data merge file (.IDML.TXT)
788-
</label>
789-
790861
{/* MAPX Full Export */}
791862
{mapxPath ? (
792863
<label style={{ display: 'block', marginBottom: 8, cursor: 'pointer' }}>
@@ -853,16 +924,19 @@ export default function DetailsPane({
853924
</button>
854925
<button
855926
onClick={async () => {
927+
if (!selectedExportFormat) return;
928+
setLastUsedExportFormat(selectedExportFormat);
856929
setShowExportDialog(false);
857930
await handleExportWithFormat(selectedExportFormat);
858931
}}
932+
disabled={!selectedExportFormat}
859933
style={{
860934
padding: '8px 16px',
861935
borderRadius: 4,
862-
border: '1px solid #1976d2',
863-
background: '#1976d2',
864-
color: 'white',
865-
cursor: 'pointer',
936+
border: selectedExportFormat ? '1px solid #1976d2' : '1px solid #ccc',
937+
background: selectedExportFormat ? '#1976d2' : '#e0e0e0',
938+
color: selectedExportFormat ? 'white' : '#999',
939+
cursor: selectedExportFormat ? 'pointer' : 'not-allowed',
866940
}}
867941
>
868942
{inLang(uiStr.ok, lang)}

0 commit comments

Comments
 (0)