113 lines
3.6 KiB
TypeScript
113 lines
3.6 KiB
TypeScript
/**
|
|
* app Provider:扫描开始菜单应用。
|
|
* 1 分钟缓存减少重复 IPC;图标按需加载(前端 Map 缓存,避免重复请求)。
|
|
*/
|
|
import { invoke } from '@tauri-apps/api/core'
|
|
import { bestScore } from '../engine'
|
|
import type { QPItem, QPProvider } from './types'
|
|
import { buildItemForms, makeAppLaunch, makeAppSubActions } from './utils'
|
|
|
|
interface AppRecord {
|
|
name: string
|
|
path: string
|
|
}
|
|
|
|
let appCache: AppRecord[] | null = null
|
|
let appCacheTime = 0
|
|
const APP_CACHE_TTL = 60_000 // 1 分钟缓存
|
|
|
|
async function loadApps(): Promise<AppRecord[]> {
|
|
if (appCache && Date.now() - appCacheTime < APP_CACHE_TTL) {
|
|
return appCache
|
|
}
|
|
try {
|
|
const apps = await invoke<AppRecord[]>('quickpanel_scan_apps')
|
|
appCache = apps
|
|
appCacheTime = Date.now()
|
|
return apps
|
|
} catch (e) {
|
|
console.error('[quickpanel] 扫描应用失败:', e)
|
|
return []
|
|
}
|
|
}
|
|
|
|
export class AppProvider implements QPProvider {
|
|
id = 'app'
|
|
label = '应用'
|
|
priority = 95
|
|
|
|
async search(query: string): Promise<QPItem[]> {
|
|
if (!query.trim()) {
|
|
// 空查询:不显示应用(避免列表过长),也跳过应用扫描 IPC(不阻塞默认视图首屏)
|
|
return []
|
|
}
|
|
const apps = await loadApps()
|
|
const results: Array<{ item: QPItem; score: number }> = []
|
|
for (const app of apps) {
|
|
const forms = buildItemForms(app.name)
|
|
const score = bestScore(query, forms)
|
|
if (score >= 0) {
|
|
results.push({
|
|
item: {
|
|
// 稳定 id(基于路径):扫描结果顺序变化时历史记录仍能恢复原应用
|
|
id: `app-${app.path}`,
|
|
title: app.name,
|
|
subtitle: app.path,
|
|
group: '应用',
|
|
iconPath: app.path,
|
|
action: makeAppLaunch(app.path),
|
|
subActions: makeAppSubActions(app.path),
|
|
appRank: 0, // 开始菜单:最可靠来源
|
|
},
|
|
score,
|
|
})
|
|
}
|
|
}
|
|
results.sort((a, b) => b.score - a.score)
|
|
return results.slice(0, 15).map(r => ({ ...r.item, score: r.score }))
|
|
}
|
|
}
|
|
|
|
// ===== 应用图标按需加载 =====
|
|
// 前端缓存(path -> dataUrl)。Rust 侧另有内存 + 磁盘缓存,此处仅避免重复 IPC。
|
|
|
|
const appIconCache = new Map<string, string>() // path -> dataUrl('' = 无图标)
|
|
|
|
/** 为搜索结果中带 iconPath 的项(应用、历史中的应用)按需加载图标(data URL),
|
|
* 并写入 item.iconUrl 触发响应式更新。
|
|
* 命中前端缓存时同步返回;否则异步调用 Rust 命令(命中 Rust 缓存则零开销)。 */
|
|
export async function loadAppIconsForResults(items: QPItem[]): Promise<void> {
|
|
const toLoad: QPItem[] = []
|
|
for (const item of items) {
|
|
if (!item.iconPath) continue
|
|
if (item.iconUrl !== undefined) continue // 已设置(含加载中)
|
|
const cached = appIconCache.get(item.iconPath)
|
|
if (cached !== undefined) {
|
|
item.iconUrl = cached
|
|
} else {
|
|
item.iconUrl = '' // 标记加载中,避免重复请求
|
|
toLoad.push(item)
|
|
}
|
|
}
|
|
if (!toLoad.length) return
|
|
await Promise.all(
|
|
toLoad.map(async item => {
|
|
const path = item.iconPath!
|
|
try {
|
|
const url = await invoke<string | null>('quickpanel_get_app_icon', { path })
|
|
const u = url ?? ''
|
|
appIconCache.set(path, u)
|
|
item.iconUrl = u
|
|
} catch {
|
|
appIconCache.set(path, '')
|
|
item.iconUrl = ''
|
|
}
|
|
}),
|
|
)
|
|
}
|
|
|
|
/** 清空前端图标缓存(Rust 端清理命令 quickpanel_clear_app_icon_cache 调用后可一并清空) */
|
|
export function invalidateAppIconCache() {
|
|
appIconCache.clear()
|
|
}
|