From 3d8cdc6371d4f22947d43b907cf2c5670bb78ff3 Mon Sep 17 00:00:00 2001 From: XiaoSong-dada <1935571817@qq.com> Date: Fri, 27 Feb 2026 23:20:28 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(app-search):=20=E5=AE=8C=E5=96=84=20Win?= =?UTF-8?q?dows=20=E5=BA=94=E7=94=A8=E7=B4=A2=E5=BC=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/app-search/win.ts | 267 +++++++++++++++++++++++++++---------- 1 file changed, 194 insertions(+), 73 deletions(-) diff --git a/src/core/app-search/win.ts b/src/core/app-search/win.ts index 7a8442c8..c2448529 100644 --- a/src/core/app-search/win.ts +++ b/src/core/app-search/win.ts @@ -14,7 +14,25 @@ const startMenu = path.join( 'Microsoft\\Windows\\Start Menu\\Programs' ); -const fileLists: any = []; +interface AppInfo { + value: string; + desc: string; + type: string; + icon: string; + pluginType: string; + action: string; + keyWords: string[]; + name: string; + names: string[]; + match?: number[]; +} + +interface StartAppEntry { + Name?: string; + AppID?: string; +} + +const fileLists: AppInfo[] = []; const isZhRegex = /[\u4e00-\u9fa5]/; const icondir = path.join(os.tmpdir(), 'ProcessIcon'); @@ -23,7 +41,7 @@ if (!exists) { fs.mkdirSync(icondir); } -const getico = (app) => { +const getico = (app: AppInfo): void => { try { // eslint-disable-next-line @typescript-eslint/no-var-requires const fileIcon = require('extract-file-icon'); @@ -42,85 +60,188 @@ const getico = (app) => { } }; -function fileDisplay(filePath) { - //根据文件路径读取文件,返回文件列表 - fs.readdir(filePath, function (err, files) { - if (err) { - console.warn(err); - } else { - files.forEach(function (filename) { - const filedir = path.join(filePath, filename); - fs.stat(filedir, function (eror, stats) { - if (eror) { - console.warn('获取文件stats失败'); - } else { - const isFile = stats.isFile(); // 是文件 - const isDir = stats.isDirectory(); // 是文件夹 - if (isFile) { - const appName = filename.split('.')[0]; - const keyWords = [appName]; - let appDetail: any = {}; - try { - appDetail = shell.readShortcutLink(filedir); - } catch (e) { - // - } - if ( - !appDetail.target || - appDetail.target.toLowerCase().indexOf('unin') >= 0 - ) - return; +function buildAppInfo(appName: string, targetPath: string): AppInfo { + const keyWords = [appName]; + keyWords.push(path.basename(targetPath, '.exe')); + + if (isZhRegex.test(appName)) { + // const [, pinyinArr] = translate(appName); + // const zh_firstLatter = pinyinArr.map((py) => py[0]); + // // 拼音 + // keyWords.push(pinyinArr.join('')); + // 缩写 + // keyWords.push(zh_firstLatter.join('')); + } else { + const firstLatter = appName + .split(' ') + .map((name) => name[0]) + .join(''); + keyWords.push(firstLatter); + } + + const icon = path.join( + os.tmpdir(), + 'ProcessIcon', + `${encodeURIComponent(appName)}.png` + ); + + const appInfo = { + value: 'plugin', + desc: targetPath, + type: 'app', + icon, + pluginType: 'app', + action: `start "dummyclient" "${targetPath}"`, + keyWords, + name: appName, + names: JSON.parse(JSON.stringify(keyWords)), + }; + + getico(appInfo); + return appInfo; +} + +function buildStartAppInfo(appName: string, appId: string): AppInfo { + const keyWords = [appName]; + + if (!isZhRegex.test(appName)) { + const firstLatter = appName + .split(' ') + .map((name) => name[0]) + .join(''); + keyWords.push(firstLatter); + } + + appId + .split(/[\\/.!_-]+/) + .filter(Boolean) + .forEach((token) => keyWords.push(token)); + + const icon = path.join( + os.tmpdir(), + 'ProcessIcon', + `${encodeURIComponent(appName)}.png` + ); + + return { + value: 'plugin', + desc: appId, + type: 'app', + icon, + pluginType: 'app', + action: `start "" "shell:AppsFolder\\${appId}"`, + keyWords, + name: appName, + names: JSON.parse(JSON.stringify(keyWords)), + }; +} + +function scanStartMenuDir(dirPath: string): Promise { + return new Promise((resolve) => { + const results: AppInfo[] = []; - // C:/program/cmd.exe => cmd - keyWords.push(path.basename(appDetail.target, '.exe')); - - if (isZhRegex.test(appName)) { - // const [, pinyinArr] = translate(appName); - // const zh_firstLatter = pinyinArr.map((py) => py[0]); - // // 拼音 - // keyWords.push(pinyinArr.join('')); - // 缩写 - // keyWords.push(zh_firstLatter.join('')); - } else { - const firstLatter = appName - .split(' ') - .map((name) => name[0]) - .join(''); - keyWords.push(firstLatter); + const walk = (currentPath: string, done: () => void): void => { + fs.readdir(currentPath, (err, files) => { + if (err) { + done(); + return; + } + + let pending = files.length; + if (!pending) { + done(); + return; + } + + files.forEach((filename) => { + const filedir = path.join(currentPath, filename); + fs.stat(filedir, (statErr, stats) => { + if (!statErr) { + if (stats.isDirectory()) { + walk(filedir, () => { + if (--pending === 0) done(); + }); + return; } - const icon = path.join( - os.tmpdir(), - 'ProcessIcon', - `${encodeURIComponent(appName)}.png` - ); - - const appInfo = { - value: 'plugin', - desc: appDetail.target, - type: 'app', - icon, - pluginType: 'app', - action: `start "dummyclient" "${appDetail.target}"`, - keyWords: keyWords, - name: appName, - names: JSON.parse(JSON.stringify(keyWords)), - }; - fileLists.push(appInfo); - getico(appInfo); - } - if (isDir) { - fileDisplay(filedir); // 递归,如果是文件夹,就继续遍历该文件夹下面的文件 + if (stats.isFile() && filedir.toLowerCase().endsWith('.lnk')) { + let appDetail: { target?: string } = {}; + try { + appDetail = shell.readShortcutLink(filedir); + } catch (e) { + // + } + + if ( + appDetail.target && + appDetail.target.toLowerCase().indexOf('unin') < 0 + ) { + const appName = filename.split('.')[0]; + results.push(buildAppInfo(appName, appDetail.target)); + } + } + } else { + console.warn('获取文件stats失败'); } - } + + if (--pending === 0) done(); + }); }); }); - } + }; + + walk(dirPath, () => resolve(results)); }); } -export default () => { - fileDisplay(filePath); - fileDisplay(startMenu); +function getStartApps(): Promise { + return new Promise((resolve) => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { exec } = require('child_process'); + const ps = 'Get-StartApps | ConvertTo-Json -Compress'; + exec( + `powershell -NoProfile -NonInteractive -Command "${ps}"`, + { timeout: 8000 }, + (err, stdout) => { + if (err || !stdout.trim()) { + resolve([]); + return; + } + + try { + const raw = JSON.parse(stdout.trim()) as StartAppEntry | StartAppEntry[]; + const apps: StartAppEntry[] = Array.isArray(raw) ? raw : [raw]; + const results = apps + .filter((item) => item && item.Name && item.AppID) + .map((item) => + buildStartAppInfo(item.Name as string, item.AppID as string) + ); + resolve(results); + } catch (e) { + resolve([]); + } + } + ); + }); +} + +export default async (): Promise => { + const [systemApps, userApps, startApps] = await Promise.all([ + scanStartMenuDir(filePath), + scanStartMenuDir(startMenu), + getStartApps(), + ]); + + const merged = [...startApps, ...systemApps, ...userApps]; + const seen = new Set(); + fileLists.length = 0; + merged.forEach((item) => { + const key = `${item.name || ''}|${item.desc || ''}`.toLowerCase(); + if (!seen.has(key)) { + seen.add(key); + fileLists.push(item); + } + }); + return fileLists; }; From 008a5b3099e99ed9b5751f0e6c2ecec5695b473e Mon Sep 17 00:00:00 2001 From: XiaoSong-dada <1935571817@qq.com> Date: Sat, 28 Feb 2026 20:44:11 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(win-app-search):=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E5=B0=8F=E5=9B=BE=E6=A0=87=E7=BC=93=E5=AD=98=E3=80=81=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E7=BC=BA=E5=9B=BE=E8=87=AA=E6=84=88=E5=92=8CStartApps?= =?UTF-8?q?=E5=85=9C=E5=BA=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/app-search/win.ts | 143 +++++++++++++++++++----- src/main/common/api.ts | 19 ++++ src/renderer/plugins-manager/options.ts | 61 ++++++++++ 3 files changed, 192 insertions(+), 31 deletions(-) diff --git a/src/core/app-search/win.ts b/src/core/app-search/win.ts index c2448529..a8f97455 100644 --- a/src/core/app-search/win.ts +++ b/src/core/app-search/win.ts @@ -19,6 +19,7 @@ interface AppInfo { desc: string; type: string; icon: string; + iconSource?: string; pluginType: string; action: string; keyWords: string[]; @@ -41,20 +42,28 @@ if (!exists) { fs.mkdirSync(icondir); } +const normalizeAppName = (name: string) => { + const base = String(name || '') + .normalize('NFC') + .replace(/[<>:"/\\|?*]/g, '_') + .trim(); + return Array.from(base) + .map((char) => (char.charCodeAt(0) < 32 ? '_' : char)) + .join(''); +}; + +const getIconPathByName = (name: string) => + path.join(icondir, `${normalizeAppName(name)}.png`); + const getico = (app: AppInfo): void => { try { // eslint-disable-next-line @typescript-eslint/no-var-requires const fileIcon = require('extract-file-icon'); const buffer = fileIcon(app.desc, 32); - const iconpath = path.join(icondir, `${app.name}.png`); - - fs.exists(iconpath, (exists) => { - if (!exists) { - fs.writeFile(iconpath, buffer, 'base64', () => { - // - }); - } - }); + const iconpath = getIconPathByName(app.name); + if (!fs.existsSync(iconpath)) { + fs.writeFileSync(iconpath, buffer); + } } catch (e) { console.log(e, app.desc); } @@ -79,11 +88,7 @@ function buildAppInfo(appName: string, targetPath: string): AppInfo { keyWords.push(firstLatter); } - const icon = path.join( - os.tmpdir(), - 'ProcessIcon', - `${encodeURIComponent(appName)}.png` - ); + const icon = getIconPathByName(appName); const appInfo = { value: 'plugin', @@ -101,7 +106,11 @@ function buildAppInfo(appName: string, targetPath: string): AppInfo { return appInfo; } -function buildStartAppInfo(appName: string, appId: string): AppInfo { +function buildStartAppInfo( + appName: string, + appId: string, + iconSource?: string +): AppInfo { const keyWords = [appName]; if (!isZhRegex.test(appName)) { @@ -117,17 +126,14 @@ function buildStartAppInfo(appName: string, appId: string): AppInfo { .filter(Boolean) .forEach((token) => keyWords.push(token)); - const icon = path.join( - os.tmpdir(), - 'ProcessIcon', - `${encodeURIComponent(appName)}.png` - ); + const icon = getIconPathByName(appName); return { value: 'plugin', desc: appId, type: 'app', icon, + iconSource, pluginType: 'app', action: `start "" "shell:AppsFolder\\${appId}"`, keyWords, @@ -194,11 +200,43 @@ function scanStartMenuDir(dirPath: string): Promise { }); } -function getStartApps(): Promise { +function collectShortcutMap(dirPath: string, target: Map) { + let files: string[] = []; + try { + files = fs.readdirSync(dirPath); + } catch { + return; + } + + files.forEach((filename) => { + const filedir = path.join(dirPath, filename); + let stats: fs.Stats | null = null; + try { + stats = fs.statSync(filedir); + } catch { + return; + } + + if (stats.isDirectory()) { + collectShortcutMap(filedir, target); + return; + } + + if (stats.isFile() && filedir.toLowerCase().endsWith('.lnk')) { + const appName = filename.slice(0, -4).toLowerCase().trim(); + if (appName && !target.has(appName)) { + target.set(appName, filedir); + } + } + }); +} + +function getStartApps(shortcutMap: Map): Promise { return new Promise((resolve) => { // eslint-disable-next-line @typescript-eslint/no-var-requires const { exec } = require('child_process'); - const ps = 'Get-StartApps | ConvertTo-Json -Compress'; + const ps = + '[Console]::OutputEncoding=[System.Text.Encoding]::UTF8; Get-StartApps | ConvertTo-Json -Compress'; exec( `powershell -NoProfile -NonInteractive -Command "${ps}"`, { timeout: 8000 }, @@ -209,13 +247,30 @@ function getStartApps(): Promise { } try { - const raw = JSON.parse(stdout.trim()) as StartAppEntry | StartAppEntry[]; + const raw = JSON.parse(stdout.trim()) as + | StartAppEntry + | StartAppEntry[]; const apps: StartAppEntry[] = Array.isArray(raw) ? raw : [raw]; const results = apps .filter((item) => item && item.Name && item.AppID) - .map((item) => - buildStartAppInfo(item.Name as string, item.AppID as string) - ); + .map((item) => { + const appName = item.Name as string; + const shortcutPath = shortcutMap.get( + appName.toLowerCase().trim() + ); + const appInfo = buildStartAppInfo( + appName, + item.AppID as string, + shortcutPath + ); + if (shortcutPath) { + getico({ + ...appInfo, + desc: shortcutPath, + }); + } + return appInfo; + }); resolve(results); } catch (e) { resolve([]); @@ -226,21 +281,47 @@ function getStartApps(): Promise { } export default async (): Promise => { + const shortcutMap = new Map(); + collectShortcutMap(filePath, shortcutMap); + collectShortcutMap(startMenu, shortcutMap); + const [systemApps, userApps, startApps] = await Promise.all([ scanStartMenuDir(filePath), scanStartMenuDir(startMenu), - getStartApps(), + getStartApps(shortcutMap), ]); + const hasReadyIcon = (item: AppInfo) => { + if (!item.icon) return false; + try { + return fs.existsSync(item.icon); + } catch { + return false; + } + }; + const merged = [...startApps, ...systemApps, ...userApps]; - const seen = new Set(); + const indexedByName = new Map(); fileLists.length = 0; + merged.forEach((item) => { - const key = `${item.name || ''}|${item.desc || ''}`.toLowerCase(); - if (!seen.has(key)) { - seen.add(key); + const nameKey = `${item.name || ''}`.toLowerCase().trim(); + if (!nameKey) { fileLists.push(item); + return; } + + const existedIndex = indexedByName.get(nameKey); + if (typeof existedIndex === 'number') { + const existed = fileLists[existedIndex]; + if (!hasReadyIcon(existed) && hasReadyIcon(item)) { + fileLists[existedIndex] = item; + } + return; + } + + indexedByName.set(nameKey, fileLists.length); + fileLists.push(item); }); return fileLists; diff --git a/src/main/common/api.ts b/src/main/common/api.ts index d455d487..5c9167e3 100644 --- a/src/main/common/api.ts +++ b/src/main/common/api.ts @@ -394,6 +394,25 @@ class API extends DBInstance { return nativeImage.toDataURL(); } + public rebuildAppIcon({ data }) { + const iconPath = typeof data?.iconPath === 'string' ? data.iconPath : ''; + const sourcePath = typeof data?.sourcePath === 'string' ? data.sourcePath : ''; + if (!iconPath || !sourcePath) return false; + + try { + if (fs.existsSync(iconPath)) return true; + // eslint-disable-next-line @typescript-eslint/no-var-requires + const fileIcon = require('extract-file-icon'); + const buffer = fileIcon(sourcePath, 32); + if (!buffer || !buffer.length) return false; + fs.mkdirSync(path.dirname(iconPath), { recursive: true }); + fs.writeFileSync(iconPath, buffer); + return true; + } catch (e) { + return false; + } + } + public shellBeep() { shell.beep(); return true; diff --git a/src/renderer/plugins-manager/options.ts b/src/renderer/plugins-manager/options.ts index edf23f49..0e8e7d0b 100644 --- a/src/renderer/plugins-manager/options.ts +++ b/src/renderer/plugins-manager/options.ts @@ -3,6 +3,7 @@ import debounce from 'lodash.debounce'; import { ipcRenderer } from 'electron'; import { getGlobal } from '@electron/remote'; import PinyinMatch from 'pinyin-match'; +import commonConst from '@/common/utils/commonConst'; import pluginClickEvent from './pluginClickEvent'; import useFocus from './clipboardWatch'; @@ -34,6 +35,8 @@ const optionsManager = ({ currentPlugin, }) => { const optionsRef = ref([]); + const fs = window.require('fs'); + const iconRetryFailed = new Set(); // 全局快捷键 ipcRenderer.on('global-short-key', (e, msg) => { @@ -98,6 +101,63 @@ const optionsManager = ({ // todo 再搜索 app const appPlugins = appList.value || []; const descMap = new Map(); + + const ensureAppIcon = (plugin) => { + if (!commonConst.windows()) return plugin.icon; + if (plugin.pluginType !== 'app') return plugin.icon; + if (!plugin.icon || typeof plugin.icon !== 'string') return plugin.icon; + if (plugin.icon.startsWith('data:image')) return plugin.icon; + try { + if (fs.existsSync(plugin.icon)) return plugin.icon; + } catch { + return plugin.icon; + } + + if (iconRetryFailed.has(plugin.icon)) return plugin.icon; + + const sourcePath = + typeof plugin.iconSource === 'string' && plugin.iconSource + ? plugin.iconSource + : plugin.desc; + if (typeof sourcePath !== 'string' || !sourcePath) { + iconRetryFailed.add(plugin.icon); + return plugin.icon; + } + + try { + const rebuilt = ipcRenderer.sendSync('msg-trigger', { + type: 'rebuildAppIcon', + data: { + iconPath: plugin.icon, + sourcePath, + }, + }); + if (rebuilt && fs.existsSync(plugin.icon)) { + return plugin.icon; + } + } catch { + // ignore and continue fallback + } + + try { + if (fs.existsSync(sourcePath)) { + const dataUrl = ipcRenderer.sendSync('msg-trigger', { + type: 'getFileIcon', + data: { path: sourcePath }, + }); + if (typeof dataUrl === 'string' && dataUrl.startsWith('data:image')) { + plugin.icon = dataUrl; + return plugin.icon; + } + } + } catch { + // ignore + } + + iconRetryFailed.add(plugin.icon); + return plugin.icon; + }; + options = [ ...options, ...appPlugins @@ -126,6 +186,7 @@ const optionsManager = ({ } }) .map((plugin) => { + plugin.icon = ensureAppIcon(plugin); const option = { ...plugin, zIndex: 0,