diff --git a/src/components/output/LaTeXCompileButton.tsx b/src/components/output/LaTeXCompileButton.tsx index 5764359..d6e88dc 100644 --- a/src/components/output/LaTeXCompileButton.tsx +++ b/src/components/output/LaTeXCompileButton.tsx @@ -10,6 +10,7 @@ import { useSettings } from '../../hooks/useSettings'; import type { DocumentList } from '../../types/documents'; import type { FileNode } from '../../types/files'; import { isTemporaryFile } from '../../utils/fileUtils'; +import { fileStorageService } from '../../services/FileStorageService'; import { ChevronDownIcon, ClearCompileIcon, PlayIcon, StopIcon, TrashIcon } from '../common/Icons'; interface LaTeXCompileButtonProps { @@ -59,6 +60,10 @@ const LaTeXCompileButton: React.FC = ({ const compileButtonRef = useRef<{ clearAndCompile: () => void }>(); const dropdownRef = useRef(null); + const effectiveAutoCompileOnSave = useSharedSettings + ? doc?.projectMetadata?.autoCompileOnSave ?? false + : false; + const projectMainFile = useSharedSettings ? doc?.projectMetadata?.mainFile : undefined; const projectEngine = useSharedSettings ? doc?.projectMetadata?.latexEngine : undefined; const effectiveEngine = projectEngine || latexEngine; @@ -128,6 +133,56 @@ const LaTeXCompileButton: React.FC = ({ }; }, []); + // Listen for save events and auto-compile if enabled + useEffect(() => { + if (!useSharedSettings || !effectiveAutoCompileOnSave || !effectiveMainFile) return; + + const handleFileSaved = async (event: Event) => { + if (isCompiling) return; + + try { + const customEvent = event as CustomEvent; + const detail = customEvent.detail; + + if (!detail) return; + + const candidatePath = detail.isFile + ? detail.fileId + ? detail.filePath || + (await fileStorageService.getFile(detail.fileId))?.path + : undefined + : linkedFileInfo?.filePath ?? detail.filePath; + + if (!candidatePath?.endsWith('.tex')) return; + + const mainFileToCompile = + detail.isFile ? effectiveMainFile : candidatePath; + + setTimeout(async () => { + if (onExpandLatexOutput) { + onExpandLatexOutput(); + } + await compileDocument(mainFileToCompile); + }, 120); + } catch (error) { + console.error('Error in auto-compile on save:', error); + } + }; + + document.addEventListener('file-saved', handleFileSaved); + return () => { + document.removeEventListener('file-saved', handleFileSaved); + }; + }, [ + useSharedSettings, + effectiveAutoCompileOnSave, + effectiveMainFile, + isCompiling, + compileDocument, + onExpandLatexOutput, + linkedFileInfo, + ]); + const shouldNavigateToMain = async (mainFilePath: string): Promise => { const navigationSetting = getSetting('latex-auto-navigate-to-main')?.value as string ?? 'conditional'; @@ -320,6 +375,17 @@ const LaTeXCompileButton: React.FC = ({ }); }; + const handleAutoCompileOnSaveChange = (checked: boolean) => { + if (!useSharedSettings || !changeDoc) return; + + changeDoc((d) => { + if (!d.projectMetadata) { + d.projectMetadata = { name: '', description: '' }; + } + d.projectMetadata.autoCompileOnSave = checked; + }); + }; + const getFileName = (path?: string) => { if (!path) return 'No .tex file'; return path.split('/').pop() || path; @@ -442,6 +508,20 @@ const LaTeXCompileButton: React.FC = ({ )} + {useSharedSettings && ( +
+ +
+ )} +
= ({ const projectFormat = useSharedSettings ? doc?.projectMetadata?.typstOutputFormat : undefined; const [localFormat, setLocalFormat] = useState('pdf'); const effectiveFormat = projectFormat || localFormat; + const effectiveAutoCompileOnSave = useSharedSettings + ? doc?.projectMetadata?.typstAutoCompileOnSave ?? false + : false; useEffect(() => { const findTypstFiles = (nodes: FileNode[]): string[] => { @@ -115,6 +119,56 @@ const TypstCompileButton: React.FC = ({ }; }, []); + useEffect(() => { + if (!useSharedSettings || !effectiveAutoCompileOnSave || !effectiveMainFile) return; + + const handleFileSaved = async (event: Event) => { + if (isCompiling) return; + + try { + const customEvent = event as CustomEvent; + const detail = customEvent.detail; + + if (!detail) return; + + const candidatePath = detail.isFile + ? detail.fileId + ? detail.filePath || + (await fileStorageService.getFile(detail.fileId))?.path + : undefined + : linkedFileInfo?.filePath ?? detail.filePath; + + if (!candidatePath?.endsWith('.typ')) return; + + const mainFileToCompile = + detail.isFile ? effectiveMainFile : candidatePath; + const targetFormat = effectiveFormat; + setTimeout(async () => { + if (onExpandTypstOutput) { + onExpandTypstOutput(); + } + await compileDocument(mainFileToCompile, targetFormat); + }, 120); + } catch (error) { + console.error('Error in Typst auto-compile on save:', error); + } + }; + + document.addEventListener('file-saved', handleFileSaved); + return () => { + document.removeEventListener('file-saved', handleFileSaved); + }; + }, [ + useSharedSettings, + effectiveAutoCompileOnSave, + effectiveMainFile, + effectiveFormat, + isCompiling, + compileDocument, + onExpandTypstOutput, + linkedFileInfo, + ]); + const shouldNavigateToMain = async (mainFilePath: string): Promise => { const navigationSetting = getSetting('typst-auto-navigate-to-main')?.value as string ?? 'conditional'; @@ -264,6 +318,17 @@ const TypstCompileButton: React.FC = ({ }); }; + const handleAutoCompileOnSaveChange = (checked: boolean) => { + if (!useSharedSettings || !changeDoc) return; + + changeDoc((d) => { + if (!d.projectMetadata) { + d.projectMetadata = { name: '', description: '' }; + } + d.projectMetadata.typstAutoCompileOnSave = checked; + }); + }; + const getFileName = (path?: string) => { if (!path) return 'No .typ file'; return path.split('/').pop() || path; @@ -390,6 +455,18 @@ const TypstCompileButton: React.FC = ({ )}
+ {useSharedSettings && ( + + )} +
setShowSaveIndicator(false), 1500); + + document.dispatchEvent( + new CustomEvent('file-saved', { + detail: { + isFile: true, + fileId: currentFileId, + }, + }), + ); } catch (error) { console.error('Error saving file:', error); } @@ -130,6 +139,17 @@ export const EditorLoader = ( setShowSaveIndicator(true); setTimeout(() => setShowSaveIndicator(false), 1500); + + document.dispatchEvent( + new CustomEvent('file-saved', { + detail: { + isFile: false, + documentId, + fileId: linkedFile.id, + filePath: linkedFile.path, + }, + }), + ); } } catch (error) { console.error('Error saving document to linked file:', error); @@ -675,19 +695,12 @@ export const EditorLoader = ( { enabled: true, delay: autoSaveDelay, - onSave: async (saveKey, content) => { + onSave: async (_saveKey, content) => { if (isEditingFile && currentFileId) { - const encoder = new TextEncoder(); - const contentBuffer = encoder.encode(content).buffer; - await fileStorageService.updateFileContent( - currentFileId, - contentBuffer, - ); + await saveFileToStorage(content); } else if (!isEditingFile && documentId) { await saveDocumentToLinkedFile(content); } - setShowSaveIndicator(true); - setTimeout(() => setShowSaveIndicator(false), 1500); }, onError: (error) => { console.error('Auto-save failed:', error); diff --git a/src/styles/components/latex-typst.css b/src/styles/components/latex-typst.css index 8766cdc..0618a4d 100644 --- a/src/styles/components/latex-typst.css +++ b/src/styles/components/latex-typst.css @@ -303,7 +303,8 @@ embed { cursor: not-allowed; } -.share-checkbox { +.share-checkbox, +.auto-compile-checkbox { display: flex; align-items: center; gap: 0.5rem; @@ -312,11 +313,13 @@ embed { cursor: pointer; } -.share-checkbox input[type="checkbox"] { +.share-checkbox input[type="checkbox"], +.auto-compile-checkbox input[type="checkbox"] { margin: 0; } -.share-checkbox:has(input:disabled) { +.share-checkbox:has(input:disabled), +.auto-compile-checkbox:has(input:disabled) { opacity: 0.6; cursor: not-allowed; } diff --git a/src/types/documents.ts b/src/types/documents.ts index cbe9c5e..4437c6c 100644 --- a/src/types/documents.ts +++ b/src/types/documents.ts @@ -20,5 +20,7 @@ export interface DocumentList { latexEngine?: 'pdftex' | 'xetex' | 'luatex'; typstEngine?: string; typstOutputFormat?: 'pdf' | 'svg' | 'canvas'; + autoCompileOnSave?: boolean; + typstAutoCompileOnSave?: boolean; }; }