新的提交

This commit is contained in:
cc
2026-01-10 13:01:37 +08:00
commit 01641834de
188 changed files with 34865 additions and 0 deletions

View File

@@ -0,0 +1,70 @@
import { create } from 'zustand'
interface ChatStatistics {
totalMessages: number
textMessages: number
imageMessages: number
voiceMessages: number
videoMessages: number
emojiMessages: number
otherMessages: number
sentMessages: number
receivedMessages: number
firstMessageTime: number | null
lastMessageTime: number | null
activeDays: number
messageTypeCounts: Record<number, number>
}
interface ContactRanking {
username: string
displayName: string
avatarUrl?: string
messageCount: number
sentCount: number
receivedCount: number
lastMessageTime: number | null
}
interface TimeDistribution {
hourlyDistribution: Record<number, number>
monthlyDistribution: Record<string, number>
}
interface AnalyticsState {
// 数据
statistics: ChatStatistics | null
rankings: ContactRanking[]
timeDistribution: TimeDistribution | null
// 状态
isLoaded: boolean
lastLoadTime: number | null
// Actions
setStatistics: (data: ChatStatistics) => void
setRankings: (data: ContactRanking[]) => void
setTimeDistribution: (data: TimeDistribution) => void
markLoaded: () => void
clearCache: () => void
}
export const useAnalyticsStore = create<AnalyticsState>((set) => ({
statistics: null,
rankings: [],
timeDistribution: null,
isLoaded: false,
lastLoadTime: null,
setStatistics: (data) => set({ statistics: data }),
setRankings: (data) => set({ rankings: data }),
setTimeDistribution: (data) => set({ timeDistribution: data }),
markLoaded: () => set({ isLoaded: true, lastLoadTime: Date.now() }),
clearCache: () => set({
statistics: null,
rankings: [],
timeDistribution: null,
isLoaded: false,
lastLoadTime: null
}),
}))

46
src/stores/appStore.ts Normal file
View File

@@ -0,0 +1,46 @@
import { create } from 'zustand'
export interface AppState {
// 数据库状态
isDbConnected: boolean
dbPath: string | null
myWxid: string | null
// 加载状态
isLoading: boolean
loadingText: string
// 操作
setDbConnected: (connected: boolean, path?: string) => void
setMyWxid: (wxid: string) => void
setLoading: (loading: boolean, text?: string) => void
reset: () => void
}
export const useAppStore = create<AppState>((set) => ({
isDbConnected: false,
dbPath: null,
myWxid: null,
isLoading: false,
loadingText: '',
setDbConnected: (connected, path) => set({
isDbConnected: connected,
dbPath: path ?? null
}),
setMyWxid: (wxid) => set({ myWxid: wxid }),
setLoading: (loading, text) => set({
isLoading: loading,
loadingText: text ?? ''
}),
reset: () => set({
isDbConnected: false,
dbPath: null,
myWxid: null,
isLoading: false,
loadingText: ''
})
}))

116
src/stores/chatStore.ts Normal file
View File

@@ -0,0 +1,116 @@
import { create } from 'zustand'
import type { ChatSession, Message, Contact } from '../types/models'
export interface ChatState {
// 连接状态
isConnected: boolean
isConnecting: boolean
connectionError: string | null
// 会话列表
sessions: ChatSession[]
filteredSessions: ChatSession[]
currentSessionId: string | null
isLoadingSessions: boolean
// 消息
messages: Message[]
isLoadingMessages: boolean
isLoadingMore: boolean
hasMoreMessages: boolean
// 联系人缓存
contacts: Map<string, Contact>
// 搜索
searchKeyword: string
// 操作
setConnected: (connected: boolean) => void
setConnecting: (connecting: boolean) => void
setConnectionError: (error: string | null) => void
setSessions: (sessions: ChatSession[]) => void
setFilteredSessions: (sessions: ChatSession[]) => void
setCurrentSession: (sessionId: string | null) => void
setLoadingSessions: (loading: boolean) => void
setMessages: (messages: Message[]) => void
appendMessages: (messages: Message[], prepend?: boolean) => void
setLoadingMessages: (loading: boolean) => void
setLoadingMore: (loading: boolean) => void
setHasMoreMessages: (hasMore: boolean) => void
setContacts: (contacts: Contact[]) => void
addContact: (contact: Contact) => void
setSearchKeyword: (keyword: string) => void
reset: () => void
}
export const useChatStore = create<ChatState>((set, get) => ({
isConnected: false,
isConnecting: false,
connectionError: null,
sessions: [],
filteredSessions: [],
currentSessionId: null,
isLoadingSessions: false,
messages: [],
isLoadingMessages: false,
isLoadingMore: false,
hasMoreMessages: true,
contacts: new Map(),
searchKeyword: '',
setConnected: (connected) => set({ isConnected: connected }),
setConnecting: (connecting) => set({ isConnecting: connecting }),
setConnectionError: (error) => set({ connectionError: error }),
setSessions: (sessions) => set({ sessions, filteredSessions: sessions }),
setFilteredSessions: (sessions) => set({ filteredSessions: sessions }),
setCurrentSession: (sessionId) => set({
currentSessionId: sessionId,
messages: [],
hasMoreMessages: true
}),
setLoadingSessions: (loading) => set({ isLoadingSessions: loading }),
setMessages: (messages) => set({ messages }),
appendMessages: (newMessages, prepend = false) => set((state) => ({
messages: prepend
? [...newMessages, ...state.messages]
: [...state.messages, ...newMessages]
})),
setLoadingMessages: (loading) => set({ isLoadingMessages: loading }),
setLoadingMore: (loading) => set({ isLoadingMore: loading }),
setHasMoreMessages: (hasMore) => set({ hasMoreMessages: hasMore }),
setContacts: (contacts) => set({
contacts: new Map(contacts.map(c => [c.username, c]))
}),
addContact: (contact) => set((state) => {
const newContacts = new Map(state.contacts)
newContacts.set(contact.username, contact)
return { contacts: newContacts }
}),
setSearchKeyword: (keyword) => set({ searchKeyword: keyword }),
reset: () => set({
isConnected: false,
isConnecting: false,
connectionError: null,
sessions: [],
filteredSessions: [],
currentSessionId: null,
isLoadingSessions: false,
messages: [],
isLoadingMessages: false,
isLoadingMore: false,
hasMoreMessages: true,
contacts: new Map(),
searchKeyword: ''
})
}))

173
src/stores/imageStore.ts Normal file
View File

@@ -0,0 +1,173 @@
import { create } from 'zustand'
export interface ImageFileInfo {
fileName: string
filePath: string
fileSize: number
isDecrypted: boolean
decryptedPath?: string
version: number
isDecrypting?: boolean
}
export interface ImageDirectory {
wxid: string
path: string
}
/**
* 检测图片质量(原图/缩略图)
* 逻辑来自原项目 app_state.dart 的 _detectImageQuality
*/
function detectImageQuality(img: ImageFileInfo): 'original' | 'thumbnail' {
const fileNameLower = img.fileName.toLowerCase()
const fileSize = img.fileSize
// 小于 50KB 是缩略图
if (fileSize < 50 * 1024) return 'thumbnail'
// 大于 500KB 是原图
if (fileSize > 500 * 1024) return 'original'
// 文件名包含 thumb/small 关键词
if (fileNameLower.includes('thumb') || fileNameLower.includes('small')) {
return 'thumbnail'
}
// 文件名以 _thumb.dat 或 _small.dat 结尾
if (fileNameLower.endsWith('_thumb.dat') || fileNameLower.endsWith('_small.dat')) {
return 'thumbnail'
}
// 路径层级判断(通过 filePath 中的分隔符数量)
const pathParts = img.filePath.split(/[/\\]/)
// 找到账号目录后的相对路径层级
// 如果层级太深,可能是缩略图
if (pathParts.length > 10) return 'thumbnail'
return 'original'
}
interface ImageState {
// 图片列表
images: ImageFileInfo[]
// 目录列表
directories: ImageDirectory[]
// 当前选中的目录
selectedDir: ImageDirectory | null
// 扫描状态
isScanning: boolean
scanCompleted: boolean
// 错误信息
error: string | null
// 统计
originalCount: number
thumbnailCount: number
decryptedCount: number
// 操作
setDirectories: (dirs: ImageDirectory[]) => void
setSelectedDir: (dir: ImageDirectory | null) => void
setScanning: (scanning: boolean) => void
setScanCompleted: (completed: boolean) => void
setError: (error: string | null) => void
addImages: (newImages: ImageFileInfo[]) => void
clearImages: () => void
updateImage: (index: number, updates: Partial<ImageFileInfo>) => void
updateStats: () => void
reset: () => void
}
export const useImageStore = create<ImageState>((set, get) => ({
images: [],
directories: [],
selectedDir: null,
isScanning: false,
scanCompleted: false,
error: null,
originalCount: 0,
thumbnailCount: 0,
decryptedCount: 0,
setDirectories: (dirs) => set({ directories: dirs }),
setSelectedDir: (dir) => set({ selectedDir: dir }),
setScanning: (scanning) => set({ isScanning: scanning }),
setScanCompleted: (completed) => set({ scanCompleted: completed }),
setError: (error) => set({ error }),
addImages: (newImages) => {
set((state) => {
const updated = [...state.images, ...newImages]
// 计算统计
let original = 0
let thumbnail = 0
let decrypted = 0
for (const img of updated) {
if (detectImageQuality(img) === 'original') {
original++
} else {
thumbnail++
}
if (img.isDecrypted) decrypted++
}
return {
images: updated,
originalCount: original,
thumbnailCount: thumbnail,
decryptedCount: decrypted
}
})
},
clearImages: () => set({
images: [],
originalCount: 0,
thumbnailCount: 0,
decryptedCount: 0,
scanCompleted: false
}),
updateImage: (index, updates) => {
set((state) => {
const images = [...state.images]
if (index >= 0 && index < images.length) {
images[index] = { ...images[index], ...updates }
}
// 重新计算已解密数量
const decryptedCount = images.filter(img => img.isDecrypted).length
return { images, decryptedCount }
})
},
updateStats: () => {
const { images } = get()
let original = 0
let thumbnail = 0
let decrypted = 0
for (const img of images) {
if (detectImageQuality(img) === 'original') {
original++
} else {
thumbnail++
}
if (img.isDecrypted) decrypted++
}
set({ originalCount: original, thumbnailCount: thumbnail, decryptedCount: decrypted })
},
reset: () => set({
images: [],
directories: [],
selectedDir: null,
isScanning: false,
scanCompleted: false,
error: null,
originalCount: 0,
thumbnailCount: 0,
decryptedCount: 0
})
}))

79
src/stores/themeStore.ts Normal file
View File

@@ -0,0 +1,79 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
export type ThemeId = 'cloud-dancer' | 'corundum-blue' | 'kiwi-green' | 'spicy-red' | 'teal-water'
export type ThemeMode = 'light' | 'dark'
export interface ThemeInfo {
id: ThemeId
name: string
description: string
primaryColor: string
bgColor: string
}
export const themes: ThemeInfo[] = [
{
id: 'cloud-dancer',
name: '云上舞白',
description: 'Pantone 2026 年度色',
primaryColor: '#8B7355',
bgColor: '#F0EEE9'
},
{
id: 'corundum-blue',
name: '刚玉蓝',
description: 'RAL 220 40 10',
primaryColor: '#4A6670',
bgColor: '#E8EEF0'
},
{
id: 'kiwi-green',
name: '冰猕猴桃汁绿',
description: 'RAL 120 90 20',
primaryColor: '#7A9A5C',
bgColor: '#E8F0E4'
},
{
id: 'spicy-red',
name: '辛辣红',
description: 'RAL 030 40 40',
primaryColor: '#8B4049',
bgColor: '#F0E8E8'
},
{
id: 'teal-water',
name: '明水鸭色',
description: 'RAL 180 80 10',
primaryColor: '#5A8A8A',
bgColor: '#E4F0F0'
}
]
interface ThemeState {
currentTheme: ThemeId
themeMode: ThemeMode
setTheme: (theme: ThemeId) => void
setThemeMode: (mode: ThemeMode) => void
toggleThemeMode: () => void
}
export const useThemeStore = create<ThemeState>()(
persist(
(set, get) => ({
currentTheme: 'cloud-dancer',
themeMode: 'light',
setTheme: (theme) => set({ currentTheme: theme }),
setThemeMode: (mode) => set({ themeMode: mode }),
toggleThemeMode: () => set({ themeMode: get().themeMode === 'light' ? 'dark' : 'light' })
}),
{
name: 'echotrace-theme'
}
)
)
// 获取当前主题信息
export const getThemeInfo = (themeId: ThemeId): ThemeInfo => {
return themes.find(t => t.id === themeId) || themes[0]
}