Initial commit

This commit is contained in:
2026-03-08 01:34:54 +08:00
commit 1f104f73c8
441 changed files with 64911 additions and 0 deletions

34
src/stores/dragStore.ts Normal file
View File

@@ -0,0 +1,34 @@
import { create } from 'zustand'
export interface DragState {
draggedPath: string | null
draggedType: 'file' | 'dir' | null
dropTargetPath: string | null
}
interface DragStore {
state: DragState
setState: (state: DragState) => void
isDraggedInsidePath: (checkPath: string) => boolean
}
const initialState: DragState = {
draggedPath: null,
draggedType: null,
dropTargetPath: null
}
export const useDragStore = create<DragStore>((set, get) => ({
state: initialState,
setState: (state) => set({ state }),
isDraggedInsidePath: (checkPath) => {
const { state } = get()
if (!state.draggedPath) return false
if (state.draggedType === 'dir') {
return checkPath.startsWith(`${state.draggedPath}/`)
}
return false
}
}))

31
src/stores/index.ts Normal file
View File

@@ -0,0 +1,31 @@
export { useUIStore, useTheme, useWallpaper, useMarkdownDisplay } from './uiStore'
export { useScreenshotStore, useScreenshot } from './screenshotStore'
export { useDragStore, type DragState } from './dragStore'
export { useTabStore, isSpecialTab, isRegularMarkdownFile, getDerivedEditState, type TabState } from './tabStore'
import { useTabStore } from './tabStore'
export const useTabManagerStore = useTabStore
export const useTabEditStore = useTabStore
export const loadTabContent = useTabStore.getState().loadContent
export const saveTabContent = useTabStore.getState().saveContent
export const useTabContentContext = () => {
const store = useTabStore()
const openFiles = Array.from(store.tabs.values()).map(t => t.file)
const selectedFile = store.activeTabId ? store.tabs.get(store.activeTabId)?.file ?? null : null
return {
openFiles,
selectedFile,
setSelectedFile: store.setActiveFile,
selectFile: store.selectFile,
closeFile: store.closeFile,
closeOtherFiles: store.closeOtherFiles,
closeAllFiles: store.closeAllFiles,
renamePath: store.renamePath,
handleDeletePath: store.handleDeletePath,
}
}

View File

@@ -0,0 +1,60 @@
import { create } from 'zustand'
import { getScreenshot, saveScreenshot, getDeviceData, saveDeviceData } from '@/modules/remote/api'
interface ScreenshotState {
screenshot: string
lastConnected: string
loadScreenshots: (deviceName?: string) => Promise<void>
updateScreenshot: (dataUrl: string, deviceName?: string) => Promise<void>
updateLastConnected: () => void
}
const formatDateTime = () => {
const now = new Date()
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`
}
export const useScreenshotStore = create<ScreenshotState>((set) => ({
screenshot: '',
lastConnected: '',
loadScreenshots: async (deviceName) => {
try {
const [loaded, data] = await Promise.all([
getScreenshot(deviceName),
getDeviceData(deviceName)
])
set({ screenshot: loaded || '', lastConnected: data?.lastConnected || '' })
} catch (err) {
console.error('Failed to load screenshot:', err)
set({ screenshot: '', lastConnected: '' })
}
},
updateScreenshot: async (dataUrl, deviceName) => {
const timeStr = formatDateTime()
set({ screenshot: dataUrl, lastConnected: timeStr })
try {
await Promise.all([
saveScreenshot(dataUrl, deviceName),
deviceName ? saveDeviceData(deviceName, timeStr) : Promise.resolve()
])
} catch (err) {
console.error('Failed to save screenshot:', err)
}
},
updateLastConnected: () => {
set({ lastConnected: formatDateTime() })
}
}))
export const useScreenshot = () => {
const screenshot = useScreenshotStore((s) => s.screenshot)
const lastConnected = useScreenshotStore((s) => s.lastConnected)
const updateScreenshot = useScreenshotStore((s) => s.updateScreenshot)
const updateLastConnected = useScreenshotStore((s) => s.updateLastConnected)
const loadScreenshots = useScreenshotStore((s) => s.loadScreenshots)
return { screenshot, lastConnected, updateScreenshot, updateLastConnected, loadScreenshots }
}

446
src/stores/tabStore.ts Normal file
View File

@@ -0,0 +1,446 @@
import { create } from 'zustand'
import type { FileItem } from '@/lib/api'
import { getModule } from '@/lib/module-registry'
import { fetchFileContent, saveFileContent } from '@/lib/api'
import { isMarkdownFile } from '@/lib/utils'
import { matchModule } from '@/lib/tabs'
import { isAsyncImportProcessingContent } from '@shared/constants'
const getHomeFileItem = (): FileItem => {
const homeModule = getModule('home')
return homeModule?.fileItem ?? {
name: '首页',
path: 'home-tab',
type: 'file',
size: 0,
modified: new Date().toISOString(),
}
}
const getFileName = (path: string): string => {
const parts = path.replace(/\\/g, '/').split('/')
return parts[parts.length - 1] || ''
}
export interface TabState {
file: FileItem
content: string
unsavedContent: string
isEditing: boolean
loading: boolean
loaded: boolean
}
export interface TabStoreState {
tabs: Map<string, TabState>
activeTabId: string | null
}
interface TabActions {
selectFile: (file: FileItem) => void
setActiveFile: (file: FileItem | null) => void
closeFile: (file: FileItem) => void
closeOtherFiles: (fileToKeep: FileItem) => void
closeAllFiles: () => void
renamePath: (oldPath: string, newPath: string) => void
handleDeletePath: (path: string, type: 'file' | 'dir') => void
loadContent: (filePath: string) => Promise<void>
saveContent: (filePath: string) => Promise<void>
setEditing: (filePath: string, editing: boolean) => void
setUnsavedContent: (filePath: string, content: string) => void
toggleEditing: (filePath: string) => void
getTab: (filePath: string) => TabState | undefined
getActiveTab: () => TabState | undefined
getActiveFile: () => FileItem | null
getOpenFiles: () => FileItem[]
hasUnsavedChanges: (filePath: string) => boolean
isLoading: (filePath: string) => boolean
}
export type TabStore = TabStoreState & TabActions
const getInitialState = (): TabStoreState => {
const homeFileItem = getHomeFileItem()
const homeTab: TabState = {
file: homeFileItem,
content: '',
unsavedContent: '',
isEditing: false,
loading: false,
loaded: true,
}
const tabs = new Map<string, TabState>()
tabs.set(homeFileItem.path, homeTab)
return {
tabs,
activeTabId: homeFileItem.path,
}
}
export const useTabStore = create<TabStore>((set, get) => {
const initialState = getInitialState()
return {
...initialState,
selectFile: (file) => set((state) => {
const tabs = new Map(state.tabs)
const existingTab = tabs.get(file.path)
if (!existingTab) {
const newTab: TabState = {
file,
content: '',
unsavedContent: '',
isEditing: false,
loading: false,
loaded: false,
}
tabs.set(file.path, newTab)
}
return {
tabs,
activeTabId: file.path,
}
}),
setActiveFile: (file) => set((state) => {
if (!file) {
return { activeTabId: null }
}
const tabs = new Map(state.tabs)
tabs.forEach((tab, path) => {
tabs.set(path, { ...tab })
})
return {
tabs,
activeTabId: file.path,
}
}),
closeFile: (file) => set((state) => {
const homeFileItem = getHomeFileItem()
const tabs = new Map(state.tabs)
const filePath = file.path
const allPaths = Array.from(tabs.keys())
const currentIndex = allPaths.indexOf(filePath)
let newActiveTabId = state.activeTabId
if (state.activeTabId === filePath) {
tabs.delete(filePath)
const remainingPaths = Array.from(tabs.keys())
if (remainingPaths.length > 0) {
const prevIndex = Math.max(0, currentIndex - 1)
newActiveTabId = remainingPaths[prevIndex]
} else {
const homeTab: TabState = {
file: homeFileItem,
content: '',
unsavedContent: '',
isEditing: false,
loading: false,
loaded: true,
}
tabs.set(homeFileItem.path, homeTab)
newActiveTabId = homeFileItem.path
}
} else {
tabs.delete(filePath)
}
return {
tabs,
activeTabId: newActiveTabId,
}
}),
closeOtherFiles: (fileToKeep) => set((state) => {
const homeFileItem = getHomeFileItem()
const tabs = new Map(state.tabs)
const keepPath = fileToKeep.path
const tabToKeep = tabs.get(keepPath)
const newTabs = new Map<string, TabState>()
if (tabToKeep) {
newTabs.set(keepPath, tabToKeep)
} else {
newTabs.set(keepPath, {
file: fileToKeep,
content: '',
unsavedContent: '',
isEditing: false,
loading: false,
loaded: false,
})
}
if (newTabs.size === 0) {
const homeTab: TabState = {
file: homeFileItem,
content: '',
unsavedContent: '',
isEditing: false,
loading: false,
loaded: true,
}
newTabs.set(homeFileItem.path, homeTab)
}
return {
tabs: newTabs,
activeTabId: keepPath,
}
}),
closeAllFiles: () => set(() => {
const homeFileItem = getHomeFileItem()
const homeTab: TabState = {
file: homeFileItem,
content: '',
unsavedContent: '',
isEditing: false,
loading: false,
loaded: true,
}
const tabs = new Map<string, TabState>()
tabs.set(homeFileItem.path, homeTab)
return {
tabs,
activeTabId: homeFileItem.path,
}
}),
renamePath: (oldPath, newPath) => set((state) => {
const tabs = new Map(state.tabs)
const oldTab = tabs.get(oldPath)
if (oldTab) {
const newFile = { ...oldTab.file, path: newPath, name: getFileName(newPath) || oldTab.file.name }
tabs.set(newPath, { ...oldTab, file: newFile })
tabs.delete(oldPath)
}
return { tabs }
}),
handleDeletePath: (path, type) => set((state) => {
const homeFileItem = getHomeFileItem()
const tabs = new Map(state.tabs)
const isAffected = (filePath: string) => {
if (type === 'file') return filePath === path
return filePath === path || filePath.startsWith(path + '/')
}
const pathsToDelete: string[] = []
tabs.forEach((_, filePath) => {
if (isAffected(filePath)) {
pathsToDelete.push(filePath)
}
})
pathsToDelete.forEach(p => tabs.delete(p))
let newActiveTabId = state.activeTabId
if (state.activeTabId && isAffected(state.activeTabId)) {
const remainingPaths = Array.from(tabs.keys())
if (remainingPaths.length > 0) {
newActiveTabId = remainingPaths[0]
} else {
const homeTab: TabState = {
file: homeFileItem,
content: '',
unsavedContent: '',
isEditing: false,
loading: false,
loaded: true,
}
tabs.set(homeFileItem.path, homeTab)
newActiveTabId = homeFileItem.path
}
}
return {
tabs,
activeTabId: newActiveTabId,
}
}),
loadContent: async (filePath) => {
const { tabs } = get()
const tab = tabs.get(filePath)
if (!tab) return
set((state) => {
const newTabs = new Map(state.tabs)
newTabs.set(filePath, { ...tab, loading: true })
return { tabs: newTabs }
})
try {
const data = await fetchFileContent(filePath)
set((state) => {
const newTabs = new Map(state.tabs)
const currentTab = newTabs.get(filePath)
if (currentTab) {
newTabs.set(filePath, {
...currentTab,
content: data.content,
unsavedContent: data.content,
loading: false,
loaded: true,
})
}
return { tabs: newTabs }
})
} catch (error) {
console.error('Failed to load content', error)
const errorMsg = '# Error\nFailed to load file content.'
set((state) => {
const newTabs = new Map(state.tabs)
const currentTab = newTabs.get(filePath)
if (currentTab) {
newTabs.set(filePath, {
...currentTab,
content: errorMsg,
unsavedContent: errorMsg,
loading: false,
loaded: true,
})
}
return { tabs: newTabs }
})
}
},
saveContent: async (filePath) => {
const tab = get().tabs.get(filePath)
if (!tab) return
const isAsyncImportProcessing = isAsyncImportProcessingContent(tab.content)
if (isAsyncImportProcessing) return
await saveFileContent(filePath, tab.unsavedContent)
set((state) => {
const newTabs = new Map(state.tabs)
const currentTab = newTabs.get(filePath)
if (currentTab) {
newTabs.set(filePath, {
...currentTab,
content: currentTab.unsavedContent,
isEditing: false,
})
}
return { tabs: newTabs }
})
},
setEditing: (filePath, editing) => set((state) => {
const tab = state.tabs.get(filePath)
if (!tab) return state
const newTabs = new Map(state.tabs)
newTabs.set(filePath, {
...tab,
isEditing: editing,
unsavedContent: editing ? tab.unsavedContent : tab.content,
})
return { tabs: newTabs }
}),
setUnsavedContent: (filePath, content) => set((state) => {
const tab = state.tabs.get(filePath)
if (!tab) return state
const newTabs = new Map(state.tabs)
newTabs.set(filePath, { ...tab, unsavedContent: content })
return { tabs: newTabs }
}),
toggleEditing: (filePath) => set((state) => {
const tab = state.tabs.get(filePath)
if (!tab) return state
const isAsyncImportProcessing = isAsyncImportProcessingContent(tab.content)
if (isAsyncImportProcessing) return state
const newTabs = new Map(state.tabs)
newTabs.set(filePath, {
...tab,
isEditing: !tab.isEditing,
unsavedContent: !tab.isEditing ? tab.content : tab.unsavedContent,
})
return { tabs: newTabs }
}),
getTab: (filePath) => get().tabs.get(filePath),
getActiveTab: () => {
const { tabs, activeTabId } = get()
return activeTabId ? tabs.get(activeTabId) : undefined
},
getActiveFile: () => {
const activeTab = get().getActiveTab()
return activeTab?.file ?? null
},
getOpenFiles: () => {
const { tabs } = get()
return Array.from(tabs.values()).map(tab => tab.file)
},
hasUnsavedChanges: (filePath) => {
const tab = get().tabs.get(filePath)
if (!tab) return false
return tab.unsavedContent !== tab.content
},
isLoading: (filePath) => {
const tab = get().tabs.get(filePath)
return tab?.loading ?? false
},
}
})
export const isSpecialTab = (file: FileItem | null): boolean => {
if (!file) return false
if (file.path.startsWith('remote-desktop://')) return true
if (file.path.startsWith('remote-git://')) return true
return matchModule(file) !== undefined
}
export const isRegularMarkdownFile = (file: FileItem | null): boolean => {
return !isSpecialTab(file) && !!file && isMarkdownFile(file.name)
}
export const getDerivedEditState = (selectedFilePath: string | null) => {
const { tabs } = useTabStore.getState()
const tab = selectedFilePath ? tabs.get(selectedFilePath) : undefined
const isEditing = tab?.isEditing || false
const hasUnsavedChanges = tab ? tab.unsavedContent !== tab.content : false
const content = isEditing ? (tab?.unsavedContent || '') : (tab?.content || '')
const wordCount = content.replace(/\s/g, '').length
return {
isCurrentTabEditing: isEditing,
hasCurrentTabUnsavedChanges: hasUnsavedChanges,
currentContent: content,
currentWordCount: wordCount,
isLoading: tab?.loading || false,
isLoaded: tab?.loaded || false,
}
}

91
src/stores/uiStore.ts Normal file
View File

@@ -0,0 +1,91 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import type { ThemeMode } from '@shared/types'
const getSystemPreferredTheme = (): ThemeMode => {
if (typeof window === 'undefined') return 'light'
try {
return window.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
} catch {
return 'light'
}
}
const clampValue = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value))
interface UIState {
theme: ThemeMode
isDark: boolean
setTheme: (theme: ThemeMode) => void
toggleTheme: () => void
opacity: number
setOpacity: (opacity: number) => void
fontSize: number
setFontSize: (fontSize: number) => void
zoom: number
setZoom: (zoom: number) => void
zoomIn: () => void
zoomOut: () => void
resetZoom: () => void
}
export const useUIStore = create<UIState>()(
persist(
(set, get) => ({
theme: getSystemPreferredTheme(),
isDark: getSystemPreferredTheme() === 'dark',
setTheme: (theme) => set({ theme, isDark: theme === 'dark' }),
toggleTheme: () => {
const newTheme = get().theme === 'dark' ? 'light' : 'dark'
set({ theme: newTheme, isDark: newTheme === 'dark' })
},
opacity: 0.8,
setOpacity: (opacity) => set({ opacity }),
fontSize: 16,
setFontSize: (fontSize) => set({ fontSize }),
zoom: 100,
setZoom: (zoom) => set({ zoom: clampValue(Math.round(zoom), 50, 200) }),
zoomIn: () => set((s) => ({ zoom: clampValue(s.zoom + 10, 50, 200) })),
zoomOut: () => set((s) => ({ zoom: clampValue(s.zoom - 10, 50, 200) })),
resetZoom: () => set({ zoom: 100 }),
}),
{
name: 'ui-storage',
partialize: (state) => ({
theme: state.theme,
opacity: state.opacity,
fontSize: state.fontSize,
zoom: state.zoom,
}),
}
)
)
export const useTheme = () => {
const theme = useUIStore((s) => s.theme)
const isDark = useUIStore((s) => s.isDark)
const setTheme = useUIStore((s) => s.setTheme)
const toggleTheme = useUIStore((s) => s.toggleTheme)
return { theme, isDark, setTheme, toggleTheme }
}
export const useWallpaper = () => {
const opacity = useUIStore((s) => s.opacity)
const setOpacity = useUIStore((s) => s.setOpacity)
return { opacity, setOpacity }
}
export const useMarkdownDisplay = () => {
const fontSize = useUIStore((s) => s.fontSize)
const setFontSize = useUIStore((s) => s.setFontSize)
const zoom = useUIStore((s) => s.zoom)
const setZoom = useUIStore((s) => s.setZoom)
const zoomIn = useUIStore((s) => s.zoomIn)
const zoomOut = useUIStore((s) => s.zoomOut)
const resetZoom = useUIStore((s) => s.resetZoom)
return { fontSize, setFontSize, zoom, setZoom, zoomIn, zoomOut, resetZoom }
}