diff --git a/packages/ui/src/components/views/FilesView.tsx b/packages/ui/src/components/views/FilesView.tsx index 6678cfc4..b3aea205 100644 --- a/packages/ui/src/components/views/FilesView.tsx +++ b/packages/ui/src/components/views/FilesView.tsx @@ -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 ; @@ -649,6 +664,7 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { const autoSaveTimerRef = React.useRef | null>(null); const lastLoadedFileStatRef = React.useRef(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(null); @@ -1331,12 +1347,32 @@ export const FilesView: React.FC = ({ 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 = ({ 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 = ({ mode = 'full' }) => { return (
{canEdit && textViewMode === 'edit' && ( - isSaving ? ( - - - {t('filesView.editor.saving')} - - ) : autoSaveStatus === 'saved' && !isDirty ? ( - - - {t('filesView.editor.saved')} - - ) : isDirty ? ( + <> + {isSaving ? ( + + + {t('filesView.editor.saving')} + + ) : autoSaveEnabled && autoSaveStatus === 'saved' && !isDirty ? ( + + + {t('filesView.editor.saved')} + + ) : isDirty ? ( + + ) : null} - ) : null + )} diff --git a/packages/ui/src/lib/i18n/messages/en.ts b/packages/ui/src/lib/i18n/messages/en.ts index 23f88208..137ca088 100644 --- a/packages/ui/src/lib/i18n/messages/en.ts +++ b/packages/ui/src/lib/i18n/messages/en.ts @@ -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', diff --git a/packages/ui/src/lib/i18n/messages/es.ts b/packages/ui/src/lib/i18n/messages/es.ts index 51781ba8..a165429d 100644 --- a/packages/ui/src/lib/i18n/messages/es.ts +++ b/packages/ui/src/lib/i18n/messages/es.ts @@ -817,7 +817,10 @@ export const dict: Record = { "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", diff --git a/packages/ui/src/lib/i18n/messages/ko.ts b/packages/ui/src/lib/i18n/messages/ko.ts index d6221e14..f8d29b18 100644 --- a/packages/ui/src/lib/i18n/messages/ko.ts +++ b/packages/ui/src/lib/i18n/messages/ko.ts @@ -854,7 +854,10 @@ export const dict: Record = { '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': '앱 새로고침', diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.ts b/packages/ui/src/lib/i18n/messages/pt-BR.ts index 910256cd..bc5ba74a 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.ts @@ -817,7 +817,10 @@ export const dict: Record = { "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", diff --git a/packages/ui/src/lib/i18n/messages/uk.ts b/packages/ui/src/lib/i18n/messages/uk.ts index 146e8f9d..5119df5a 100644 --- a/packages/ui/src/lib/i18n/messages/uk.ts +++ b/packages/ui/src/lib/i18n/messages/uk.ts @@ -817,7 +817,10 @@ export const dict: Record = { "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": "Оновити застосунки", diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.ts b/packages/ui/src/lib/i18n/messages/zh-CN.ts index 9e028d27..a4af4128 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.ts @@ -817,7 +817,10 @@ export const dict: Record = { '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': '刷新应用',