diff --git a/app/page.tsx b/app/page.tsx index e0ee1c9..a3c4716 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -3,80 +3,56 @@ import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import Image from 'next/image'; -import { useEffect, useMemo, useState } from 'react'; +import { useState } from 'react'; import { checkForAppUpdates } from '@/lib/updater'; - -import { open } from '@tauri-apps/plugin-dialog'; import { useTauriAppVersion } from '@/hooks/useTauriApp'; + import { FileArea } from '@/components/file-area'; import { WritingArea } from '@/components/writing-area'; import { ThemeToggle } from '@/components/theme-toggle'; import { LanguageToggle } from '@/components/language-toggle'; -import { - Sheet, - SheetContent, - SheetHeader, - SheetTitle, - SheetTrigger, -} from '@/components/ui/sheet'; import { ResizableHandle, ResizablePanel, ResizablePanelGroup, } from '@/components/ui/resizable'; -import { FolderOpen, PanelLeft, Sparkles } from 'lucide-react'; +import { FolderOpen, Sparkles } from 'lucide-react'; import { Card, CardContent, - CardDescription, CardHeader, CardTitle, } from '@/components/ui/card'; import { Separator } from '@/components/ui/separator'; import { useI18n } from '@/hooks/useI18n'; +import { useWorkspaceStore, useSettingsStore } from '@/lib/stores'; +import { selectFolderName, selectActiveFileName } from '@/lib/stores/workspace-store'; export default function Home() { const { t } = useI18n(); const appVersion = useTauriAppVersion(); const [isCheckingUpdate, setIsCheckingUpdate] = useState(false); - const [folderPath, setFolderPath] = useState(null); - const [selectedFilePath, setSelectedFilePath] = useState(null); - const [isFileSheetOpen, setIsFileSheetOpen] = useState(false); - // 使用 useEffect 在组件挂载时检查更新 - useEffect(() => { - checkForAppUpdates(false); - }, []); + const workspaceState = useWorkspaceStore() + const { folderPath } = workspaceState + const { autosaveEnabled } = useSettingsStore() - const openFolder = async () => { + const handleFolderSelect = async () => { + const { open } = await import('@tauri-apps/plugin-dialog'); const result = await open({ directory: true, multiple: false, }); if (typeof result === 'string') { + const { setFolderPath } = useWorkspaceStore.getState(); setFolderPath(result); - setSelectedFilePath(null); } }; - const handleFileSelect = (filePath: string) => { - setSelectedFilePath(filePath); - setIsFileSheetOpen(false); - }; - - const folderName = useMemo(() => { - if (!folderPath) return null; - const parts = folderPath.split(/[\\/]/); - return parts[parts.length - 1] || folderPath; - }, [folderPath]); - - const fileName = useMemo(() => { - if (!selectedFilePath) return null; - const parts = selectedFilePath.split(/[\\/]/); - return parts[parts.length - 1] || selectedFilePath; - }, [selectedFilePath]); + const folderName = selectFolderName(workspaceState) + const fileName = selectActiveFileName(workspaceState) return (
@@ -94,12 +70,15 @@ export default function Home() { height={32} priority /> -
+

OnlyWrite

{t('app.subtitle')}

+
+ +
{folderName && ( @@ -113,57 +92,38 @@ export default function Home() { )}
-
- {folderPath && ( - - - - - - - {t('actions.openFiles')} - - - - - )} - - +
+ {folderPath && ( + + )} -
- - -
+
+ + +
- +
@@ -192,13 +152,13 @@ export default function Home() { {t('app.welcomeTitle')} - {t('app.welcomeLead')} - +
@@ -216,11 +176,13 @@ export default function Home() { - + - {t('app.efficiencyTitle')} + + {t('app.efficiencyTitle')} + - +
@@ -258,27 +220,18 @@ export default function Home() {
- +
- +
- +
)} @@ -291,7 +244,7 @@ export default function Home() { - {t('editor.autosave')} + {autosaveEnabled ? t('editor.autosave') : t('editor.autosaveOff')}
diff --git a/components/file-area.tsx b/components/file-area.tsx index caca282..1e383ed 100644 --- a/components/file-area.tsx +++ b/components/file-area.tsx @@ -1,136 +1,126 @@ 'use client' -import React, { useEffect, useState, useCallback, useMemo } from 'react'; -import { readDir, writeTextFile } from '@tauri-apps/plugin-fs'; -import { join } from '@tauri-apps/api/path'; -import { Button } from './ui/button'; -import { Card, CardContent, CardHeader, CardTitle } from './ui/card'; -import { ScrollArea } from './ui/scroll-area'; -import { Badge } from './ui/badge'; -import { FolderOpen } from 'lucide-react'; -import { cn } from '@/lib/utils'; -import { useI18n } from '@/hooks/useI18n'; - -interface FileEntry { - name: string; - isFile: boolean; -} +import React, { useEffect, useCallback } from 'react' +import { readDir, writeTextFile } from '@tauri-apps/plugin-fs' +import { join } from '@tauri-apps/api/path' +import { Button } from './ui/button' +import { Card, CardContent, CardHeader, CardTitle } from './ui/card' +import { ScrollArea } from './ui/scroll-area' +import { Badge } from './ui/badge' +import { FolderOpen, File as FileIcon, Plus, RefreshCw } from 'lucide-react' +import { cn } from '@/lib/utils' +import { useI18n } from '@/hooks/useI18n' +import { useWorkspaceStore } from '@/lib/stores' +import { selectFileCount, selectActiveFileName, selectFolderName } from '@/lib/stores/workspace-store' +import { UXFeedback } from '@/lib/ux-feedback' -interface FileAreaProps { - folderPath: string | null; - onFileSelect: (filePath: string) => void; - className?: string; -} +export function FileArea({ className }: { className?: string }) { + const { t } = useI18n() + + const workspaceState = useWorkspaceStore() + + const fileCount = selectFileCount(workspaceState) + const activeFileName = selectActiveFileName(workspaceState) + const folderName = selectFolderName(workspaceState) -export function FileArea({ folderPath, onFileSelect, className }: FileAreaProps) { - const { t } = useI18n(); - const [files, setFiles] = useState([]); - const [selectedFile, setSelectedFile] = useState(null); + const { + folderPath, + files, + isLoadingFiles, + fileError, + setFiles, + setSelectedFile, + setIsLoadingFiles, + setFileError, + } = workspaceState const loadFiles = useCallback(async () => { - if (!folderPath) return; - + if (!folderPath) return + + setIsLoadingFiles(true) + setFileError(null) + try { - const entries = await readDir(folderPath); - const fileEntries: FileEntry[] = entries.map((entry) => ({ + const entries = await readDir(folderPath) + const fileEntries = entries.map((entry) => ({ name: entry.name, - isFile: entry.isFile - })); - - // 按照文件夹在前,文件在后,然后按名称排序 + isFile: entry.isFile, + })) + fileEntries.sort((a, b) => { if (a.isFile !== b.isFile) { - return a.isFile ? 1 : -1; + return a.isFile ? 1 : -1 } - return a.name.localeCompare(b.name); - }); - - setFiles(fileEntries); + return a.name.localeCompare(b.name) + }) + + setFiles(fileEntries) } catch (error) { - console.error(t('file.loadFailed'), error); + UXFeedback.handleError(error, 'Failed to load files') } - }, [folderPath, t]); + }, [folderPath, setFiles, setIsLoadingFiles, setFileError]) useEffect(() => { - if (folderPath) { - loadFiles(); - } - }, [folderPath, loadFiles]); - - const handleFileClick = async (fileName: string, isFile: boolean) => { - if (folderPath && isFile) { - const filePath = await join(folderPath, fileName); - setSelectedFile(fileName); - onFileSelect(filePath); - } - }; + loadFiles() + }, [folderPath, loadFiles]) - const handleCreateFile = async () => { - if (!folderPath) return; + const handleFileClick = useCallback( + async (fileName: string, isFile: boolean) => { + if (folderPath && isFile) { + const filePath = await join(folderPath, fileName) + setSelectedFile(filePath) + } + }, + [folderPath, setSelectedFile], + ) - const fileName = prompt(t('file.promptName')); - if (!fileName) return; + const handleCreateFile = useCallback(async () => { + if (!folderPath) return - // 确保文件名以 .md 结尾 - const finalFileName = fileName.endsWith('.md') ? fileName : `${fileName}.md`; - try { - const filePath = await join(folderPath, finalFileName); + const fileName = await UXFeedback.showSaveDialog({ + filters: [ + { + name: 'Markdown', + extensions: ['md'], + }, + ], + }) + + if (!fileName) return + + const finalFileName = fileName.endsWith('.md') ? fileName : `${fileName}.md` + + const filePath = await join(folderPath, finalFileName) + await writeTextFile( filePath, - '# ' + - finalFileName.replace('.md', '') + - '\n\n' + - t('file.newFileContent') + - '\n' - ); - - // 重新加载文件列表 - await loadFiles(); - - // 自动选择新创建的文件 - onFileSelect(filePath); - setSelectedFile(finalFileName); + '# ' + finalFileName.replace('.md', '') + '\n\n' + (t('file.newFileContent') || 'Start writing here...') + '\n' + ) + + await loadFiles() + setSelectedFile(filePath) + UXFeedback.success(t('file.created') || `Created ${finalFileName}`) } catch (error) { - console.error(t('file.createFailed'), error); - alert(t('file.createFailed') + error); + UXFeedback.handleError(error, 'Failed to create file') } - }; + }, [folderPath, loadFiles, setSelectedFile, t]) const getFileIcon = (fileName: string, isFile: boolean) => { if (!isFile) { return ( - + - ); + ) } - + if (fileName.endsWith('.md')) { - return ( - - - - ); + return } - - return ( - - - - ); - }; - - const fileCount = useMemo( - () => files.filter((entry) => entry.isFile).length, - [files] - ); - - const folderName = useMemo(() => { - if (!folderPath) return t('file.noFolder'); - const parts = folderPath.split(/[\\/]/); - return parts[parts.length - 1] || folderPath; - }, [folderPath, t]); + + return + } return (

- {t('file.folderLabel')} + {t('file.folderLabel') || 'FOLDER'}

- {folderName} + {folderName || (t('file.noFolder') || 'No folder selected')}
{folderPath && ( - {t('file.filesCount', { count: fileCount })} + {t('file.filesCount', { count: fileCount }) || `${fileCount} files`} )} {folderPath && ( @@ -162,7 +152,8 @@ export function FileArea({ folderPath, onFileSelect, className }: FileAreaProps) variant="outline" className="h-8 px-3 text-xs" > - {t('file.newFile')} + + {t('file.newFile') || 'New'} )}
@@ -170,51 +161,75 @@ export function FileArea({ folderPath, onFileSelect, className }: FileAreaProps) - -
- {folderPath ? ( -
- {files.map((entry, index) => { - const isActive = selectedFile === entry.name && entry.isFile - return ( - - ) - })} - {files.length === 0 && ( -
-
- -

- {t('file.emptyFolder')} -

-
-
- )} -
- ) : ( -
- -

- {t('file.chooseFolder')} -

-
- )} + {isLoadingFiles ? ( +
+ +

+ {t('file.loading') || 'Loading files...'} +

- + ) : fileError ? ( +
+ +

+ {fileError} +

+ +
+ ) : ( + +
+ {folderPath ? ( +
+ {files.map((entry, index) => { + const isEntryActive = entry.isFile && entry.name === activeFileName + return ( + + ) + })} + {files.length === 0 && ( +
+
+ +

+ {t('file.emptyFolder') || 'Empty folder'} +

+
+
+ )} +
+ ) : ( +
+ +

+ {t('file.chooseFolder') || 'Choose a folder'} +

+
+ )} +
+
+ )} - ); + ) } diff --git a/components/writing-area.tsx b/components/writing-area.tsx index 64f6d8b..ead01f8 100644 --- a/components/writing-area.tsx +++ b/components/writing-area.tsx @@ -1,10 +1,9 @@ 'use client' -import React, { useState, useEffect, useCallback } from 'react' +import React, { useEffect, useCallback, useRef } from 'react' import dynamic from 'next/dynamic' import { MDXEditorMethods } from '@mdxeditor/editor' import { Button } from './ui/button' -import { save } from '@tauri-apps/plugin-dialog' import { writeTextFile, readTextFile } from '@tauri-apps/plugin-fs' import { ToggleGroup, ToggleGroupItem } from './ui/toggle-group' import { Badge } from './ui/badge' @@ -14,81 +13,103 @@ import { Columns2, Eye, RefreshCw, Save, Settings } from 'lucide-react' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' import { useI18n } from '@/hooks/useI18n' -import { S3ConfigDialog } from './s3-config-dialog' import { useS3Config } from '@/hooks/useS3Config' +import { useWorkspaceStore, useEditorStore, useSettingsStore } from '@/lib/stores' +import { selectShowEditor, selectShowPreview } from '@/lib/stores/editor-store' +import { selectActiveFileName } from '@/lib/stores/workspace-store' +import { toast } from 'sonner' +import { useHotkeys } from 'react-hotkeys-hook' +import { S3ConfigDialog } from './s3-config-dialog' const MarkdownEditor = dynamic(() => import('./markdown-editor'), { ssr: false }) -interface WritingAreaProps { - folderPath: string | null - filePath: string | null -} - -export function WritingArea({ folderPath, filePath }: WritingAreaProps) { +export function WritingArea() { const { t } = useI18n() const { reloadConfig } = useS3Config() - const [markdown, setMarkdown] = useState('') - const [currentFilePath, setCurrentFilePath] = useState(null) - const [isModified, setIsModified] = useState(false) - const [lastSavedMarkdown, setLastSavedMarkdown] = useState('') - const [wordCount, setWordCount] = useState(0) - const [isSaving, setIsSaving] = useState(false) - const [viewMode, setViewMode] = useState<'edit' | 'render' | 'split'>('edit') - const editorRef = React.useRef(null) - // 计算字数 + const editorRef = useRef(null) + + const { + markdown, + isModified, + isSaving, + viewMode, + wordCount, + isLoadingContent, + error, + setMarkdown, + setIsModified, + setIsSaving, + setViewMode, + setWordCount, + setIsLoadingContent, + setError, + markAsSaved, + } = useEditorStore() + + const { folderPath, selectedFilePath } = useWorkspaceStore() + const { shortcuts } = useSettingsStore() + + const editorState = useEditorStore.getState() + const showEditor = selectShowEditor(editorState) + const showPreview = selectShowPreview(editorState) + const workspaceState = useWorkspaceStore.getState() + const fileName = selectActiveFileName(workspaceState) + const calculateWordCount = useCallback((text: string) => { - const words = text.trim().split(/\s+/).filter(word => word.length > 0) + const words = text.trim().split(/\s+/).filter((word: string) => word.length > 0) return words.length }, []) - useEffect(() => { - if (filePath) { - setCurrentFilePath(filePath) - readTextFile(filePath) - .then((content) => { - setMarkdown(content) - setLastSavedMarkdown(content) - setWordCount(calculateWordCount(content)) - setIsModified(false) - editorRef.current?.setMarkdown(content) - }) - .catch(console.error) + const loadFileContent = useCallback(async () => { + if (!selectedFilePath) { + setMarkdown('') + setError(null) + setIsModified(false) + return } - }, [filePath, calculateWordCount]) - // 自动保存功能 - useEffect(() => { - if (!isModified || !currentFilePath) return; + setIsLoadingContent(true) + setError(null) - const autoSaveTimer = setTimeout(async () => { - try { - const currentContent = editorRef.current?.getMarkdown() || markdown; - await writeTextFile(currentFilePath, currentContent); - setLastSavedMarkdown(currentContent); - setIsModified(false); - console.log('自动保存:', currentFilePath); - } catch (error) { - console.error('自动保存失败:', error); - } - }, 5000); // 5秒后自动保存 + try { + const content = await readTextFile(selectedFilePath) + setMarkdown(content) + markAsSaved() + setWordCount(calculateWordCount(content)) + editorRef.current?.setMarkdown(content) + } catch (error) { + console.error('Failed to load file:', error) + const errorMessage = t('editor.loadFailed') || 'Failed to load file' + setError(errorMessage) + toast.error(errorMessage) + } finally { + setIsLoadingContent(false) + } + }, [selectedFilePath, setMarkdown, setIsModified, markAsSaved, setWordCount, setIsLoadingContent, setError, calculateWordCount, t]) + + useEffect(() => { + loadFileContent() + }, [loadFileContent]) - return () => { - clearTimeout(autoSaveTimer); - }; - }, [isModified, currentFilePath, markdown]); + const handleContentChange = useCallback((content: string) => { + setMarkdown(content) + setIsModified(content !== useEditorStore.getState().lastSavedMarkdown) + setWordCount(calculateWordCount(content)) + }, [setMarkdown, setIsModified, setWordCount, calculateWordCount]) - // 快捷键支持 const handleSave = useCallback(async () => { - if (isSaving) return; - - setIsSaving(true); + if (isSaving) return + + setIsSaving(true) + setError(null) + try { - // 从编辑器获取最新内容 - const currentContent = editorRef.current?.getMarkdown() || markdown; - let savePath = currentFilePath; + const currentContent = editorRef.current?.getMarkdown() || markdown + let savePath = selectedFilePath if (!savePath) { + const { save } = await import('@tauri-apps/plugin-dialog') savePath = await save({ filters: [ { @@ -96,171 +117,211 @@ export function WritingArea({ folderPath, filePath }: WritingAreaProps) { extensions: ['md'], }, ], - }); + }) + + if (savePath) { + const { setSelectedFile } = useWorkspaceStore.getState() + setSelectedFile(savePath) + } } if (savePath) { - await writeTextFile(savePath, currentContent); - setCurrentFilePath(savePath); - setMarkdown(currentContent); - setLastSavedMarkdown(currentContent); - setIsModified(false); - setWordCount(calculateWordCount(currentContent)); - console.log('文件已保存:', savePath); + await writeTextFile(savePath, currentContent) + setMarkdown(currentContent) + markAsSaved() + setWordCount(calculateWordCount(currentContent)) + toast.success(t('editor.saved') || 'File saved') } } catch (error) { - console.error(t('editor.saveFailed'), error); - alert(t('editor.saveFailed') + error); + console.error('Failed to save file:', error) + const errorMessage = t('editor.saveFailed') || 'Failed to save file' + setError(errorMessage) + toast.error(errorMessage) } finally { - setIsSaving(false); + setIsSaving(false) } - }, [isSaving, markdown, currentFilePath, calculateWordCount, t]); - - useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - if (event.ctrlKey && event.key === 's') { - event.preventDefault(); - handleSave(); - } - }; - - document.addEventListener('keydown', handleKeyDown); - return () => { - document.removeEventListener('keydown', handleKeyDown); - }; - }, [handleSave]); + }, [isSaving, markdown, selectedFilePath, setMarkdown, markAsSaved, setWordCount, setIsSaving, setError, calculateWordCount, t]) const handleRestore = useCallback(async () => { - if (!currentFilePath) return; + if (!selectedFilePath) return + + setIsLoadingContent(true) + setError(null) try { - const content = await readTextFile(currentFilePath); - setMarkdown(content); - setLastSavedMarkdown(content); - setIsModified(false); - setWordCount(calculateWordCount(content)); - editorRef.current?.setMarkdown(content); + const content = await readTextFile(selectedFilePath) + setMarkdown(content) + markAsSaved() + setWordCount(calculateWordCount(content)) + editorRef.current?.setMarkdown(content) + toast.success(t('editor.restored') || 'File restored') } catch (error) { - console.error(t('editor.restoreFailed'), error); - alert(t('editor.restoreFailed') + error); + console.error('Failed to restore file:', error) + const errorMessage = t('editor.restoreFailed') || 'Failed to restore file' + setError(errorMessage) + toast.error(errorMessage) + } finally { + setIsLoadingContent(false) } - }, [currentFilePath, calculateWordCount, t]); + }, [selectedFilePath, setMarkdown, markAsSaved, setWordCount, setIsLoadingContent, setError, calculateWordCount, t]) - const handleContentChange = (content: string) => { - setMarkdown(content) - setIsModified(content !== lastSavedMarkdown) - setWordCount(calculateWordCount(content)) - } + useHotkeys(shortcuts.save, (e: KeyboardEvent) => { + if ((e.ctrlKey || e.metaKey) && e.key === 's') { + e.preventDefault() + handleSave() + } + }) - const showEditor = viewMode !== 'render' - const showPreview = viewMode !== 'edit' - const previewSource = - markdown.trim().length === 0 ? t('editor.previewEmpty') : markdown - const fileName = currentFilePath - ? currentFilePath.split(/[\\/]/).pop() - : t('editor.untitled') + const previewSource = markdown.trim().length === 0 + ? t('editor.previewEmpty') || 'Start writing to see preview...' + : markdown return (
-
-
- - {fileName} - - {isModified ? ( - {t('editor.unsaved')} - ) : ( - {t('editor.saved')} - )} +
+
+ + {fileName || (t('editor.untitled') || 'Untitled')} + + {isLoadingContent ? ( + + {t('editor.loading') || 'Loading...'} + + ) : isModified ? ( + + {t('editor.unsaved') || 'Unsaved'} + + ) : ( + + {t('editor.saved') || 'Saved'} + + )} +
+
+ {t('editor.wordCount', { count: wordCount })} ·{' '} + {selectedFilePath + ? selectedFilePath + : t('editor.notSavedToDisk') || 'Not saved to disk'} +
-
- {t('editor.wordCount', { count: wordCount })} ·{' '} - {currentFilePath ? currentFilePath : t('editor.notSavedToDisk')} +
+ { + if (value) setViewMode(value as 'edit' | 'render' | 'split') + }} + className="border-foreground" + > + + {t('actions.edit') || 'Edit'} + + + + + {t('actions.render') || 'Render'} + + + + + + {t('actions.split') || 'Split'} + + + + + + + + S3 Settings + + } + onConfigSaved={reloadConfig} + />
-
- { - if (value) setViewMode(value as typeof viewMode) - }} - className="border-foreground" - > - - {t('actions.edit')} - - - - {t('actions.render')} - - - - {t('actions.split')} - - - - - - - S3 Settings - - } - onConfigSaved={reloadConfig} - /> -
-
+ {error && ( +
+ {error} +
+ )} -
- {showEditor && ( -
- -
- )} - {showPreview && ( - -
- - {previewSource} - + {isLoadingContent ? ( +
+ +

+ {t('editor.loadingFile') || 'Loading file...'} +

+
+ ) : ( +
+ {showEditor && ( +
+
- - )} -
+ )} + {showPreview && ( + +
+ + {previewSource} + +
+
+ )} +
+ )} ) diff --git a/lib/stores/editor-store.ts b/lib/stores/editor-store.ts new file mode 100644 index 0000000..5401553 --- /dev/null +++ b/lib/stores/editor-store.ts @@ -0,0 +1,109 @@ +import { create } from 'zustand' +import { subscribeWithSelector } from 'zustand/middleware' + +/** + * Editor Store - manages markdown editor state + * + * Domain: Document content, modification status, view modes, and cursor state + * Persistence: Last opened file and view mode persisted to Tauri Store + */ +export type ViewMode = 'edit' | 'render' | 'split' + +export interface EditorState { + // Document content + markdown: string + lastSavedMarkdown: string + + // Modification state + isModified: boolean + isSaving: boolean + lastSaveTime: number | null + + // View mode + viewMode: ViewMode + + // Editor metadata + wordCount: number + isLoadingContent: boolean + error: string | null + + // Autosave control + autosaveEnabled: boolean + autosaveInterval: number // in seconds + + // Actions + setMarkdown: (content: string) => void + setLastSavedMarkdown: (content: string) => void + setIsModified: (modified: boolean) => void + setIsSaving: (saving: boolean) => void + setLastSaveTime: (time: number | null) => void + setViewMode: (mode: ViewMode) => void + setWordCount: (count: number) => void + setIsLoadingContent: (loading: boolean) => void + setError: (error: string | null) => void + setAutosaveEnabled: (enabled: boolean) => void + setAutosaveInterval: (seconds: number) => void + + // Composite actions + markAsSaved: () => void + resetEditor: () => void +} + +export const useEditorStore = create()( + subscribeWithSelector((set) => ({ + // Initial state + markdown: '', + lastSavedMarkdown: '', + isModified: false, + isSaving: false, + lastSaveTime: null, + viewMode: 'edit', + wordCount: 0, + isLoadingContent: false, + error: null, + autosaveEnabled: true, + autosaveInterval: 5, + + // Actions + setMarkdown: (content) => set({ markdown: content }), + setLastSavedMarkdown: (content) => set({ lastSavedMarkdown: content }), + setIsModified: (modified) => set({ isModified: modified }), + setIsSaving: (saving) => set({ isSaving: saving }), + setLastSaveTime: (time) => set({ lastSaveTime: time }), + setViewMode: (mode) => set({ viewMode: mode }), + setWordCount: (count) => set({ wordCount: count }), + setIsLoadingContent: (loading) => set({ isLoadingContent: loading }), + setError: (error) => set({ error }), + setAutosaveEnabled: (enabled) => set({ autosaveEnabled: enabled }), + setAutosaveInterval: (seconds) => set({ autosaveInterval: seconds }), + + // Composite actions + markAsSaved: () => + set((state) => ({ + lastSavedMarkdown: state.markdown, + isModified: false, + isSaving: false, + lastSaveTime: Date.now(), + })), + + resetEditor: () => + set({ + markdown: '', + lastSavedMarkdown: '', + isModified: false, + isSaving: false, + lastSaveTime: null, + wordCount: 0, + isLoadingContent: false, + error: null, + }), + })) +) + +// Selectors for derived state +export const selectIsDirty = (state: EditorState) => + state.markdown !== state.lastSavedMarkdown + +export const selectShowEditor = (state: EditorState) => state.viewMode !== 'render' +export const selectShowPreview = (state: EditorState) => state.viewMode !== 'edit' +export const selectShowSplitView = (state: EditorState) => state.viewMode === 'split' diff --git a/lib/stores/index.ts b/lib/stores/index.ts new file mode 100644 index 0000000..a8fcd0e --- /dev/null +++ b/lib/stores/index.ts @@ -0,0 +1,27 @@ +/** + * Stores Module + * + * This module exports all Zustand stores for the application. + * Each store represents a domain of state: + * - workspace: File system and navigation state + * - editor: Document and editor state + * - ui: Ephemeral UI state (modals, panels) + * - settings: User preferences and app settings + * + * Architecture: + * - Domain-based separation (no god store) + * - Type-safe with TypeScript + * - Selectors for derived state + * - Tauri Store persistence for long-lived state + * - Ephemeral state stays in-memory + */ + +export { useWorkspaceStore, selectActiveFileName, selectFolderName, selectFileCount } from './workspace-store' +export { useEditorStore, selectIsDirty, selectShowEditor, selectShowPreview, selectShowSplitView } from './editor-store' +export { useUIStore } from './ui-store' +export { useSettingsStore, persistSettings } from './settings-store' + +export type { WorkspaceState } from './workspace-store' +export type { EditorState, ViewMode } from './editor-store' +export type { UIState, Toast } from './ui-store' +export type { SettingsState } from './settings-store' diff --git a/lib/stores/settings-store.ts b/lib/stores/settings-store.ts new file mode 100644 index 0000000..754d248 --- /dev/null +++ b/lib/stores/settings-store.ts @@ -0,0 +1,145 @@ +import { invoke } from '@tauri-apps/api/core' +import { create } from 'zustand' +import { subscribeWithSelector } from 'zustand/middleware' + +export interface SettingsData { + theme: 'light' | 'dark' | 'system' + locale: string + fontSize: number + lineHeight: number + tabWidth: number + showLineNumbers: boolean + wordWrap: boolean + spellCheck: boolean + autosaveEnabled: boolean + autosaveInterval: number + shortcuts: { + save: string + newFile: string + openFile: string + toggleSidebar: string + toggleCommandPalette: string + } + rememberWindowSize: boolean + showMinimap: boolean +} + +export interface SettingsState extends SettingsData { + setTheme: (theme: SettingsData['theme']) => void + setLocale: (locale: string) => void + setFontSize: (size: number) => void + setLineHeight: (height: number) => void + setTabWidth: (width: number) => void + setShowLineNumbers: (show: boolean) => void + setWordWrap: (wrap: boolean) => void + setSpellCheck: (check: boolean) => void + setAutosaveEnabled: (enabled: boolean) => void + setAutosaveInterval: (seconds: number) => void + setShortcuts: (newShortcuts: Partial) => void + setRememberWindowSize: (remember: boolean) => void + setShowMinimap: (show: boolean) => void + loadSettings: () => Promise + saveSettings: () => Promise + resetSettings: () => void +} + +const defaultSettingsData: SettingsData = { + theme: 'system', + locale: 'en', + fontSize: 16, + lineHeight: 1.6, + tabWidth: 4, + showLineNumbers: false, + wordWrap: true, + spellCheck: false, + autosaveEnabled: true, + autosaveInterval: 5, + shortcuts: { + save: 'mod+s', + newFile: 'mod+n', + openFile: 'mod+o', + toggleSidebar: 'mod+b', + toggleCommandPalette: 'mod+shift+p', + }, + rememberWindowSize: true, + showMinimap: false, +} + +export const useSettingsStore = create()( + subscribeWithSelector((set, get) => ({ + ...defaultSettingsData, + + setTheme: (theme) => set({ theme }), + setLocale: (locale) => set({ locale }), + setFontSize: (size) => set({ fontSize: size }), + setLineHeight: (height) => set({ lineHeight: height }), + setTabWidth: (width) => set({ tabWidth: width }), + setShowLineNumbers: (show) => set({ showLineNumbers: show }), + setWordWrap: (wrap) => set({ wordWrap: wrap }), + setSpellCheck: (check) => set({ spellCheck: check }), + setAutosaveEnabled: (enabled) => set({ autosaveEnabled: enabled }), + setAutosaveInterval: (seconds) => set({ autosaveInterval: seconds }), + setShortcuts: (newShortcuts) => + set((state) => ({ + shortcuts: { ...state.shortcuts, ...newShortcuts }, + })), + setRememberWindowSize: (remember) => set({ rememberWindowSize: remember }), + setShowMinimap: (show) => set({ showMinimap: show }), + resetSettings: () => set({ ...defaultSettingsData }), + + loadSettings: async () => { + try { + const stored = await invoke | null>('get_settings') + + if (stored) { + set({ ...defaultSettingsData, ...stored }) + } + + if (stored?.theme) { + const root = document.documentElement + const isDark = stored.theme === 'dark' || (stored.theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches) + root.classList.toggle('dark', isDark) + root.classList.toggle('light', !isDark) + } + } catch (error) { + console.error('Failed to load settings:', error) + } + }, + + saveSettings: async () => { + try { + const state = get() + + const persistable: SettingsData = { + theme: state.theme, + locale: state.locale, + fontSize: state.fontSize, + lineHeight: state.lineHeight, + tabWidth: state.tabWidth, + showLineNumbers: state.showLineNumbers, + wordWrap: state.wordWrap, + spellCheck: state.spellCheck, + autosaveEnabled: state.autosaveEnabled, + autosaveInterval: state.autosaveInterval, + shortcuts: state.shortcuts, + rememberWindowSize: state.rememberWindowSize, + showMinimap: state.showMinimap, + } + + await invoke('save_settings', { settings: persistable }) + } catch (error) { + console.error('Failed to save settings:', error) + } + }, + })) +); + +let saveTimeout: NodeJS.Timeout | null = null +export const persistSettings = () => { + if (saveTimeout) clearTimeout(saveTimeout) + saveTimeout = setTimeout(() => { + useSettingsStore.getState().saveSettings() + }, 500) +} + +useSettingsStore.getState().loadSettings() diff --git a/lib/stores/ui-store.ts b/lib/stores/ui-store.ts new file mode 100644 index 0000000..ad8dcca --- /dev/null +++ b/lib/stores/ui-store.ts @@ -0,0 +1,73 @@ +import { create } from 'zustand' +import { subscribeWithSelector } from 'zustand/middleware' + +/** + * UI Store - manages ephemeral UI state (modals, panels, toasts) + * + * Domain: UI components visibility and interaction state + * Persistence: Not persisted (runtime-only state) + */ +export type ToastVariant = 'success' | 'error' | 'warning' | 'info' + +export interface Toast { + id: string + message: string + variant: ToastVariant + duration?: number +} + +export interface UIState { + // Modal/dialog state + activeModal: 'settings' | 's3-config' | 'about' | null + isCommandPaletteOpen: boolean + isFileSheetOpen: boolean // Mobile file drawer + + // Panel state + sidebarOpen: boolean + inspectorOpen: boolean + + // Toast queue (managed by sonner, but we track queued toasts for coordination) + queuedToasts: Toast[] + + // Actions + setActiveModal: (modal: UIState['activeModal']) => void + setCommandPaletteOpen: (open: boolean) => void + setFileSheetOpen: (open: boolean) => void + setSidebarOpen: (open: boolean) => void + setInspectorOpen: (open: boolean) => void + queueToast: (toast: Toast) => void + removeQueuedToast: (id: string) => void + + // Keyboard shortcuts state + shortcutsEnabled: boolean + setShortcutsEnabled: (enabled: boolean) => void +} + +export const useUIStore = create()( + subscribeWithSelector((set) => ({ + // Initial state + activeModal: null, + isCommandPaletteOpen: false, + isFileSheetOpen: false, + sidebarOpen: true, + inspectorOpen: false, + queuedToasts: [], + shortcutsEnabled: true, + + // Actions + setActiveModal: (modal) => set({ activeModal: modal }), + setCommandPaletteOpen: (open) => set({ isCommandPaletteOpen: open }), + setFileSheetOpen: (open) => set({ isFileSheetOpen: open }), + setSidebarOpen: (open) => set({ sidebarOpen: open }), + setInspectorOpen: (open) => set({ inspectorOpen: open }), + setShortcutsEnabled: (enabled) => set({ shortcutsEnabled: enabled }), + queueToast: (toast) => + set((state) => ({ + queuedToasts: [...state.queuedToasts, toast], + })), + removeQueuedToast: (id) => + set((state) => ({ + queuedToasts: state.queuedToasts.filter((t) => t.id !== id), + })), + })) +) diff --git a/lib/stores/workspace-store.ts b/lib/stores/workspace-store.ts new file mode 100644 index 0000000..61ad434 --- /dev/null +++ b/lib/stores/workspace-store.ts @@ -0,0 +1,80 @@ +import { create } from 'zustand' +import { subscribeWithSelector } from 'zustand/middleware' + +/** + * Workspace Store - manages file system and workspace state + * + * Domain: File system navigation, selection, and workspace metadata + * Persistence: Partial state persisted to Tauri Store + */ +export interface WorkspaceState { + // Workspace paths + folderPath: string | null + selectedFilePath: string | null + + // File list state + files: Array<{ + name: string + isFile: boolean + }> + isLoadingFiles: boolean + fileError: string | null + + // Recent files (for dashboard/history) + recentFiles: Array<{ + path: string + name: string + accessedAt: number + }> + + // Actions + setFolderPath: (path: string | null) => void + setSelectedFile: (path: string | null) => void + setFiles: (files: WorkspaceState['files']) => void + setIsLoadingFiles: (loading: boolean) => void + setFileError: (error: string | null) => void + addRecentFile: (file: WorkspaceState['recentFiles'][0]) => void + clearRecentFiles: () => void +} + +export const useWorkspaceStore = create()( + subscribeWithSelector((set) => ({ + // Initial state + folderPath: null, + selectedFilePath: null, + files: [], + isLoadingFiles: false, + fileError: null, + recentFiles: [], + + // Actions + setFolderPath: (path) => set({ folderPath: path }), + setSelectedFile: (path) => set({ selectedFilePath: path }), + setFiles: (files) => set({ files }), + setIsLoadingFiles: (loading) => set({ isLoadingFiles: loading }), + setFileError: (error) => set({ fileError: error }), + addRecentFile: (file) => set((state) => { + // Remove existing entry if present, add to front, keep last 10 + const filtered = state.recentFiles.filter((f) => f.path !== file.path) + const updated = [{ ...file, accessedAt: Date.now() }, ...filtered].slice(0, 10) + return { recentFiles: updated } + }), + clearRecentFiles: () => set({ recentFiles: [] }), + })) +) + +// Selectors for derived state +export const selectActiveFileName = (state: WorkspaceState) => { + if (!state.selectedFilePath) return null + const parts = state.selectedFilePath.split(/[\\/]/) + return parts[parts.length - 1] || state.selectedFilePath +} + +export const selectFolderName = (state: WorkspaceState) => { + if (!state.folderPath) return null + const parts = state.folderPath.split(/[\\/]/) + return parts[parts.length - 1] || state.folderPath +} + +export const selectFileCount = (state: WorkspaceState) => + state.files.filter((f) => f.isFile).length diff --git a/lib/ux-feedback.ts b/lib/ux-feedback.ts new file mode 100644 index 0000000..b920de3 --- /dev/null +++ b/lib/ux-feedback.ts @@ -0,0 +1,118 @@ +import { toast } from 'sonner' +import { save, open, message, confirm } from '@tauri-apps/plugin-dialog' + +/** + * UX Feedback Utility + * + * Provides consistent desktop-friendly user feedback mechanisms + * - Replaces browser alert/prompt/confirm with native dialogs + * - Uses Sonner toast notifications for non-blocking feedback + */ + +export class UXFeedback { + static success(msg: string) { + toast.success(msg) + } + + static error(msg: string) { + toast.error(msg) + } + + static warning(msg: string) { + toast.warning(msg) + } + + static info(msg: string) { + toast.info(msg) + } + + static async showSaveDialog(options?: { + defaultPath?: string + filters?: Array<{ name: string; extensions: string[] }> + }): Promise { + try { + const result = await save({ + defaultPath: options?.defaultPath, + filters: options?.filters || [ + { + name: 'Markdown', + extensions: ['md'], + }, + ], + }) + + if (typeof result === 'string') { + return result + } + + return null + } catch (error) { + console.error('Failed to show save dialog:', error) + this.error('Failed to open save dialog') + return null + } + } + + static async showOpenDialog(options?: { + directory?: boolean + multiple?: boolean + filters?: Array<{ name: string; extensions: string[] }> + }): Promise { + try { + const result = await open({ + directory: options?.directory || false, + multiple: options?.multiple || false, + filters: options?.filters, + }) + + if (typeof result === 'string' || Array.isArray(result)) { + return result + } + + return null + } catch (error) { + console.error('Failed to show open dialog:', error) + this.error('Failed to open dialog') + return null + } + } + + static async showConfirm(options: { + title: string + message?: string + okLabel?: string + cancelLabel?: string + }): Promise { + try { + const confirmed = await confirm(options.title, { + okLabel: options?.okLabel || 'OK', + cancelLabel: options?.cancelLabel || 'Cancel', + }) + + return confirmed + } catch (error) { + console.error('Failed to show confirm dialog:', error) + return false + } + } + + static async showMessage(options: { + title: string + message: string + }): Promise { + try { + await message(options.title, { + okLabel: 'OK', + }) + } catch (error) { + console.error('Failed to show message dialog:', error) + } + } + + static handleError(error: unknown, context?: string): void { + console.error(context ? `${context}:` : 'Error:', error) + + const message = error instanceof Error ? error.message : 'An error occurred' + this.error(message) + } +} diff --git a/package.json b/package.json index 44bed33..3166947 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "react-day-picker": "8.10.1", "react-dom": "^19.0.0", "react-hook-form": "^7.55.0", + "react-hotkeys-hook": "^5.2.1", "react-markdown": "^9.0.1", "react-resizable-panels": "^2.1.7", "recharts": "^2.15.2", @@ -76,13 +77,15 @@ "tailwind-merge": "^3.1.0", "tw-animate-css": "^1.2.5", "vaul": "^1.1.2", - "zod": "^3.24.2" + "zod": "^3.24.2", + "zustand": "^5.0.9" }, "devDependencies": { "@eslint/eslintrc": "^3", "@playwright/test": "^1.55.0", "@tailwindcss/postcss": "^4", "@tauri-apps/cli": "^2.4.1", + "@tauri-apps/plugin-store": "2.4.1", "@testing-library/jest-dom": "^6.8.0", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ac27917..43d4bdd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -170,6 +170,9 @@ importers: react-hook-form: specifier: ^7.55.0 version: 7.55.0(react@19.1.0) + react-hotkeys-hook: + specifier: ^5.2.1 + version: 5.2.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react-markdown: specifier: ^9.0.1 version: 9.1.0(@types/react@19.1.0)(react@19.1.0) @@ -197,6 +200,9 @@ importers: zod: specifier: ^3.24.2 version: 3.24.2 + zustand: + specifier: ^5.0.9 + version: 5.0.9(@types/react@19.1.0)(react@19.1.0) devDependencies: '@eslint/eslintrc': specifier: ^3 @@ -210,6 +216,9 @@ importers: '@tauri-apps/cli': specifier: ^2.4.1 version: 2.4.1 + '@tauri-apps/plugin-store': + specifier: 2.4.1 + version: 2.4.1 '@testing-library/jest-dom': specifier: ^6.8.0 version: 6.8.0 @@ -816,89 +825,105 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -1101,24 +1126,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@next/swc-linux-arm64-musl@15.4.8': resolution: {integrity: sha512-DX/L8VHzrr1CfwaVjBQr3GWCqNNFgyWJbeQ10Lx/phzbQo3JNAxUok1DZ8JHRGcL6PgMRgj6HylnLNndxn4Z6A==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@next/swc-linux-x64-gnu@15.4.8': resolution: {integrity: sha512-9fLAAXKAL3xEIFdKdzG5rUSvSiZTLLTCc6JKq1z04DR4zY7DbAPcRvNm3K1inVhTiQCs19ZRAgUerHiVKMZZIA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@next/swc-linux-x64-musl@15.4.8': resolution: {integrity: sha512-s45V7nfb5g7dbS7JK6XZDcapicVrMMvX2uYgOHP16QuKH/JA285oy6HcxlKqwUNaFY/UC6EvQ8QZUOo19cBKSA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@next/swc-win32-arm64-msvc@15.4.8': resolution: {integrity: sha512-KjgeQyOAq7t/HzAJcWPGA8X+4WY03uSCZ2Ekk98S9OgCFsb6lfBE3dbUzUuEQAN2THbwYgFfxX2yFTCMm8Kehw==} @@ -2174,56 +2203,67 @@ packages: resolution: {integrity: sha512-OVSQgEZDVLnTbMq5NBs6xkmz3AADByCWI4RdKSFNlDsYXdFtlxS59J+w+LippJe8KcmeSSM3ba+GlsM9+WwC1w==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.49.0': resolution: {integrity: sha512-ZnfSFA7fDUHNa4P3VwAcfaBLakCbYaxCk0jUnS3dTou9P95kwoOLAMlT3WmEJDBCSrOEFFV0Y1HXiwfLYJuLlA==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.49.0': resolution: {integrity: sha512-Z81u+gfrobVK2iV7GqZCBfEB1y6+I61AH466lNK+xy1jfqFLiQ9Qv716WUM5fxFrYxwC7ziVdZRU9qvGHkYIJg==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.49.0': resolution: {integrity: sha512-zoAwS0KCXSnTp9NH/h9aamBAIve0DXeYpll85shf9NJ0URjSTzzS+Z9evmolN+ICfD3v8skKUPyk2PO0uGdFqg==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loongarch64-gnu@4.49.0': resolution: {integrity: sha512-2QyUyQQ1ZtwZGiq0nvODL+vLJBtciItC3/5cYN8ncDQcv5avrt2MbKt1XU/vFAJlLta5KujqyHdYtdag4YEjYQ==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.49.0': resolution: {integrity: sha512-k9aEmOWt+mrMuD3skjVJSSxHckJp+SiFzFG+v8JLXbc/xi9hv2icSkR3U7uQzqy+/QbbYY7iNB9eDTwrELo14g==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.49.0': resolution: {integrity: sha512-rDKRFFIWJ/zJn6uk2IdYLc09Z7zkE5IFIOWqpuU0o6ZpHcdniAyWkwSUWE/Z25N/wNDmFHHMzin84qW7Wzkjsw==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.49.0': resolution: {integrity: sha512-FkkhIY/hYFVnOzz1WeV3S9Bd1h0hda/gRqvZCMpHWDHdiIHn6pqsY3b5eSbvGccWHMQ1uUzgZTKS4oGpykf8Tw==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.49.0': resolution: {integrity: sha512-gRf5c+A7QiOG3UwLyOOtyJMD31JJhMjBvpfhAitPAoqZFcOeK3Kc1Veg1z/trmt+2P6F/biT02fU19GGTS529A==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.49.0': resolution: {integrity: sha512-BR7+blScdLW1h/2hB/2oXM+dhTmpW3rQt1DeSiCP9mc2NMMkqVgjIN3DDsNpKmezffGC9R8XKVOLmBkRUcK/sA==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.49.0': resolution: {integrity: sha512-hDMOAe+6nX3V5ei1I7Au3wcr9h3ktKzDvF2ne5ovX8RZiAHEtX1A5SNNk4zt1Qt77CmnbqT+upb/umzoPMWiPg==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.49.0': resolution: {integrity: sha512-wkNRzfiIGaElC9kXUT+HLx17z7D0jl+9tGYRKwd8r7cUqTL7GYAvgUY++U2hK6Ar7z5Z6IRRoWC8kQxpmM7TDA==} @@ -2301,24 +2341,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.1': resolution: {integrity: sha512-1KPnDMlHdqjPTUSFjx55pafvs8RZXRgxfeRgUrukwDKkuj7gFk28vW3Mx65YdiugAc9NWs3VgueZWaM1Po6uGw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.1': resolution: {integrity: sha512-4WdzA+MRlsinEEE6yxNMLJxpw0kE9XVipbAKdTL8BeUpyC2TdA3TL46lBulXzKp3BIxh3nqyR/UCqzl5o+3waQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.1': resolution: {integrity: sha512-q7Ugbw3ARcjCW2VMUYrcMbJ6aMQuWPArBBE2EqC/swPZTdGADvMQSlvR0VKusUM4HoSsO7ZbvcZ53YwR57+AKw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-win32-arm64-msvc@4.1.1': resolution: {integrity: sha512-0KpqsovgHcIzm7eAGzzEZsEs0/nPYXnRBv+aPq/GehpNQuE/NAQu+YgZXIIof+VflDFuyXOEnaFr7T5MZ1INhA==} @@ -2356,6 +2400,9 @@ packages: '@tauri-apps/api@2.7.0': resolution: {integrity: sha512-v7fVE8jqBl8xJFOcBafDzXFc8FnicoH3j8o8DNNs0tHuEBmXUDqrCOAzMRX0UkfpwqZLqvrvK0GNQ45DfnoVDg==} + '@tauri-apps/api@2.9.1': + resolution: {integrity: sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw==} + '@tauri-apps/cli-darwin-arm64@2.4.1': resolution: {integrity: sha512-QME7s8XQwy3LWClTVlIlwXVSLKkeJ/z88pr917Mtn9spYOjnBfsgHAgGdmpWD3NfJxjg7CtLbhH49DxoFL+hLg==} engines: {node: '>= 10'} @@ -2379,30 +2426,35 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@tauri-apps/cli-linux-arm64-musl@2.4.1': resolution: {integrity: sha512-Hp0zXgeZNKmT+eoJSCxSBUm2QndNuRxR55tmIeNm3vbyUMJN/49uW7nurZ5fBPsacN4Pzwlx1dIMK+Gnr9A69w==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@tauri-apps/cli-linux-riscv64-gnu@2.4.1': resolution: {integrity: sha512-3T3bo2E4fdYRvzcXheWUeQOVB+LunEEi92iPRgOyuSVexVE4cmHYl+MPJF+EUV28Et0hIVTsHibmDO0/04lAFg==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] + libc: [glibc] '@tauri-apps/cli-linux-x64-gnu@2.4.1': resolution: {integrity: sha512-kLN0FdNONO+2i+OpU9+mm6oTGufRC00e197TtwjpC0N6K2K8130w7Q3FeODIM2CMyg0ov3tH+QWqKW7GNhHFzg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@tauri-apps/cli-linux-x64-musl@2.4.1': resolution: {integrity: sha512-a8exvA5Ub9eg66a6hsMQKJIkf63QAf9OdiuFKOsEnKZkNN2x0NLgfvEcqdw88VY0UMs9dBoZ1AGbWMeYnLrLwQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@tauri-apps/cli-win32-arm64-msvc@2.4.1': resolution: {integrity: sha512-4JFrslsMCJQG1c573T9uqQSAbF3j/tMKkMWzsIssv8jvPiP++OG61A2/F+y9te9/Q/O95cKhDK63kaiO5xQaeg==} @@ -2442,6 +2494,9 @@ packages: '@tauri-apps/plugin-shell@2.2.1': resolution: {integrity: sha512-G1GFYyWe/KlCsymuLiNImUgC8zGY0tI0Y3p8JgBCWduR5IEXlIJS+JuG1qtveitwYXlfJrsExt3enhv5l2/yhA==} + '@tauri-apps/plugin-store@2.4.1': + resolution: {integrity: sha512-ckGSEzZ5Ii4Hf2D5x25Oqnm2Zf9MfDWAzR+volY0z/OOBz6aucPKEY0F649JvQ0Vupku6UJo7ugpGRDOFOunkA==} + '@tauri-apps/plugin-updater@2.7.0': resolution: {integrity: sha512-oBug5UCH2wOsoYk0LW5LEMAT51mszjg11s8eungRH26x/qOrEjLvnuJJoxVVr9nsWowJ6vnpXKS+lUMfFTlvHQ==} @@ -2645,31 +2700,37 @@ packages: resolution: {integrity: sha512-v81R2wjqcWXJlQY23byqYHt9221h4anQ6wwN64oMD/WAE+FmxPHFZee5bhRkNVtzqO/q7wki33VFWlhiADwUeQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.3.3': resolution: {integrity: sha512-cAOx/j0u5coMg4oct/BwMzvWJdVciVauUvsd+GQB/1FZYKQZmqPy0EjJzJGbVzFc6gbnfEcSqvQE6gvbGf2N8Q==} cpu: [arm64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.3.3': resolution: {integrity: sha512-mq2blqwErgDJD4gtFDlTX/HZ7lNP8YCHYFij2gkXPtMzrXxPW1hOtxL6xg4NWxvnj4bppppb0W3s/buvM55yfg==} cpu: [ppc64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-s390x-gnu@1.3.3': resolution: {integrity: sha512-u0VRzfFYysarYHnztj2k2xr+eu9rmgoTUUgCCIT37Nr+j0A05Xk2c3RY8Mh5+DhCl2aYibihnaAEJHeR0UOFIQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.3.3': resolution: {integrity: sha512-OrVo5ZsG29kBF0Ug95a2KidS16PqAMmQNozM6InbquOfW/udouk063e25JVLqIBhHLB2WyBnixOQ19tmeC/hIg==} cpu: [x64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.3.3': resolution: {integrity: sha512-PYnmrwZ4HMp9SkrOhqPghY/aoL+Rtd4CQbr93GlrRTjK6kDzfMfgz3UH3jt6elrQAfupa1qyr1uXzeVmoEAxUA==} cpu: [x64] os: [linux] + libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.3.3': resolution: {integrity: sha512-81AnQY6fShmktQw4hWDUIilsKSdvr/acdJ5azAreu2IWNlaJOKphJSsUVWE+yCk6kBMoQyG9ZHCb/krb5K0PEA==} @@ -3830,24 +3891,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.29.2: resolution: {integrity: sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.29.2: resolution: {integrity: sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.29.2: resolution: {integrity: sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.29.2: resolution: {integrity: sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==} @@ -4331,6 +4396,12 @@ packages: peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 + react-hotkeys-hook@5.2.1: + resolution: {integrity: sha512-xbKh6zJxd/vJHT4Bw4+0pBD662Fk20V+VFhLqciCg+manTVO4qlqRqiwFOYelfHN9dBvWj9vxaPkSS26ZSIJGg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -4998,6 +5069,24 @@ packages: zod@3.24.2: resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==} + zustand@5.0.9: + resolution: {integrity: sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -7325,6 +7414,8 @@ snapshots: '@tauri-apps/api@2.7.0': {} + '@tauri-apps/api@2.9.1': {} + '@tauri-apps/cli-darwin-arm64@2.4.1': optional: true @@ -7392,6 +7483,10 @@ snapshots: dependencies: '@tauri-apps/api': 2.7.0 + '@tauri-apps/plugin-store@2.4.1': + dependencies: + '@tauri-apps/api': 2.9.1 + '@tauri-apps/plugin-updater@2.7.0': dependencies: '@tauri-apps/api': 2.7.0 @@ -9781,6 +9876,11 @@ snapshots: dependencies: react: 19.1.0 + react-hotkeys-hook@5.2.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-is@16.13.1: {} react-is@17.0.2: {} @@ -10630,4 +10730,9 @@ snapshots: zod@3.24.2: {} + zustand@5.0.9(@types/react@19.1.0)(react@19.1.0): + optionalDependencies: + '@types/react': 19.1.0 + react: 19.1.0 + zwitch@2.0.4: {} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 06ca4d5..36e5a70 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -83,6 +83,7 @@ dependencies = [ "tauri-plugin-os", "tauri-plugin-process", "tauri-plugin-shell", + "tauri-plugin-store", "tauri-plugin-updater", "tokio", ] @@ -578,7 +579,7 @@ dependencies = [ "http-body 0.4.6", "http-body 1.0.1", "http-body-util", - "itoa 1.0.15", + "itoa", "num-integer", "pin-project-lite", "pin-utils", @@ -705,9 +706,9 @@ dependencies = [ [[package]] name = "brotli" -version = "7.0.0" +version = "8.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -716,9 +717,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "4.0.2" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74fa05ad7d803d413eb8380983b092cbbaf9a85f151b871360e7b00cd7060b37" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -825,7 +826,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02260d489095346e5cafd04dea8e8cb54d1d74fcd759022a9b72986ebe9a1257" dependencies = [ "serde", - "toml", + "toml 0.8.20", ] [[package]] @@ -889,7 +890,7 @@ dependencies = [ "iana-time-zone", "num-traits", "serde", - "windows-link 0.1.1", + "windows-link 0.1.3", ] [[package]] @@ -1087,15 +1088,15 @@ dependencies = [ [[package]] name = "cssparser" -version = "0.27.2" +version = "0.29.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a" +checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" dependencies = [ "cssparser-macros", "dtoa-short", - "itoa 0.4.8", + "itoa", "matches", - "phf 0.8.0", + "phf 0.10.1", "proc-macro2", "quote", "smallvec", @@ -1230,7 +1231,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1264,9 +1265,9 @@ dependencies = [ [[package]] name = "dlopen2" -version = "0.7.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1297103d2bbaea85724fcee6294c2d50b1081f9ad47d0f6f6f61eda65315a6" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" dependencies = [ "dlopen2_derive", "libc", @@ -1368,7 +1369,7 @@ dependencies = [ "cc", "memchr", "rustc_version", - "toml", + "toml 0.8.20", "vswhom", "winreg", ] @@ -2017,7 +2018,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.8.0", + "indexmap 2.13.0", "slab", "tokio", "tokio-util", @@ -2036,7 +2037,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.3.1", - "indexmap 2.8.0", + "indexmap 2.13.0", "slab", "tokio", "tokio-util", @@ -2060,6 +2061,12 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "heck" version = "0.4.1" @@ -2089,16 +2096,14 @@ dependencies = [ [[package]] name = "html5ever" -version = "0.26.0" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" +checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" dependencies = [ "log", "mac", "markup5ever", - "proc-macro2", - "quote", - "syn 1.0.109", + "match_token", ] [[package]] @@ -2109,7 +2114,7 @@ checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ "bytes", "fnv", - "itoa 1.0.15", + "itoa", ] [[package]] @@ -2120,7 +2125,7 @@ checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", - "itoa 1.0.15", + "itoa", ] [[package]] @@ -2184,7 +2189,7 @@ dependencies = [ "http-body 0.4.6", "httparse", "httpdate", - "itoa 1.0.15", + "itoa", "pin-project-lite", "socket2", "tokio", @@ -2206,7 +2211,7 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "httparse", - "itoa 1.0.15", + "itoa", "pin-project-lite", "smallvec", "tokio", @@ -2280,7 +2285,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.61.0", + "windows-core 0.60.1", ] [[package]] @@ -2460,13 +2465,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.8.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.16.1", "serde", + "serde_core", ] [[package]] @@ -2503,12 +2509,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "itoa" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" - [[package]] name = "itoa" version = "1.0.15" @@ -2615,14 +2615,13 @@ dependencies = [ [[package]] name = "kuchikiki" -version = "0.8.2" +version = "0.8.8-speedreader" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8" +checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" dependencies = [ "cssparser", "html5ever", - "indexmap 1.9.3", - "matches", + "indexmap 2.13.0", "selectors", ] @@ -2728,18 +2727,29 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] name = "markup5ever" -version = "0.11.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" +checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" dependencies = [ "log", - "phf 0.10.1", - "phf_codegen 0.10.0", + "phf 0.11.3", + "phf_codegen 0.11.3", "string_cache", "string_cache_codegen", "tendril", ] +[[package]] +name = "match_token" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "matches" version = "0.1.10" @@ -2806,9 +2816,9 @@ dependencies = [ [[package]] name = "muda" -version = "0.16.1" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4de14a9b5d569ca68d7c891d613b390cf5ab4f851c77aaa2f9e435555d3d9492" +checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" dependencies = [ "crossbeam-channel", "dpi", @@ -2822,7 +2832,7 @@ dependencies = [ "png", "serde", "thiserror 2.0.12", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -3123,6 +3133,17 @@ dependencies = [ "objc2-foundation 0.3.0", ] +[[package]] +name = "objc2-security" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3126341c65c5d5728423ae95d788e1b660756486ad0592307ab87ba02d9a7268" +dependencies = [ + "bitflags 2.9.0", + "objc2 0.6.0", + "objc2-core-foundation", +] + [[package]] name = "objc2-ui-kit" version = "0.3.0" @@ -3147,6 +3168,7 @@ dependencies = [ "objc2-app-kit", "objc2-core-foundation", "objc2-foundation 0.3.0", + "objc2-security", ] [[package]] @@ -3328,9 +3350,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_macros 0.8.0", "phf_shared 0.8.0", - "proc-macro-hack", ] [[package]] @@ -3339,7 +3359,9 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" dependencies = [ + "phf_macros 0.10.0", "phf_shared 0.10.0", + "proc-macro-hack", ] [[package]] @@ -3364,12 +3386,12 @@ dependencies = [ [[package]] name = "phf_codegen" -version = "0.10.0" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", + "phf_generator 0.11.3", + "phf_shared 0.11.3", ] [[package]] @@ -3404,12 +3426,12 @@ dependencies = [ [[package]] name = "phf_macros" -version = "0.8.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" dependencies = [ - "phf_generator 0.8.0", - "phf_shared 0.8.0", + "phf_generator 0.10.0", + "phf_shared 0.10.0", "proc-macro-hack", "proc-macro2", "quote", @@ -3491,7 +3513,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eac26e981c03a6e53e0aee43c113e3202f5581d5360dae7bd2c70e800dd0451d" dependencies = [ "base64 0.22.1", - "indexmap 2.8.0", + "indexmap 2.13.0", "quick-xml", "serde", "time", @@ -4202,22 +4224,20 @@ dependencies = [ [[package]] name = "selectors" -version = "0.22.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" +checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" dependencies = [ "bitflags 1.3.2", "cssparser", "derive_more", "fxhash", "log", - "matches", "phf 0.8.0", "phf_codegen 0.8.0", "precomputed-hash", "servo_arc", "smallvec", - "thin-slice", ] [[package]] @@ -4231,10 +4251,11 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ + "serde_core", "serde_derive", ] @@ -4249,11 +4270,20 @@ dependencies = [ "typeid", ] +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -4277,7 +4307,7 @@ version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ - "itoa 1.0.15", + "itoa", "memchr", "ryu", "serde", @@ -4303,6 +4333,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -4310,7 +4349,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.15", + "itoa", "ryu", "serde", ] @@ -4325,7 +4364,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.8.0", + "indexmap 2.13.0", "serde", "serde_derive", "serde_json", @@ -4369,9 +4408,9 @@ dependencies = [ [[package]] name = "servo_arc" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432" +checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" dependencies = [ "nodrop", "stable_deref_trait", @@ -4655,17 +4694,18 @@ dependencies = [ "cfg-expr", "heck 0.5.0", "pkg-config", - "toml", + "toml 0.8.20", "version-compare", ] [[package]] name = "tao" -version = "0.32.8" +version = "0.34.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63c8b1020610b9138dd7b1e06cf259ae91aa05c30f3bd0d6b42a03997b92dec1" +checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" dependencies = [ "bitflags 2.9.0", + "block2 0.6.0", "core-foundation 0.10.0", "core-graphics", "crossbeam-channel", @@ -4693,7 +4733,7 @@ dependencies = [ "unicode-segmentation", "url", "windows", - "windows-core 0.60.1", + "windows-core 0.61.2", "windows-version", "x11-dl", ] @@ -4728,17 +4768,16 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.4.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d08db1ff9e011e04014e737ec022610d756c0eae0b3b3a9037bccaf3003173a" +checksum = "352a4bc7bf6c25f5624227e3641adf475a6535707451b09bb83271df8b7a6ac7" dependencies = [ "anyhow", "bytes", "dirs", "dunce", "embed_plist", - "futures-util", - "getrandom 0.2.15", + "getrandom 0.3.2", "glob", "gtk", "heck 0.5.0", @@ -4751,6 +4790,7 @@ dependencies = [ "objc2 0.6.0", "objc2-app-kit", "objc2-foundation 0.3.0", + "objc2-ui-kit", "percent-encoding", "plist", "raw-window-handle", @@ -4778,9 +4818,9 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.1.1" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fd20e4661c2cce65343319e6e8da256958f5af958cafc47c0d0af66a55dcd17" +checksum = "17fcb8819fd16463512a12f531d44826ce566f486d7ccd211c9c8cebdaec4e08" dependencies = [ "anyhow", "cargo_toml", @@ -4794,15 +4834,15 @@ dependencies = [ "serde_json", "tauri-utils", "tauri-winres", - "toml", + "toml 0.9.11+spec-1.1.0", "walkdir", ] [[package]] name = "tauri-codegen" -version = "2.1.1" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458258b19032450ccf975840116ecf013e539eadbb74420bd890e8c56ab2b1a4" +checksum = "9fa9844cefcf99554a16e0a278156ae73b0d8680bbc0e2ad1e4287aadd8489cf" dependencies = [ "base64 0.22.1", "brotli", @@ -4827,9 +4867,9 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.1.1" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d402813d3b9c773a0fa58697c457c771f10e735498fdcb7b343264d18e5a601f" +checksum = "3764a12f886d8245e66b7ee9b43ccc47883399be2019a61d80cf0f4117446fde" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -4841,9 +4881,9 @@ dependencies = [ [[package]] name = "tauri-plugin" -version = "2.1.1" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4190775d6ff73fe66d9af44c012739a2659720efd9c0e1e56a918678038699d" +checksum = "0e1d0a4860b7ff570c891e1d2a586bf1ede205ff858fbc305e0b5ae5d14c1377" dependencies = [ "anyhow", "glob", @@ -4852,7 +4892,7 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "toml", + "toml 0.9.11+spec-1.1.0", "walkdir", ] @@ -4892,7 +4932,7 @@ dependencies = [ "tauri-plugin", "tauri-utils", "thiserror 2.0.12", - "toml", + "toml 0.8.20", "url", "uuid", ] @@ -4946,6 +4986,22 @@ dependencies = [ "tokio", ] +[[package]] +name = "tauri-plugin-store" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5916c609664a56c82aeaefffca9851fd072d4d41f73d63f22ee3ee451508194f" +dependencies = [ + "dunce", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.12", + "tokio", + "tracing", +] + [[package]] name = "tauri-plugin-updater" version = "2.7.0" @@ -4980,29 +5036,34 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.5.1" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00ada7ac2f9276f09b8c3afffd3215fd5d9bff23c22df8a7c70e7ef67cacd532" +checksum = "87f766fe9f3d1efc4b59b17e7a891ad5ed195fa8d23582abb02e6c9a01137892" dependencies = [ "cookie", "dpi", "gtk", "http 1.3.1", "jni", + "objc2 0.6.0", + "objc2-ui-kit", + "objc2-web-kit", "raw-window-handle", "serde", "serde_json", "tauri-utils", "thiserror 2.0.12", "url", + "webkit2gtk", + "webview2-com", "windows", ] [[package]] name = "tauri-runtime-wry" -version = "2.5.1" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf2e5842c57e154af43a20a49c7efee0ce2578c20b4c2bdf266852b422d2e421" +checksum = "187a3f26f681bdf028f796ccf57cf478c1ee422c50128e5a0a6ebeb3f5910065" dependencies = [ "gtk", "http 1.3.1", @@ -5027,9 +5088,9 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.3.1" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f037e66c7638cc0a2213f61566932b9a06882b8346486579c90e4b019bac447" +checksum = "76a423c51176eb3616ee9b516a9fa67fed5f0e78baaba680e44eb5dd2cc37490" dependencies = [ "anyhow", "brotli", @@ -5056,7 +5117,7 @@ dependencies = [ "serde_with", "swift-rs", "thiserror 2.0.12", - "toml", + "toml 0.9.11+spec-1.1.0", "url", "urlpattern", "uuid", @@ -5070,7 +5131,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56eaa45f707bedf34d19312c26d350bc0f3c59a47e58e8adbeecdc850d2c13a0" dependencies = [ "embed-resource", - "toml", + "toml 0.8.20", ] [[package]] @@ -5097,12 +5158,6 @@ dependencies = [ "utf-8", ] -[[package]] -name = "thin-slice" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" - [[package]] name = "thiserror" version = "1.0.69" @@ -5150,7 +5205,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ "deranged", - "itoa 1.0.15", + "itoa", "num-conv", "powerfmt", "serde", @@ -5212,10 +5267,22 @@ dependencies = [ "pin-project-lite", "signal-hook-registry", "socket2", + "tokio-macros", "tracing", "windows-sys 0.52.0", ] +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "tokio-rustls" version = "0.24.1" @@ -5256,11 +5323,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" dependencies = [ "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.8", + "toml_datetime 0.6.8", "toml_edit 0.22.24", ] +[[package]] +name = "toml" +version = "0.9.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" +dependencies = [ + "indexmap 2.13.0", + "serde_core", + "serde_spanned 1.0.4", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.14", +] + [[package]] name = "toml_datetime" version = "0.6.8" @@ -5270,14 +5352,23 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.8.0", - "toml_datetime", + "indexmap 2.13.0", + "toml_datetime 0.6.8", "winnow 0.5.40", ] @@ -5287,8 +5378,8 @@ version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" dependencies = [ - "indexmap 2.8.0", - "toml_datetime", + "indexmap 2.13.0", + "toml_datetime 0.6.8", "winnow 0.5.40", ] @@ -5298,13 +5389,28 @@ version = "0.22.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ - "indexmap 2.8.0", + "indexmap 2.13.0", "serde", - "serde_spanned", - "toml_datetime", - "winnow 0.7.4", + "serde_spanned 0.6.8", + "toml_datetime 0.6.8", + "winnow 0.7.14", ] +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +dependencies = [ + "winnow 0.7.14", +] + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + [[package]] name = "tower" version = "0.5.2" @@ -5365,9 +5471,9 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.20.0" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d433764348e7084bad2c5ea22c96c71b61b17afe3a11645710f533bd72b6a2b5" +checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" dependencies = [ "crossbeam-channel", "dirs", @@ -5382,7 +5488,7 @@ dependencies = [ "png", "serde", "thiserror 2.0.12", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -5768,23 +5874,23 @@ dependencies = [ [[package]] name = "webview2-com" -version = "0.36.0" +version = "0.38.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0d606f600e5272b514dbb66539dd068211cc20155be8d3958201b4b5bd79ed3" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" dependencies = [ "webview2-com-macros", "webview2-com-sys", "windows", - "windows-core 0.60.1", - "windows-implement 0.59.0", + "windows-core 0.61.2", + "windows-implement 0.60.0", "windows-interface", ] [[package]] name = "webview2-com-macros" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" dependencies = [ "proc-macro2", "quote", @@ -5793,13 +5899,13 @@ dependencies = [ [[package]] name = "webview2-com-sys" -version = "0.36.0" +version = "0.38.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfb27fccd3c27f68e9a6af1bcf48c2d82534b8675b83608a4d81446d095a17ac" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" dependencies = [ "thiserror 2.0.12", "windows", - "windows-core 0.60.1", + "windows-core 0.61.2", ] [[package]] @@ -5850,24 +5956,24 @@ dependencies = [ [[package]] name = "windows" -version = "0.60.0" +version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddf874e74c7a99773e62b1c671427abf01a425e77c3d3fb9fb1e4883ea934529" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ "windows-collections", - "windows-core 0.60.1", + "windows-core 0.61.2", "windows-future", - "windows-link 0.1.1", + "windows-link 0.1.3", "windows-numerics", ] [[package]] name = "windows-collections" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5467f79cc1ba3f52ebb2ed41dbb459b8e7db636cc3429458d9a852e15bc24dec" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ - "windows-core 0.60.1", + "windows-core 0.61.2", ] [[package]] @@ -5878,32 +5984,33 @@ checksum = "ca21a92a9cae9bf4ccae5cf8368dce0837100ddf6e6d57936749e85f152f6247" dependencies = [ "windows-implement 0.59.0", "windows-interface", - "windows-link 0.1.1", + "windows-link 0.1.3", "windows-result", "windows-strings 0.3.1", ] [[package]] name = "windows-core" -version = "0.61.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement 0.60.0", "windows-interface", - "windows-link 0.1.1", + "windows-link 0.1.3", "windows-result", - "windows-strings 0.4.0", + "windows-strings 0.4.2", ] [[package]] name = "windows-future" -version = "0.1.1" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a787db4595e7eb80239b74ce8babfb1363d8e343ab072f2ffe901400c03349f0" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ - "windows-core 0.60.1", - "windows-link 0.1.1", + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", ] [[package]] @@ -5941,9 +6048,9 @@ dependencies = [ [[package]] name = "windows-link" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-link" @@ -5953,12 +6060,12 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-numerics" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "005dea54e2f6499f2cee279b8f703b3cf3b5734a2d8d21867c8f44003182eeed" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ - "windows-core 0.60.1", - "windows-link 0.1.1", + "windows-core 0.61.2", + "windows-link 0.1.3", ] [[package]] @@ -5969,16 +6076,16 @@ checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" dependencies = [ "windows-result", "windows-strings 0.3.1", - "windows-targets 0.53.0", + "windows-targets 0.53.5", ] [[package]] name = "windows-result" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link 0.1.1", + "windows-link 0.1.3", ] [[package]] @@ -5987,16 +6094,16 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" dependencies = [ - "windows-link 0.1.1", + "windows-link 0.1.3", ] [[package]] name = "windows-strings" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link 0.1.1", + "windows-link 0.1.3", ] [[package]] @@ -6035,6 +6142,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -6092,10 +6208,11 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.0" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ + "windows-link 0.2.1", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -6106,13 +6223,22 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + [[package]] name = "windows-version" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e04a5c6627e310a23ad2358483286c7df260c964eb2d003d8efd6d0f4e79265c" dependencies = [ - "windows-link 0.1.1", + "windows-link 0.1.3", ] [[package]] @@ -6306,9 +6432,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.4" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] @@ -6346,14 +6472,15 @@ checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] name = "wry" -version = "0.50.5" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b19b78efae8b853c6c817e8752fc1dbf9cab8a8ffe9c30f399bd750ccf0f0730" +checksum = "728b7d4c8ec8d81cab295e0b5b8a4c263c0d41a785fb8f8c4df284e5411140a2" dependencies = [ "base64 0.22.1", "block2 0.6.0", "cookie", "crossbeam-channel", + "dirs", "dpi", "dunce", "gdkx11", @@ -6383,7 +6510,7 @@ dependencies = [ "webkit2gtk-sys", "webview2-com", "windows", - "windows-core 0.60.1", + "windows-core 0.61.2", "windows-version", "x11-dl", ] @@ -6482,7 +6609,7 @@ dependencies = [ "tracing", "uds_windows", "windows-sys 0.59.0", - "winnow 0.7.4", + "winnow 0.7.14", "xdg-home", "zbus_macros", "zbus_names", @@ -6512,7 +6639,7 @@ checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" dependencies = [ "serde", "static_assertions", - "winnow 0.7.4", + "winnow 0.7.14", "zvariant", ] @@ -6594,7 +6721,7 @@ dependencies = [ "arbitrary", "crc32fast", "crossbeam-utils", - "indexmap 2.8.0", + "indexmap 2.13.0", "memchr", ] @@ -6609,7 +6736,7 @@ dependencies = [ "serde", "static_assertions", "url", - "winnow 0.7.4", + "winnow 0.7.14", "zvariant_derive", "zvariant_utils", ] @@ -6638,5 +6765,5 @@ dependencies = [ "serde", "static_assertions", "syn 2.0.100", - "winnow 0.7.4", + "winnow 0.7.14", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 427dcc7..f24ddf1 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -23,6 +23,7 @@ tauri-plugin-process = "2" tauri-plugin-dialog = "2" tauri-plugin-fs = "2" tauri-plugin-os = "2.0.0" +tauri-plugin-store = "2" aws-config = { version = "1.5", features = ["behavior-version-latest"] } aws-sdk-s3 = "1.63" tokio = "1.41" diff --git a/src-tauri/src/commands/persistence.rs b/src-tauri/src/commands/persistence.rs new file mode 100644 index 0000000..1476db5 --- /dev/null +++ b/src-tauri/src/commands/persistence.rs @@ -0,0 +1,61 @@ +use std::collections::HashMap; + +#[tauri::command] +pub fn get_settings() -> Result, String> { + let settings_store = crate::settings::get_settings_store(); + settings_store.load() + .map_err(|e| format!("Failed to load settings: {}", e)) +} + +#[tauri::command] +pub fn save_settings(settings: HashMap) -> Result<(), String> { + let mut settings_store = crate::settings::get_settings_store(); + settings_store.set(settings_store) + .map_err(|e| format!("Failed to save settings: {}", e))?; + settings_store.save() + .map_err(|e| format!("Failed to save settings: {}", e)) +} + +#[tauri::command] +pub fn get_workspace_state() -> Result { + let workspace_store = crate::workspace::get_workspace_store(); + workspace_store.load() + .map_err(|e| format!("Failed to load workspace state: {}", e)) +} + +#[tauri::command] +pub fn save_workspace_state(state: WorkspaceData) -> Result<(), String> { + let mut workspace_store = crate::workspace::get_workspace_store(); + workspace_store.set_state(state) + .map_err(|e| format!("Failed to set workspace state: {}", e))?; + workspace_store.save() + .map_err(|e| format!("Failed to save workspace state: {}", e)) +} + +#[tauri::command] +pub fn add_recent_file(file: RecentFile) -> Result<(), String> { + let mut workspace_store = crate::workspace::get_workspace_store(); + workspace_store.add_recent_file(file) + .map_err(|e| format!("Failed to add recent file: {}", e))?; + workspace_store.save() + .map_err(|e| format!("Failed to save workspace state: {}", e)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_settings_commands() { + let mut settings = HashMap::new(); + settings.insert("theme".to_string(), serde_json::Value::String("system".to_string())); + settings.insert("fontSize".to_string(), serde_json::Value::Number(16)); + + let result = crate::commands::save_settings(settings.clone()); + assert!(result.is_ok(), "save_settings should succeed"); + + let loaded = crate::commands::get_settings().unwrap(); + assert_eq!(loaded.get("theme"), Some(&serde_json::Value::String("system".to_string()))); + assert_eq!(loaded.get("fontSize"), Some(&serde_json::Value::Number(16))); + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs new file mode 100644 index 0000000..e2a3fbc --- /dev/null +++ b/src-tauri/src/lib.rs @@ -0,0 +1,7 @@ +mod persistence; +mod types; +mod workspace_commands; + +pub use persistence::*; +pub use types::*; +pub use workspace_commands::*; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 3aad58a..7099d97 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -2,7 +2,10 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] mod events; +mod persistence; mod s3; +mod types; +mod workspace_commands; #[tauri::command] fn scoop_update() -> Result { @@ -29,6 +32,7 @@ fn main() { .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_updater::Builder::new().build()) + .plugin(tauri_plugin_store::Builder::new().build()) .plugin(tauri_plugin_shell::init()) .invoke_handler(tauri::generate_handler![ scoop_update, @@ -39,6 +43,14 @@ fn main() { s3::load_s3_config, s3::delete_s3_config, s3::upload_image_to_s3, + workspace_commands::get_current_folder_path, + workspace_commands::set_current_folder_path, + workspace_commands::get_current_file_path, + workspace_commands::set_current_file_path, + workspace_commands::get_recent_files, + persistence::get_settings, + persistence::save_settings, + workspace_commands::add_recent_file, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/persistence.rs b/src-tauri/src/persistence.rs new file mode 100644 index 0000000..812d105 --- /dev/null +++ b/src-tauri/src/persistence.rs @@ -0,0 +1,34 @@ +use std::collections::HashMap; + +use serde_json::Value; +use tauri::AppHandle; +use tauri_plugin_store::StoreExt; + +#[tauri::command] +pub fn get_settings(app: AppHandle) -> Result, String> { + let store = app + .store("settings.json") + .map_err(|e| format!("Failed to open settings store: {e}"))?; + + let mut settings = HashMap::new(); + for (key, value) in store.entries() { + settings.insert(key.to_string(), value.clone()); + } + + Ok(settings) +} + +#[tauri::command] +pub fn save_settings(app: AppHandle, settings: HashMap) -> Result<(), String> { + let store = app + .store("settings.json") + .map_err(|e| format!("Failed to open settings store: {e}"))?; + + for (key, value) in settings { + store.set(key, value); + } + + store + .save() + .map_err(|e| format!("Failed to save settings store: {e}")) +} diff --git a/src-tauri/src/stores.rs b/src-tauri/src/stores.rs new file mode 100644 index 0000000..a37a701 --- /dev/null +++ b/src-tauri/src/stores.rs @@ -0,0 +1,5 @@ +mod stores; +mod types; + +pub use stores::*; +pub use types::*; diff --git a/src-tauri/src/types.rs b/src-tauri/src/types.rs new file mode 100644 index 0000000..18d5998 --- /dev/null +++ b/src-tauri/src/types.rs @@ -0,0 +1,77 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WorkspaceData { + pub folder_path: Option, + pub selected_file_path: Option, + pub recent_files: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RecentFile { + pub path: String, + pub name: String, + pub accessed_at: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SettingsData { + pub theme: String, + pub locale: String, + pub font_size: u32, + pub line_height: f32, + pub tab_width: u32, + pub show_line_numbers: bool, + pub word_wrap: bool, + pub spell_check: bool, + pub autosave_enabled: bool, + pub autosave_interval: u32, + pub shortcuts: Shortcuts, + pub remember_window_size: bool, + pub show_minimap: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Shortcuts { + pub save: String, + pub new_file: String, + pub open_file: String, + pub toggle_sidebar: String, + pub toggle_command_palette: String, +} + +impl Default for SettingsData { + fn default() -> Self { + Self { + theme: "system".to_string(), + locale: "en".to_string(), + font_size: 16, + line_height: 1.6, + tab_width: 4, + show_line_numbers: false, + word_wrap: true, + spell_check: false, + autosave_enabled: true, + autosave_interval: 5, + shortcuts: Shortcuts { + save: "mod+s".to_string(), + new_file: "mod+n".to_string(), + open_file: "mod+o".to_string(), + toggle_sidebar: "mod+b".to_string(), + toggle_command_palette: "mod+shift+p".to_string(), + }, + remember_window_size: true, + show_minimap: false, + } + } +} + +impl Default for WorkspaceData { + fn default() -> Self { + Self { + folder_path: None, + selected_file_path: None, + recent_files: Vec::new(), + } + } +} diff --git a/src-tauri/src/workspace.rs b/src-tauri/src/workspace.rs new file mode 100644 index 0000000..a3471c9 --- /dev/null +++ b/src-tauri/src/workspace.rs @@ -0,0 +1,3 @@ +mod workspace_commands; + +pub use workspace_commands::*; diff --git a/src-tauri/src/workspace_commands.rs b/src-tauri/src/workspace_commands.rs new file mode 100644 index 0000000..908d483 --- /dev/null +++ b/src-tauri/src/workspace_commands.rs @@ -0,0 +1,89 @@ +use serde_json::Value; +use tauri::AppHandle; +use tauri_plugin_store::StoreExt; + +use crate::types::RecentFile; + +#[tauri::command] +pub fn get_current_folder_path(app: AppHandle) -> Result, String> { + let store = app + .store("workspace.json") + .map_err(|e| format!("Failed to open workspace store: {e}"))?; + + Ok(store + .get("folderPath") + .and_then(|value| value.as_str().map(|path| path.to_string()))) +} + +#[tauri::command] +pub fn set_current_folder_path(app: AppHandle, path: String) -> Result<(), String> { + let store = app + .store("workspace.json") + .map_err(|e| format!("Failed to open workspace store: {e}"))?; + + store.set("folderPath", Value::String(path)); + store + .save() + .map_err(|e| format!("Failed to save workspace store: {e}")) +} + +#[tauri::command] +pub fn get_current_file_path(app: AppHandle) -> Result, String> { + let store = app + .store("workspace.json") + .map_err(|e| format!("Failed to open workspace store: {e}"))?; + + Ok(store + .get("selectedFilePath") + .and_then(|value| value.as_str().map(|path| path.to_string()))) +} + +#[tauri::command] +pub fn set_current_file_path(app: AppHandle, path: String) -> Result<(), String> { + let store = app + .store("workspace.json") + .map_err(|e| format!("Failed to open workspace store: {e}"))?; + + store.set("selectedFilePath", Value::String(path)); + store + .save() + .map_err(|e| format!("Failed to save workspace store: {e}")) +} + +#[tauri::command] +pub fn get_recent_files(app: AppHandle) -> Result, String> { + let store = app + .store("workspace.json") + .map_err(|e| format!("Failed to open workspace store: {e}"))?; + + let recent_files = store + .get("recentFiles") + .and_then(|value| serde_json::from_value::>(value.clone()).ok()) + .unwrap_or_default(); + + Ok(recent_files) +} + +#[tauri::command] +pub fn add_recent_file(app: AppHandle, file: RecentFile) -> Result<(), String> { + let store = app + .store("workspace.json") + .map_err(|e| format!("Failed to open workspace store: {e}"))?; + + let mut recent_files: Vec = store + .get("recentFiles") + .and_then(|value| serde_json::from_value::>(value.clone()).ok()) + .unwrap_or_default(); + + recent_files.retain(|entry| entry.path != file.path); + recent_files.insert(0, file); + recent_files.truncate(10); + + let serialized = serde_json::to_value(&recent_files) + .map_err(|e| format!("Failed to serialize recent files: {e}"))?; + + store.set("recentFiles", serialized); + store + .save() + .map_err(|e| format!("Failed to save workspace store: {e}")) +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 2152217..dc24567 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -55,6 +55,9 @@ "https://github.com/markbang/OnlyWrite/releases/latest/download/latest.json" ], "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDM3RTM4NUUwMzQyNTczMjcKUldRbmN5VTA0SVhqTjlGdUUwRUV5WWdHeWNLTDUwSjNnOVN0VGFTSzNpY2ZSMm5zYUZFM01lNlUK" + }, + "store": { + "active": true } }, "app": {