more stuff to improve robustness
This commit is contained in:
@ -44,37 +44,38 @@ const LAST_MEDIA_PATH_KEY = 'talkedit:lastMediaPath';
|
||||
type Panel = 'ai' | 'settings' | 'export' | 'silence' | 'zones' | 'markers' | 'music' | 'append' | 'help' | null;
|
||||
|
||||
export default function App() {
|
||||
const {
|
||||
projectFilePath,
|
||||
videoPath,
|
||||
exportedAudioPath,
|
||||
words,
|
||||
segments,
|
||||
cutRanges,
|
||||
muteRanges,
|
||||
gainRanges,
|
||||
speedRanges,
|
||||
globalGainDb,
|
||||
silenceTrimGroups,
|
||||
transcriptionModel,
|
||||
language,
|
||||
isTranscribing,
|
||||
transcriptionStatus,
|
||||
markInTime,
|
||||
markOutTime,
|
||||
loadVideo,
|
||||
setProjectFilePath,
|
||||
setBackendUrl,
|
||||
clearMarkRange,
|
||||
setTranscription,
|
||||
setTranscriptionModel,
|
||||
setTranscribing,
|
||||
selectedWordIndices,
|
||||
addCutRange,
|
||||
addMuteRange,
|
||||
addGainRange,
|
||||
addSpeedRange,
|
||||
} = useEditorStore();
|
||||
const {
|
||||
projectFilePath,
|
||||
videoPath,
|
||||
exportedAudioPath,
|
||||
words,
|
||||
segments,
|
||||
cutRanges,
|
||||
muteRanges,
|
||||
gainRanges,
|
||||
speedRanges,
|
||||
globalGainDb,
|
||||
silenceTrimGroups,
|
||||
transcriptionModel,
|
||||
language,
|
||||
isTranscribing,
|
||||
transcriptionStatus,
|
||||
markInTime,
|
||||
markOutTime,
|
||||
loadVideo,
|
||||
setProjectFilePath,
|
||||
setBackendUrl,
|
||||
clearMarkRange,
|
||||
setTranscription,
|
||||
setTranscriptionModel,
|
||||
setTranscribing,
|
||||
selectedWordIndices,
|
||||
addCutRange,
|
||||
addMuteRange,
|
||||
addGainRange,
|
||||
addSpeedRange,
|
||||
backendUrl,
|
||||
} = useEditorStore();
|
||||
|
||||
const [activePanel, setActivePanel] = useState<Panel>(null);
|
||||
const [projectName, setProjectName] = useState<string | null>(null);
|
||||
@ -142,6 +143,10 @@ export default function App() {
|
||||
const [pendingProceedAction, setPendingProceedAction] = useState<(() => Promise<void>) | null>(null);
|
||||
const [lastSavedSignature, setLastSavedSignature] = useState<string | null>(null);
|
||||
const [showFileMenu, setShowFileMenu] = useState(false);
|
||||
const [showRecoveryDialog, setShowRecoveryDialog] = useState(false);
|
||||
const [recoveryData, setRecoveryData] = useState<any>(null);
|
||||
const [recoveryMinutesAgo, setRecoveryMinutesAgo] = useState(0);
|
||||
const [backendDown, setBackendDown] = useState(false);
|
||||
const canEdit = useLicenseStore((s) => s.canEdit);
|
||||
const licenseStatus = useLicenseStore((s) => s.status);
|
||||
const setShowLicenseDialog = useLicenseStore((s) => s.setShowDialog);
|
||||
@ -180,7 +185,10 @@ export default function App() {
|
||||
const hasUnsavedChanges = Boolean(projectSignature) && projectSignature !== lastSavedSignature;
|
||||
|
||||
const loadProjectFromData = (data: any) => {
|
||||
useEditorStore.getState().loadProject(data);
|
||||
const removedCount = useEditorStore.getState().loadProject(data);
|
||||
if (removedCount > 0) {
|
||||
window.alert(`${removedCount} invalid zones were removed from the loaded project.`);
|
||||
}
|
||||
const loadedSignature = JSON.stringify({
|
||||
videoPath: data.videoPath,
|
||||
exportedAudioPath: data.exportedAudioPath ?? null,
|
||||
@ -211,6 +219,18 @@ export default function App() {
|
||||
|
||||
useEffect(() => {
|
||||
useLicenseStore.getState().checkStatus();
|
||||
window.electronAPI?.readAutosave().then((data) => {
|
||||
if (data) {
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
const savedAt = parsed.savedAt;
|
||||
const minutesAgo = savedAt ? Math.round((Date.now() - savedAt) / 60000) : 0;
|
||||
setRecoveryData(parsed);
|
||||
setRecoveryMinutesAgo(minutesAgo);
|
||||
setShowRecoveryDialog(true);
|
||||
} catch {}
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Handle Escape key to exit timeline zone modes and close menus
|
||||
@ -248,6 +268,36 @@ export default function App() {
|
||||
sessionStorage.removeItem(LAST_MEDIA_PATH_KEY);
|
||||
}, [videoPath]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!videoPath) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
const state = useEditorStore.getState();
|
||||
const hasData = state.words.length > 0 || state.cutRanges.length > 0 || state.muteRanges.length > 0 || state.gainRanges.length > 0 || state.speedRanges.length > 0;
|
||||
if (!hasData) return;
|
||||
const autosaveData = {
|
||||
savedAt: Date.now(),
|
||||
videoPath: state.videoPath,
|
||||
words: state.words,
|
||||
segments: state.segments,
|
||||
cutRanges: state.cutRanges,
|
||||
muteRanges: state.muteRanges,
|
||||
gainRanges: state.gainRanges,
|
||||
speedRanges: state.speedRanges,
|
||||
globalGainDb: state.globalGainDb,
|
||||
silenceTrimGroups: state.silenceTrimGroups,
|
||||
transcriptionModel: state.transcriptionModel,
|
||||
language: state.language,
|
||||
markInTime: state.markInTime,
|
||||
markOutTime: state.markOutTime,
|
||||
timelineMarkers: state.timelineMarkers,
|
||||
};
|
||||
window.electronAPI.writeAutosave(JSON.stringify(autosaveData));
|
||||
}, 60000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [videoPath]);
|
||||
|
||||
const handleLoadProject = async () => {
|
||||
await runGuarded(async () => {
|
||||
try {
|
||||
@ -404,6 +454,26 @@ export default function App() {
|
||||
setPendingProceedAction(null);
|
||||
};
|
||||
|
||||
const handleRecoverAutosave = () => {
|
||||
if (!recoveryData) return;
|
||||
const data = recoveryData;
|
||||
const removedCount = useEditorStore.getState().loadProject(data);
|
||||
if (removedCount > 0) {
|
||||
window.alert(`${removedCount} invalid zones were removed from the loaded project.`);
|
||||
}
|
||||
if (data.markInTime != null) useEditorStore.getState().setMarkInTime(data.markInTime);
|
||||
if (data.markOutTime != null) useEditorStore.getState().setMarkOutTime(data.markOutTime);
|
||||
window.electronAPI.deleteAutosave();
|
||||
setShowRecoveryDialog(false);
|
||||
setRecoveryData(null);
|
||||
};
|
||||
|
||||
const handleDismissRecovery = () => {
|
||||
window.electronAPI.deleteAutosave();
|
||||
setShowRecoveryDialog(false);
|
||||
setRecoveryData(null);
|
||||
};
|
||||
|
||||
const togglePanel = (panel: Panel) => {
|
||||
setActivePanel((prev) => (prev === panel ? null : panel));
|
||||
};
|
||||
@ -606,6 +676,21 @@ export default function App() {
|
||||
);
|
||||
}
|
||||
|
||||
// Health check timer
|
||||
useEffect(() => {
|
||||
const checkHealth = async () => {
|
||||
try {
|
||||
const res = await fetch(`${backendUrl}/health`);
|
||||
setBackendDown(!res.ok);
|
||||
} catch {
|
||||
setBackendDown(true);
|
||||
}
|
||||
};
|
||||
checkHealth();
|
||||
const interval = setInterval(checkHealth, 30000);
|
||||
return () => clearInterval(interval);
|
||||
}, [videoPath, backendUrl]);
|
||||
|
||||
return (
|
||||
<div className="h-screen flex flex-col bg-editor-bg overflow-hidden">
|
||||
{/* Top bar */}
|
||||
@ -1009,6 +1094,44 @@ export default function App() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showRecoveryDialog && (
|
||||
<div
|
||||
className="fixed inset-0 z-[80] flex items-center justify-center bg-black/60 px-4"
|
||||
onClick={handleDismissRecovery}
|
||||
>
|
||||
<div
|
||||
className="w-full max-w-md rounded-xl border border-editor-border bg-editor-bg p-4 space-y-3"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<h3 className="text-sm font-semibold">Recover unsaved work?</h3>
|
||||
<p className="text-xs text-editor-text-muted leading-relaxed">
|
||||
TalkEdit recovered a project from {recoveryMinutesAgo} minute{recoveryMinutesAgo !== 1 ? 's' : ''} ago.
|
||||
</p>
|
||||
<div className="flex items-center justify-end gap-2 pt-1">
|
||||
<button
|
||||
onClick={handleDismissRecovery}
|
||||
className="px-3 py-1.5 rounded-md text-xs text-editor-text-muted hover:text-editor-text hover:bg-editor-surface"
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
<button
|
||||
onClick={handleRecoverAutosave}
|
||||
className="px-3 py-1.5 rounded-md text-xs bg-editor-accent hover:bg-editor-accent-hover text-white"
|
||||
>
|
||||
Recover
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{backendDown && (
|
||||
<div className="fixed bottom-0 left-0 right-0 z-[90] flex items-center justify-center gap-2 px-4 py-2 bg-amber-500/15 border-t border-amber-500/30 text-amber-400 text-xs font-medium">
|
||||
<AlertTriangle className="w-3.5 h-3.5" />
|
||||
Backend disconnected — retrying...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user