Files
Thing/src/stores/clipboardStore.ts
T
2026-07-30 09:09:29 +08:00

294 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { invoke } from '@tauri-apps/api/core'
import { listen, type UnlistenFn } from '@tauri-apps/api/event'
import { createLogger } from '@/lib/logger'
const logger = createLogger('clipboard')
// ===== 与 Rust 端对应的数据结构(camelCase =====
export type ClipboardKind = 'text' | 'image' | 'files'
export interface ClipboardItem {
id: number
kind: ClipboardKind
preview: string
size: number
pinned: boolean
pinnedOrder: number | null
createdAt: number
}
/** 历史分页结果(与 Rust 端 HistoryPage 对应) */
export interface HistoryPage {
items: ClipboardItem[]
total: number
}
export interface ClipboardItemDetail extends ClipboardItem {
content: string | null
imageBase64: string | null
}
export interface ClipboardSettings {
enabled: boolean
maxItems: number
maxImageKb: number
recordText: boolean
recordImage: boolean
recordFiles: boolean
dedup: boolean
shortcut: string
}
export interface ClipboardStatus {
running: boolean
count: number
}
const DEFAULT_SETTINGS: ClipboardSettings = {
enabled: true,
maxItems: 500,
maxImageKb: 5120,
recordText: true,
recordImage: true,
recordFiles: true,
dedup: true,
shortcut: 'Alt+V',
}
export const useClipboardStore = defineStore('clipboard', () => {
const history = ref<ClipboardItem[]>([])
const historyTotal = ref(0)
const pinned = ref<ClipboardItem[]>([])
const settings = ref<ClipboardSettings>({ ...DEFAULT_SETTINGS })
const status = ref<ClipboardStatus>({ running: false, count: 0 })
const loading = ref(false)
// 事件监听(应用级单例,只注册一次)
let changedUnlisten: UnlistenFn | null = null
let debounceTimer: ReturnType<typeof setTimeout> | null = null
/** 初始化:加载状态/设置,注册事件监听 */
const init = async () => {
try {
const [s, st] = await Promise.all([
invoke<ClipboardSettings>('clipboard_get_settings'),
invoke<ClipboardStatus>('clipboard_status'),
])
settings.value = { ...DEFAULT_SETTINGS, ...s }
status.value = st
} catch (e) {
logger.error('初始化失败: ' + e)
}
if (!changedUnlisten) {
changedUnlisten = await listen('clipboard-changed', () => {
// 防抖:短时间内多次复制只刷新一次
if (debounceTimer) clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => {
refreshHistory()
refreshStatus()
}, 250)
})
}
}
const dispose = () => {
if (changedUnlisten) {
changedUnlisten()
changedUnlisten = null
}
if (debounceTimer) {
clearTimeout(debounceTimer)
debounceTimer = null
}
}
// ===== 查询 =====
/** 拉取指定页的历史数据。pageSize 默认 50。 */
const fetchHistoryPage = async (opts: {
kind?: string
page?: number
pageSize?: number
} = {}) => {
const kind = opts.kind ?? 'all'
const pageSize = opts.pageSize ?? 50
const page = Math.max(1, opts.page ?? 1)
const offset = (page - 1) * pageSize
try {
const res = await invoke<HistoryPage>('clipboard_get_history', {
limit: pageSize,
offset,
kind,
})
history.value = res.items
historyTotal.value = res.total
} catch (e) {
logger.error('获取历史失败: ' + e)
}
return history.value
}
/** 兼容旧调用:拉取第一页 */
const refreshHistory = async (kind: string = 'all') => fetchHistoryPage({ kind, page: 1 })
const refreshPinned = async () => {
try {
pinned.value = await invoke<ClipboardItem[]>('clipboard_get_pinned')
} catch (e) {
logger.error('获取固定条目失败: ' + e)
}
return pinned.value
}
/** 搜索(分页)。pageSize 默认 50。 */
const searchPage = async (query: string, page: number = 1, pageSize: number = 50) => {
if (!query.trim()) {
return fetchHistoryPage({ page, pageSize })
}
try {
const res = await invoke<HistoryPage>('clipboard_search', {
query,
limit: pageSize,
offset: (page - 1) * pageSize,
})
history.value = res.items
historyTotal.value = res.total
} catch (e) {
logger.error('搜索失败: ' + e)
}
return history.value
}
/** 兼容旧调用:搜索第一页 */
const search = async (query: string) => searchPage(query, 1)
const getItem = async (id: number) => {
try {
return await invoke<ClipboardItemDetail | null>('clipboard_get_item', { id })
} catch (e) {
logger.error('获取详情失败: ' + e)
return null
}
}
const refreshStatus = async () => {
try {
status.value = await invoke<ClipboardStatus>('clipboard_status')
} catch (e) {
logger.error('获取状态失败: ' + e)
}
}
// ===== 操作 =====
const setPinned = async (id: number, pinned: boolean) => {
try {
await invoke('clipboard_set_pinned', { id, pinned })
// 固定/取消后刷新两个列表
await Promise.all([refreshHistory(), refreshPinned()])
} catch (e) {
logger.error('切换固定失败: ' + e)
}
}
const remove = async (id: number) => {
try {
await invoke('clipboard_delete', { id })
history.value = history.value.filter((i) => i.id !== id)
pinned.value = pinned.value.filter((i) => i.id !== id)
status.value.count = Math.max(0, status.value.count - 1)
} catch (e) {
logger.error('删除失败: ' + e)
}
}
const clear = async () => {
try {
await invoke('clipboard_clear')
history.value = []
await refreshStatus()
} catch (e) {
logger.error('清空失败: ' + e)
}
}
const copyBack = async (id: number) => {
await invoke('clipboard_copy_back', { id })
// copy_back 会触发 suppress,不会产生 clipboard-changed 事件
}
const saveSettings = async (s: ClipboardSettings) => {
try {
await invoke('clipboard_save_settings', { settings: s })
settings.value = { ...s }
await refreshStatus()
} catch (e) {
logger.error('保存设置失败: ' + e)
throw e
}
}
const start = async () => {
await invoke('clipboard_start')
await refreshStatus()
}
const stop = async () => {
await invoke('clipboard_stop')
await refreshStatus()
}
// ===== 快捷弹窗 =====
const showPopup = async () => {
await invoke('clipboard_show_popup')
}
const hidePopup = async () => {
await invoke('clipboard_hide_popup')
}
/// 隐藏弹窗并模拟 Ctrl+V 粘贴到原窗口
const pasteToTarget = async () => {
await invoke('clipboard_paste_to_target')
}
const registerShortcut = async (shortcut: string) => {
await invoke('clipboard_register_shortcut', { shortcut })
}
const unregisterShortcut = async () => {
await invoke('clipboard_unregister_shortcut')
}
return {
history,
historyTotal,
pinned,
settings,
status,
loading,
init,
dispose,
fetchHistoryPage,
refreshHistory,
refreshPinned,
searchPage,
search,
getItem,
refreshStatus,
setPinned,
remove,
clear,
copyBack,
saveSettings,
start,
stop,
showPopup,
hidePopup,
pasteToTarget,
registerShortcut,
unregisterShortcut,
}
})