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}>
|
||||
|
||||
@@ -851,7 +851,10 @@ export const dict = {
|
||||
'filesView.dialog.delete.confirm': 'Delete',
|
||||
'filesView.editor.saving': 'Saving...',
|
||||
'filesView.editor.saved': 'Saved',
|
||||
'filesView.editor.autoSaveOn': 'Auto-save on',
|
||||
'filesView.editor.manualSave': 'Manual save',
|
||||
'filesView.editor.saveNowTitle': 'Save now ({shortcut}) - auto-saves after 1.5s',
|
||||
'filesView.editor.saveNowManualTitle': 'Save now ({shortcut})',
|
||||
'filesView.editor.saveAria': 'Save ({shortcut})',
|
||||
'filesView.editor.openInDesktopApp': 'Open in desktop app',
|
||||
'filesView.editor.refreshApps': 'Refresh Apps',
|
||||
|
||||
@@ -817,7 +817,10 @@ export const dict: Record<I18nKey, string> = {
|
||||
"filesView.dialog.delete.confirm": "Eliminar",
|
||||
"filesView.editor.saving": "Guardando...",
|
||||
"filesView.editor.saved": "Guardado",
|
||||
"filesView.editor.autoSaveOn": "Guardado automático activado",
|
||||
"filesView.editor.manualSave": "Guardado manual",
|
||||
"filesView.editor.saveNowTitle": "Guardar ahora ({shortcut}) - se guarda automáticamente después de 1.5 segundos",
|
||||
"filesView.editor.saveNowManualTitle": "Guardar ahora ({shortcut})",
|
||||
"filesView.editor.saveAria": "Guardar ({shortcut})",
|
||||
"filesView.editor.openInDesktopApp": "Abrir en la aplicación de escritorio",
|
||||
"filesView.editor.refreshApps": "Actualizar aplicaciones",
|
||||
|
||||
@@ -854,7 +854,10 @@ export const dict: Record<I18nKey, string> = {
|
||||
'filesView.dialog.delete.confirm': '삭제',
|
||||
'filesView.editor.saving': '저장 중…',
|
||||
'filesView.editor.saved': '저장됨',
|
||||
'filesView.editor.autoSaveOn': '자동 저장 켜짐',
|
||||
'filesView.editor.manualSave': '수동 저장',
|
||||
'filesView.editor.saveNowTitle': '지금 저장({shortcut}) · 1.5초 후 자동 저장',
|
||||
'filesView.editor.saveNowManualTitle': '지금 저장({shortcut})',
|
||||
'filesView.editor.saveAria': '저장 ({shortcut})',
|
||||
'filesView.editor.openInDesktopApp': '데스크톱 앱에서 열기',
|
||||
'filesView.editor.refreshApps': '앱 새로고침',
|
||||
|
||||
@@ -817,7 +817,10 @@ export const dict: Record<I18nKey, string> = {
|
||||
"filesView.dialog.delete.confirm": "Excluir",
|
||||
"filesView.editor.saving": "Salvando...",
|
||||
"filesView.editor.saved": "Salvo",
|
||||
"filesView.editor.autoSaveOn": "Salvamento automático ativado",
|
||||
"filesView.editor.manualSave": "Salvamento manual",
|
||||
"filesView.editor.saveNowTitle": "Salvar agora ({shortcut}) - salva automaticamente após 1,5 segundo",
|
||||
"filesView.editor.saveNowManualTitle": "Salvar agora ({shortcut})",
|
||||
"filesView.editor.saveAria": "Salvar ({shortcut})",
|
||||
"filesView.editor.openInDesktopApp": "Abrir no aplicativo de desktop",
|
||||
"filesView.editor.refreshApps": "Atualizar aplicativos",
|
||||
|
||||
@@ -817,7 +817,10 @@ export const dict: Record<I18nKey, string> = {
|
||||
"filesView.dialog.delete.confirm": "Видалити",
|
||||
"filesView.editor.saving": "Збереження...",
|
||||
"filesView.editor.saved": "Збережено",
|
||||
"filesView.editor.autoSaveOn": "Автозбереження ввімкнено",
|
||||
"filesView.editor.manualSave": "Ручне збереження",
|
||||
"filesView.editor.saveNowTitle": "Зберегти зараз ({shortcut}) - автоматично зберігає через 1,5 с",
|
||||
"filesView.editor.saveNowManualTitle": "Зберегти зараз ({shortcut})",
|
||||
"filesView.editor.saveAria": "Зберегти ({shortcut})",
|
||||
"filesView.editor.openInDesktopApp": "Відкрити в десктопному застосунку",
|
||||
"filesView.editor.refreshApps": "Оновити застосунки",
|
||||
|
||||
@@ -817,7 +817,10 @@ export const dict: Record<I18nKey, string> = {
|
||||
'filesView.dialog.delete.confirm': '删除',
|
||||
'filesView.editor.saving': '保存中...',
|
||||
'filesView.editor.saved': '已保存',
|
||||
'filesView.editor.autoSaveOn': '自动保存已开启',
|
||||
'filesView.editor.manualSave': '手动保存',
|
||||
'filesView.editor.saveNowTitle': '立即保存({shortcut})- 1.5 秒后自动保存',
|
||||
'filesView.editor.saveNowManualTitle': '立即保存({shortcut})',
|
||||
'filesView.editor.saveAria': '保存({shortcut})',
|
||||
'filesView.editor.openInDesktopApp': '在桌面应用中打开',
|
||||
'filesView.editor.refreshApps': '刷新应用',
|
||||
|
||||
Reference in New Issue
Block a user