294 lines
7.2 KiB
TypeScript
294 lines
7.2 KiB
TypeScript
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,
|
||
}
|
||
})
|