feat: add manual save mode to file editor
Added a persistent autosave toggle for file editing Manual save mode applies across current and future opened files Updated localized toolbar labels
This commit is contained in:
@@ -10,6 +10,8 @@ import {
|
||||
RiFolder3Fill,
|
||||
RiFolderOpenFill,
|
||||
RiFolderReceivedLine,
|
||||
RiFileCheckFill,
|
||||
RiFileCheckLine,
|
||||
RiFullscreenExitLine,
|
||||
RiFullscreenLine,
|
||||
RiLoader4Line,
|
||||
@@ -266,6 +268,19 @@ const isDirectoryReadError = (error: unknown): boolean => {
|
||||
};
|
||||
|
||||
const MAX_VIEW_CHARS = 200_000;
|
||||
const FILE_EDITOR_AUTO_SAVE_KEY = 'openchamber:files:auto-save-enabled';
|
||||
|
||||
const getInitialAutoSaveEnabled = (): boolean => {
|
||||
if (typeof window === 'undefined') {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
return window.localStorage.getItem(FILE_EDITOR_AUTO_SAVE_KEY) !== 'false';
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const getFileIcon = (filePath: string, extension?: string): React.ReactNode => {
|
||||
return <FileTypeIcon filePath={filePath} extension={extension} />;
|
||||
@@ -649,6 +664,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
const autoSaveTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const lastLoadedFileStatRef = React.useRef<FileStatSnapshot | null>(null);
|
||||
const [autoSaveStatus, setAutoSaveStatus] = React.useState<'idle' | 'saved'>('idle');
|
||||
const [autoSaveEnabled, setAutoSaveEnabled] = React.useState(getInitialAutoSaveEnabled);
|
||||
|
||||
const [confirmDiscardOpen, setConfirmDiscardOpen] = React.useState(false);
|
||||
const pendingSelectFileRef = React.useRef<FileNode | null>(null);
|
||||
@@ -1331,12 +1347,32 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
};
|
||||
}, [isDirty, setMainTabGuard]);
|
||||
|
||||
React.useEffect(() => {
|
||||
try {
|
||||
window.localStorage.setItem(FILE_EDITOR_AUTO_SAVE_KEY, autoSaveEnabled ? 'true' : 'false');
|
||||
} catch {
|
||||
// Ignore localStorage errors; the in-memory preference still applies.
|
||||
}
|
||||
}, [autoSaveEnabled]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (autoSaveEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
setAutoSaveStatus('idle');
|
||||
if (autoSaveTimerRef.current) {
|
||||
clearTimeout(autoSaveTimerRef.current);
|
||||
autoSaveTimerRef.current = null;
|
||||
}
|
||||
}, [autoSaveEnabled]);
|
||||
|
||||
// Auto-save: debounce 1.5s after user stops typing
|
||||
const AUTO_SAVE_DELAY = 1500;
|
||||
|
||||
React.useEffect(() => {
|
||||
const canWrite = Boolean(selectedFile && files.writeFile);
|
||||
if (!isDirty || !canWrite || isSaving) {
|
||||
if (!autoSaveEnabled || !isDirty || !canWrite || isSaving) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1353,7 +1389,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
autoSaveTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [draftContent, isDirty, selectedFile, files.writeFile, isSaving, saveDraft]);
|
||||
}, [autoSaveEnabled, draftContent, isDirty, selectedFile, files.writeFile, isSaving, saveDraft]);
|
||||
|
||||
// Reset auto-save status when switching files
|
||||
React.useEffect(() => {
|
||||
@@ -2478,28 +2514,43 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
return (
|
||||
<div className="pointer-events-auto flex items-center gap-1 rounded-lg border border-[var(--interactive-border)] bg-[var(--surface-elevated)] p-1 shadow-sm">
|
||||
{canEdit && textViewMode === 'edit' && (
|
||||
isSaving ? (
|
||||
<span className="flex items-center gap-1 px-1 text-muted-foreground typography-meta">
|
||||
<RiLoader4Line className="h-3.5 w-3.5 animate-spin" />
|
||||
{t('filesView.editor.saving')}
|
||||
</span>
|
||||
) : autoSaveStatus === 'saved' && !isDirty ? (
|
||||
<span className="flex items-center gap-1 px-1 text-[color:var(--status-success)] typography-meta">
|
||||
<RiCheckLine className="h-3.5 w-3.5" />
|
||||
{t('filesView.editor.saved')}
|
||||
</span>
|
||||
) : isDirty ? (
|
||||
<>
|
||||
{isSaving ? (
|
||||
<span className="flex items-center gap-1 px-1 text-muted-foreground typography-meta">
|
||||
<RiLoader4Line className="h-3.5 w-3.5 animate-spin" />
|
||||
{t('filesView.editor.saving')}
|
||||
</span>
|
||||
) : autoSaveEnabled && autoSaveStatus === 'saved' && !isDirty ? (
|
||||
<span className="flex items-center gap-1 px-1 text-[color:var(--status-success)] typography-meta">
|
||||
<RiCheckLine className="h-3.5 w-3.5" />
|
||||
{t('filesView.editor.saved')}
|
||||
</span>
|
||||
) : isDirty ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => void saveDraft()}
|
||||
className="h-6 gap-1 px-1 text-muted-foreground opacity-80 hover:bg-transparent hover:opacity-100 focus-visible:bg-transparent active:bg-transparent"
|
||||
title={t(autoSaveEnabled ? 'filesView.editor.saveNowTitle' : 'filesView.editor.saveNowManualTitle', { shortcut: `${getModifierLabel()}+S` })}
|
||||
aria-label={t('filesView.editor.saveAria', { shortcut: `${getModifierLabel()}+S` })}
|
||||
>
|
||||
<RiSave3Line className="h-4 w-4" />
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => void saveDraft()}
|
||||
className="h-6 gap-1 px-1 text-muted-foreground opacity-80 hover:bg-transparent hover:opacity-100 focus-visible:bg-transparent active:bg-transparent"
|
||||
title={t('filesView.editor.saveNowTitle', { shortcut: `${getModifierLabel()}+S` })}
|
||||
aria-label={t('filesView.editor.saveAria', { shortcut: `${getModifierLabel()}+S` })}
|
||||
onClick={() => setAutoSaveEnabled((enabled) => !enabled)}
|
||||
className={cn(
|
||||
'h-6 w-6 p-0 transition-opacity hover:bg-transparent focus-visible:bg-transparent active:bg-transparent',
|
||||
autoSaveEnabled ? 'text-foreground opacity-100' : 'text-muted-foreground opacity-65 hover:opacity-100'
|
||||
)}
|
||||
title={autoSaveEnabled ? t('filesView.editor.autoSaveOn') : t('filesView.editor.manualSave')}
|
||||
aria-label={autoSaveEnabled ? t('filesView.editor.autoSaveOn') : t('filesView.editor.manualSave')}
|
||||
>
|
||||
<RiSave3Line className="h-4 w-4" />
|
||||
{autoSaveEnabled ? <RiFileCheckFill className="size-4" /> : <RiFileCheckLine className="size-4" />}
|
||||
</Button>
|
||||
) : null
|
||||
</>
|
||||
)}
|
||||
|
||||
<DropdownMenu onOpenChange={handleToolbarDropdownOpenChange}>
|
||||
|
||||
Reference in New Issue
Block a user