diff options
Diffstat (limited to 'src/stores')
| -rw-r--r-- | src/stores/useCommandStore.ts | 137 | ||||
| -rw-r--r-- | src/stores/useContextMenuStore.ts | 31 | ||||
| -rw-r--r-- | src/stores/useFilesStore.ts | 84 | ||||
| -rw-r--r-- | src/stores/useLayoutStore.ts | 51 | ||||
| -rw-r--r-- | src/stores/useSettingsStore.ts | 34 | ||||
| -rw-r--r-- | src/stores/useTabsStore.ts | 152 | ||||
| -rw-r--r-- | src/stores/useThemeStore.ts | 41 | ||||
| -rw-r--r-- | src/stores/useToastStore.ts | 31 |
8 files changed, 561 insertions, 0 deletions
diff --git a/src/stores/useCommandStore.ts b/src/stores/useCommandStore.ts new file mode 100644 index 0000000..2975436 --- /dev/null +++ b/src/stores/useCommandStore.ts @@ -0,0 +1,137 @@ +import { create } from 'zustand' +import { useTabsStore } from './useTabsStore' +import { useFilesStore } from './useFilesStore' +import { useLayoutStore } from './useLayoutStore' + +export interface Command { + id: string + label: string + shortcut?: string + category: string + action: () => void +} + +export interface CommandState { + isOpen: boolean + query: string + selectedIndex: number + commands: Command[] + open: () => void + close: () => void + toggle: () => void + setQuery: (query: string) => void + setSelectedIndex: (index: number) => void + executeSelected: () => void +} + +function buildCommands(): Command[] { + const filesStore = useFilesStore.getState() + const layoutStore = useLayoutStore.getState() + + return [ + { + id: 'open-folder', + label: 'Open Folder', + shortcut: 'Ctrl+Shift+K', + category: 'File', + action: () => filesStore.openFolder(), + }, + { + id: 'toggle-sidebar', + label: 'Toggle Sidebar', + shortcut: 'Ctrl+B', + category: 'View', + action: () => layoutStore.toggleSidePanel(), + }, + { + id: 'save-file', + label: 'Save File', + shortcut: 'Ctrl+S', + category: 'File', + action: () => { + const state = useTabsStore.getState() + if (state.activeTabId) state.saveFile(state.activeTabId) + }, + }, + { + id: 'close-tab', + label: 'Close Current Tab', + shortcut: 'Ctrl+W', + category: 'File', + action: () => { + const state = useTabsStore.getState() + if (state.activeTabId) state.closeTab(state.activeTabId) + }, + }, + { + id: 'close-all-tabs', + label: 'Close All Tabs', + shortcut: '', + category: 'File', + action: () => { + const state = useTabsStore.getState() + state.tabs.forEach((t) => state.closeTab(t.id)) + }, + }, + { + id: 'show-explorer', + label: 'Focus Explorer', + shortcut: '', + category: 'View', + action: () => layoutStore.setActiveView('explorer'), + }, + { + id: 'show-search', + label: 'Show Search', + shortcut: '', + category: 'View', + action: () => layoutStore.setActiveView('search'), + }, + ] +} + +export const useCommandStore = create<CommandState>((set, get) => ({ + isOpen: false, + query: '', + selectedIndex: 0, + commands: [], + + open: () => { + set({ isOpen: true, query: '', selectedIndex: 0, commands: buildCommands() }) + }, + + close: () => { + set({ isOpen: false, query: '', selectedIndex: 0 }) + }, + + toggle: () => { + const { isOpen } = get() + if (isOpen) { + get().close() + } else { + get().open() + } + }, + + setQuery: (query: string) => { + set({ query, selectedIndex: 0 }) + }, + + setSelectedIndex: (index: number) => { + set({ selectedIndex: index }) + }, + + executeSelected: () => { + const { commands, query, selectedIndex } = get() + const filtered = commands.filter( + (c) => + c.label.toLowerCase().includes(query.toLowerCase()) || + c.category.toLowerCase().includes(query.toLowerCase()) + ) + const cmd = filtered[selectedIndex] + if (cmd) { + cmd.action() + get().close() + } + }, +})) diff --git a/src/stores/useContextMenuStore.ts b/src/stores/useContextMenuStore.ts new file mode 100644 index 0000000..a0fd2cf --- /dev/null +++ b/src/stores/useContextMenuStore.ts @@ -0,0 +1,31 @@ +import { create } from 'zustand' + +export type ContextMenuIcon = 'copy' | 'cut' | 'paste' | 'undo' | 'redo' | 'selectAll' | 'close' | 'folder' | 'file' | 'link' | 'rename' | 'trash' + +export interface ContextMenuItem { + id: string + label: string + shortcut?: string + icon?: ContextMenuIcon + action: () => void + disabled?: boolean + separator?: boolean +} + +interface ContextMenuState { + isOpen: boolean + x: number + y: number + items: ContextMenuItem[] + open: (x: number, y: number, items: ContextMenuItem[]) => void + close: () => void +} + +export const useContextMenuStore = create<ContextMenuState>((set) => ({ + isOpen: false, + x: 0, + y: 0, + items: [], + open: (x, y, items) => set({ isOpen: true, x, y, items }), + close: () => set({ isOpen: false }), +})) diff --git a/src/stores/useFilesStore.ts b/src/stores/useFilesStore.ts new file mode 100644 index 0000000..200fb68 --- /dev/null +++ b/src/stores/useFilesStore.ts @@ -0,0 +1,84 @@ +import { create } from 'zustand' +import type { FileEntry } from '@/types' +import { openDir, pickDirectory, writeFile, createDir } from '@/lib/tauri' + +interface FilesState { + fileTree: FileEntry[] + rootPath: string | null + loading: boolean + openFolder: () => Promise<void> + loadFolder: (path: string) => Promise<void> + createFile: (parentPath?: string) => Promise<string | null> + createFolder: (parentPath?: string) => Promise<string | null> +} + +function generateName(base: string, existing: string[]): string { + const used = new Set(existing) + let name = base + let counter = 1 + while (used.has(name)) { + name = `${base} ${counter++}` + } + return name +} + +function collectNames(entries: FileEntry[]): string[] { + const names: string[] = [] + for (const e of entries) { + names.push(e.name) + } + return names +} + +export const useFilesStore = create<FilesState>((set, get) => ({ + fileTree: [], + rootPath: null, + loading: false, + + openFolder: async () => { + const path = await pickDirectory() + if (!path) return + set({ rootPath: path, loading: true }) + localStorage.setItem('yace_last_folder', path) + try { + const entries = await openDir(path) + set({ fileTree: entries, loading: false }) + } catch { + set({ loading: false }) + } + }, + + loadFolder: async (path: string) => { + set({ loading: true }) + try { + const entries = await openDir(path) + set({ fileTree: entries, rootPath: path, loading: false }) + } catch { + set({ loading: false }) + } + }, + + createFile: async (parentPath?: string): Promise<string | null> => { + const { rootPath, fileTree } = get() + if (!rootPath) return null + const targetDir = parentPath ?? rootPath + const name = generateName('Untitled-1', collectNames(fileTree)) + const filePath = `${targetDir}\\${name}` + await writeFile(filePath, '') + const entries = await openDir(rootPath) + set({ fileTree: entries }) + return filePath + }, + + createFolder: async (parentPath?: string): Promise<string | null> => { + const { rootPath, fileTree } = get() + if (!rootPath) return null + const targetDir = parentPath ?? rootPath + const name = generateName('New Folder', collectNames(fileTree)) + const dirPath = `${targetDir}\\${name}` + await createDir(dirPath) + const entries = await openDir(rootPath) + set({ fileTree: entries }) + return dirPath + }, +})) diff --git a/src/stores/useLayoutStore.ts b/src/stores/useLayoutStore.ts new file mode 100644 index 0000000..adcdae7 --- /dev/null +++ b/src/stores/useLayoutStore.ts @@ -0,0 +1,51 @@ +import { create } from 'zustand' + +export type ActivityView = 'explorer' | 'search' | 'extensions' | 'settings' + +interface LayoutState { + activeView: ActivityView | null + sidePanelVisible: boolean + sidePanelWidth: number + setActiveView: (view: ActivityView | null) => void + toggleSidePanel: () => void + showSidePanel: () => void + hideSidePanel: () => void + setSidePanelWidth: (width: number) => void +} + +const SIDEPANEL_MIN = 180 +const SIDEPANEL_MAX = 500 +const SIDEPANEL_DEFAULT = 240 + +export const useLayoutStore = create<LayoutState>((set, get) => ({ + activeView: 'explorer', + sidePanelVisible: true, + sidePanelWidth: SIDEPANEL_DEFAULT, + + setActiveView: (view: ActivityView | null) => { + const { activeView, hideSidePanel, showSidePanel } = get() + if (activeView === view) { + hideSidePanel() + set({ activeView: null }) + } else { + set({ activeView: view }) + showSidePanel() + } + }, + + toggleSidePanel: () => { + set((s) => ({ sidePanelVisible: !s.sidePanelVisible })) + }, + + showSidePanel: () => { + set({ sidePanelVisible: true }) + }, + + hideSidePanel: () => { + set({ sidePanelVisible: false }) + }, + + setSidePanelWidth: (width: number) => { + set({ sidePanelWidth: Math.max(SIDEPANEL_MIN, Math.min(SIDEPANEL_MAX, width)) }) + }, +})) diff --git a/src/stores/useSettingsStore.ts b/src/stores/useSettingsStore.ts new file mode 100644 index 0000000..7f2be4f --- /dev/null +++ b/src/stores/useSettingsStore.ts @@ -0,0 +1,34 @@ +import { create } from 'zustand' +import { persist } from 'zustand/middleware' + +interface SettingsState { + fontSize: number + tabSize: number + fontFamily: string + wordWrap: boolean + lineNumbers: boolean + ruler: boolean + rulerColumn: number + autoSave: boolean + autoSaveDelay: number + + set: <K extends keyof SettingsState>(key: K, value: SettingsState[K]) => void +} + +export const useSettingsStore = create<SettingsState>()( + persist( + (set) => ({ + fontSize: 14, + tabSize: 2, + fontFamily: 'JetBrains Mono', + wordWrap: true, + lineNumbers: true, + ruler: true, + rulerColumn: 80, + autoSave: false, + autoSaveDelay: 2000, + set: (key, value) => set({ [key]: value }), + }), + { name: 'yace-settings' } + ) +) diff --git a/src/stores/useTabsStore.ts b/src/stores/useTabsStore.ts new file mode 100644 index 0000000..eb9a03a --- /dev/null +++ b/src/stores/useTabsStore.ts @@ -0,0 +1,152 @@ +import { create } from 'zustand' +import type { Tab } from '@/types' +import { readFile, writeFile, saveDialog, pickFile } from '@/lib/tauri' +import { detectLanguage } from '@/lib/languageMap' + +interface TabsState { + tabs: Tab[] + activeTabId: string | null + cursorLine: number + cursorCol: number + savingId: string | null + statusMessage: string | null + untitledCounter: number + + openFile: (path: string) => Promise<void> + pickAndOpenFile: () => Promise<void> + createUntitledFile: () => void + closeTab: (id: string) => void + setActiveTab: (id: string) => void + updateContent: (id: string, content: string) => void + saveFile: (id: string) => Promise<void> + setCursorPosition: (line: number, col: number) => void + clearStatusMessage: () => void +} + +function isUntitled(id: string) { + return id.startsWith('untitled://') +} + +export const useTabsStore = create<TabsState>((set, get) => ({ + tabs: [], + activeTabId: null, + cursorLine: 1, + cursorCol: 1, + savingId: null, + statusMessage: null, + untitledCounter: 0, + + openFile: async (path: string) => { + const { tabs } = get() + const existing = tabs.find((t) => t.path === path) + if (existing) { + set({ activeTabId: existing.id }) + return + } + + try { + const content = await readFile(path) + const name = path.split(/[\\/]/).pop() ?? 'untitled' + const id = path + const tab: Tab = { + id, + path, + name, + language: detectLanguage(name), + content, + modified: false, + } + set({ tabs: [...tabs, tab], activeTabId: id }) + } catch (err) { + console.error('Failed to open file:', err) + } + }, + + pickAndOpenFile: async () => { + const path = await pickFile() + if (path) get().openFile(path) + }, + + createUntitledFile: () => { + const { tabs, untitledCounter } = get() + const n = untitledCounter + 1 + const name = `Untitled-${n}` + const id = `untitled://${n}` + const tab: Tab = { + id, + path: id, + name, + language: 'plaintext', + content: '', + modified: false, + } + set({ tabs: [...tabs, tab], activeTabId: id, untitledCounter: n }) + }, + + closeTab: (id: string) => { + const { tabs, activeTabId } = get() + const newTabs = tabs.filter((t) => t.id !== id) + let newActive = activeTabId + if (activeTabId === id) { + const idx = tabs.findIndex((t) => t.id === id) + newActive = newTabs[Math.min(idx, newTabs.length - 1)]?.id ?? null + } + set({ tabs: newTabs, activeTabId: newActive }) + }, + + setActiveTab: (id: string) => { + set({ activeTabId: id }) + }, + + updateContent: (id: string, content: string) => { + set((state) => ({ + tabs: state.tabs.map((t) => + t.id === id ? { ...t, content, modified: true } : t + ), + })) + }, + + saveFile: async (id: string) => { + const { tabs } = get() + const tab = tabs.find((t) => t.id === id) + if (!tab) return + + set({ savingId: id, statusMessage: null }) + + try { + let targetPath = tab.path + + if (isUntitled(targetPath)) { + const picked = await saveDialog(tab.name) + if (!picked) { + set({ savingId: null }) + return + } + targetPath = picked + } + + await writeFile(targetPath, tab.content) + const name = targetPath.split(/[\\/]/).pop() ?? tab.name + set({ + tabs: tabs.map((t) => + t.id === id + ? { ...t, path: targetPath, name, language: detectLanguage(name), modified: false } + : t + ), + savingId: null, + statusMessage: 'Saved', + }) + } catch (err) { + set({ savingId: null, statusMessage: 'Save failed' }) + console.error('Failed to save file:', err) + } + }, + + setCursorPosition: (line: number, col: number) => { + set({ cursorLine: line, cursorCol: col }) + }, + + clearStatusMessage: () => { + set({ statusMessage: null }) + }, +})) diff --git a/src/stores/useThemeStore.ts b/src/stores/useThemeStore.ts new file mode 100644 index 0000000..d9c3d06 --- /dev/null +++ b/src/stores/useThemeStore.ts @@ -0,0 +1,41 @@ +import { create } from 'zustand' + +export type Theme = 'dark' | 'light' + +interface ThemeState { + theme: Theme + setTheme: (theme: Theme) => void + toggleTheme: () => void +} + +function getInitialTheme(): Theme { + const stored = localStorage.getItem('yace_theme') + if (stored === 'light' || stored === 'dark') return stored + return 'dark' +} + +function applyTheme(theme: Theme) { + document.documentElement.setAttribute('data-theme', theme) +} + +const initial = getInitialTheme() +applyTheme(initial) + +export const useThemeStore = create<ThemeState>((set) => ({ + theme: initial, + + setTheme: (theme: Theme) => { + applyTheme(theme) + localStorage.setItem('yace_theme', theme) + set({ theme }) + }, + + toggleTheme: () => { + set((state) => { + const next = state.theme === 'dark' ? 'light' : 'dark' + applyTheme(next) + localStorage.setItem('yace_theme', next) + return { theme: next } + }) + }, +})) diff --git a/src/stores/useToastStore.ts b/src/stores/useToastStore.ts new file mode 100644 index 0000000..ab45325 --- /dev/null +++ b/src/stores/useToastStore.ts @@ -0,0 +1,31 @@ +import { create } from 'zustand' + +export type ToastType = 'success' | 'error' | 'info' | 'warning' + +export interface Toast { + id: string + message: string + type: ToastType +} + +interface ToastState { + toasts: Toast[] + addToast: (message: string, type?: ToastType, duration?: number) => void + removeToast: (id: string) => void +} + +let counter = 0 + +export const useToastStore = create<ToastState>((set, get) => ({ + toasts: [], + addToast: (message, type = 'info', duration = 3500) => { + const id = `toast-${++counter}` + set((s) => ({ toasts: [...s.toasts, { id, message, type }] })) + setTimeout(() => { + get().removeToast(id) + }, duration) + }, + removeToast: (id) => { + set((s) => ({ toasts: s.toasts.filter((t) => t.id !== id) })) + }, +})) |