托盘
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
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,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user