diff --git a/renderer/src/components/EditorPage/AudioEditor/AudioEditor.js b/renderer/src/components/EditorPage/AudioEditor/AudioEditor.js
index 83d7efb2..ee9b2e0b 100644
--- a/renderer/src/components/EditorPage/AudioEditor/AudioEditor.js
+++ b/renderer/src/components/EditorPage/AudioEditor/AudioEditor.js
@@ -96,6 +96,33 @@ const loadVerseStructureFromFile = (projectsDir, bookId, chapter) => {
}
};
+const parsedUsfmCache = new Map();
+
+const getParsedUsfmBook = (usfmPath) => {
+ const fs = window.require('fs');
+
+ const stat = fs.statSync(usfmPath);
+ const cached = parsedUsfmCache.get(usfmPath);
+
+ if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
+ logger.debug('Using cached parsed USFM:', usfmPath);
+ return cached.bookContent;
+ }
+
+ const usfm = fs.readFileSync(usfmPath, 'utf8');
+ const myUsfmParser = new grammar.USFMParser(usfm, grammar.LEVEL.RELAXED);
+ const jsonOutput = myUsfmParser.toJSON();
+ const bookContent = jsonOutput.chapters;
+
+ parsedUsfmCache.set(usfmPath, {
+ mtimeMs: stat.mtimeMs,
+ size: stat.size,
+ bookContent,
+ });
+
+ return bookContent;
+};
+
// Priority 2: Load from USFM
const loadVersesFromUSFM = (projectsDir, bookId, chapter) => {
try {
@@ -116,19 +143,7 @@ const loadVersesFromUSFM = (projectsDir, bookId, chapter) => {
};
}
- const usfm = fs.readFileSync(usfmPath, 'utf8');
- const myUsfmParser = new grammar.USFMParser(usfm, grammar.LEVEL.RELAXED);
- const isJsonValid = myUsfmParser.validate();
-
- if (!isJsonValid) {
- logger.error('Invalid USFM file');
- return {
- success: false, bookContent: null, verses: null, source: null,
- };
- }
-
- const jsonOutput = myUsfmParser.toJSON();
- const bookContent = jsonOutput.chapters;
+ const bookContent = getParsedUsfmBook(usfmPath);
const chapterData = bookContent.find(
(ch) => ch.chapterNumber === chapter.toString(),
@@ -495,6 +510,8 @@ const AudioEditor = ({ editor }) => {
useEffect(() => {
if (!isElectron()) { return; }
+ let cancelled = false;
+
structureAppliedRef.current = false;
setIsLoading(true);
@@ -605,6 +622,8 @@ const AudioEditor = ({ editor }) => {
logger.debug('Data source:', dataSource);
logger.debug('Verse count:', finalVerses.length);
+ if (cancelled) { return; }
+
setOriginalBookContent(bookContent);
const versesWithAudios = structureAppliedRef.current
@@ -629,25 +648,30 @@ const AudioEditor = ({ editor }) => {
}),
);
- if (_books.includes(bookId.toUpperCase()) === false) {
+ if (!cancelled && _books.includes(bookId.toUpperCase()) === false) {
setAudioContent();
setDisplayScreen(true);
+ setIsLoading(false);
}
}
} catch (error) {
logger.error('Error in useEffect:', error);
- setIsLoading(false);
- setDisplayScreen(true);
+ if (!cancelled) {
+ setIsLoading(false);
+ setDisplayScreen(true);
+ }
}
});
+
+ return () => { cancelled = true; };
}, [bookId, chapter]);
return (
- {((isLoading || !audioContent) && displyScreen) && }
- {isLoading && !displyScreen && }
- {audioContent && isLoading === false
+ {isLoading && }
+ {!isLoading && displyScreen && }
+ {!isLoading && audioContent
&& (
{
const { t } = useTranslation();
- if (!visible) {
+ const hasMenuItems = (!isFirstVerse && !isJoinedVerse)
+ || isJoinedVerse
+ || canOpenComments;
+
+ if (!visible || !hasMenuItems) {
return null;
}
diff --git a/renderer/src/components/EditorPage/VideoEditor/VideoEditor.js b/renderer/src/components/EditorPage/VideoEditor/VideoEditor.js
index 40ab58af..0edc6209 100644
--- a/renderer/src/components/EditorPage/VideoEditor/VideoEditor.js
+++ b/renderer/src/components/EditorPage/VideoEditor/VideoEditor.js
@@ -118,6 +118,33 @@ const loadVerseStructureFromFile = (projectsDir, bookId, chapter) => {
}
};
+const parsedUsfmCache = new Map();
+
+const getParsedUsfmBook = (usfmPath) => {
+ const fs = window.require('fs');
+
+ const stat = fs.statSync(usfmPath);
+ const cached = parsedUsfmCache.get(usfmPath);
+
+ if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
+ logger.debug('Using cached parsed USFM:', usfmPath);
+ return cached.bookContent;
+ }
+
+ const usfm = fs.readFileSync(usfmPath, 'utf8');
+ const myUsfmParser = new grammar.USFMParser(usfm, grammar.LEVEL.RELAXED);
+ const jsonOutput = myUsfmParser.toJSON();
+ const bookContent = jsonOutput.chapters;
+
+ parsedUsfmCache.set(usfmPath, {
+ mtimeMs: stat.mtimeMs,
+ size: stat.size,
+ bookContent,
+ });
+
+ return bookContent;
+};
+
const loadVersesFromUSFM = (projectsDir, bookId, chapter) => {
try {
const fs = window.require('fs');
@@ -137,19 +164,7 @@ const loadVersesFromUSFM = (projectsDir, bookId, chapter) => {
};
}
- const usfm = fs.readFileSync(usfmPath, 'utf8');
- const myUsfmParser = new grammar.USFMParser(usfm, grammar.LEVEL.RELAXED);
- const isJsonValid = myUsfmParser.validate();
-
- if (!isJsonValid) {
- logger.error('Invalid USFM file');
- return {
- success: false, bookContent: null, verses: null, source: null,
- };
- }
-
- const jsonOutput = myUsfmParser.toJSON();
- const bookContent = jsonOutput.chapters;
+ const bookContent = getParsedUsfmBook(usfmPath);
const chapterData = bookContent.find(
(ch) => ch.chapterNumber === chapter.toString(),
@@ -609,6 +624,8 @@ const VideoEditor = ({ editor }) => {
modelClose();
};
useEffect(() => {
+ let cancelled = false;
+
if (isElectron()) {
setIsLoading(true);
setDisplayScreen(false);
@@ -728,6 +745,8 @@ const VideoEditor = ({ editor }) => {
chapter,
);
+ if (cancelled) { return; }
+
setOriginalBookContent(bookContent);
logger.debug('Starting video attachment process...');
@@ -760,18 +779,23 @@ const VideoEditor = ({ editor }) => {
}),
);
- if (_books.includes(bookId.toUpperCase()) === false) {
+ if (!cancelled && _books.includes(bookId.toUpperCase()) === false) {
setVideoContent();
setDisplayScreen(true);
+ setIsLoading(false);
}
}
} catch (error) {
logger.error('Error in useEffect:', error);
- setIsLoading(false);
- setDisplayScreen(true);
+ if (!cancelled) {
+ setIsLoading(false);
+ setDisplayScreen(true);
+ }
}
});
}
+
+ return () => { cancelled = true; };
}, [bookId, chapter]);
useEffect(() => {
@@ -808,9 +832,9 @@ const VideoEditor = ({ editor }) => {
return (
- {((isLoading || !videoContent) && displyScreen) && }
- {isLoading && !displyScreen && }
- {videoContent && isLoading === false
+ {isLoading && }
+ {!isLoading && displyScreen && }
+ {!isLoading && videoContent
&& (
{
+ const openImportPicker = () => {
+ if (importInputRef.current) {
+ importInputRef.current.value = '';
+ importInputRef.current.click();
+ }
+ };
+
+ if (existingVideo) {
+ setOpenModal({
+ openModel: true,
+ title: 'Replace Existing Video?',
+ confirmMessage: 'A video already exists for this verse. Importing a new video will replace the current recording.',
+ buttonName: 'Replace',
+ action: 'custom',
+ actionData: { callback: openImportPicker },
+ });
+ return;
+ }
+
+ openImportPicker();
+ };
+
+ const handleImportFileChange = async (e) => {
+ const file = e.target.files?.[0];
+ if (!file) { return; }
+
+ const supportedTypes = ['video/webm'];
+ const supportedExtensions = ['.webm'];
+ const fileExtension = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
+
+ if (!supportedTypes.includes(file.type) && !supportedExtensions.includes(fileExtension)) {
+ setError('Unsupported file type. Please select a .webm video file.');
+ return;
+ }
+
+ try {
+ setIsImporting(true);
+
+ const destFilename = fileNameOverride || `${chapter}_${verse}.webm`;
+ const destPath = path.join(projectPath, destFilename);
+
+ const arrayBuffer = await file.arrayBuffer();
+ const buffer = Buffer.from(arrayBuffer);
+ fs.writeFileSync(destPath, buffer);
+
+ if (onRecordingComplete) {
+ onRecordingComplete({
+ verse,
+ chapter,
+ filePath: destPath,
+ filename: destFilename,
+ });
+ }
+
+ setExistingVideo(true);
+ setCurrentMode('view');
+
+ setNotify('success');
+ setSnackText(`Video "${file.name}" imported successfully`);
+ setOpenSnackBar(true);
+
+ setTimeout(() => {
+ if (videoPreviewRef.current) {
+ const blob = new Blob([buffer], { type: file.type || 'video/mp4' });
+ const objectUrl = URL.createObjectURL(blob);
+
+ videoPreviewRef.current.pause();
+ videoPreviewRef.current.srcObject = null;
+ videoPreviewRef.current.removeAttribute('src');
+ videoPreviewRef.current.load();
+
+ videoPreviewRef.current.src = objectUrl;
+ videoPreviewRef.current.load();
+ }
+ }, 100);
+ } catch (err) {
+ setError(`Failed to import video: ${err.message}`);
+ } finally {
+ setIsImporting(false);
+ }
+ };
+
const formatTime = (seconds) => {
if (!Number.isFinite(seconds) || seconds <= 0) { return '00:00'; }
const mins = Math.floor(seconds / 60);
@@ -272,10 +358,17 @@ const VideoRecorder = ({
const hasNextVerse = () => getNextVerseNumber(verse, content) !== null;
return (
-
+
+
{(() => {
+ if (isImporting) {
+ return 'Importing video...';
+ }
if (isRecording) {
return `${isPaused ? 'Paused' : 'Recording'}: ${formatTime(recordingTime)}`;
}
@@ -310,7 +406,6 @@ const VideoRecorder = ({
return 'Ready to record';
})()}
-
@@ -318,12 +413,11 @@ const VideoRecorder = ({
type="button"
onClick={onClose}
className="hover:bg-white hover:bg-opacity-20 p-2 rounded-full transition-colors"
- disabled={isRecording}
+ disabled={isRecording || isImporting}
title="Close"
>
-
@@ -379,11 +473,11 @@ const VideoRecorder = ({
)}
- {isProcessing && (
+ {(isProcessing || isImporting) && (
-
Saving video...
+
{isImporting ? 'Importing video...' : 'Saving video...'}
)}
@@ -520,7 +614,7 @@ const VideoRecorder = ({
)}
-
+
- {!hideDeleteButton && (
+
+ {!hideDeleteButton && (
+
+ )}
+
+
+
+
+
+
- )}
-
-
-
+
-
);
};
@@ -713,7 +832,6 @@ VideoRecorder.defaultProps = {
commentCount: 0,
onOpenComments: null,
showCommentsButton: false,
-
};
export default VideoRecorder;