From 21b80c566b9cd888157078a9af11ca57cabc2178 Mon Sep 17 00:00:00 2001 From: lqtmcstudio Date: Thu, 5 Feb 2026 23:44:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20-=20=E6=90=9C=E7=B4=A2=E9=A1=B5UI=20-?= =?UTF-8?q?=20=E6=8F=92=E4=BB=B6=E7=B3=BB=E7=BB=9F=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=90=9C=E7=B4=A2=E5=8D=95=E6=9B=B2=E6=8E=A5=E5=8F=A3=20-=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=9F=B3=E9=87=8F=E5=88=9D=E5=A7=8B=E5=8C=96?= =?UTF-8?q?=E5=92=8C=E7=8A=B6=E6=80=81=20-=20=E6=90=9C=E7=B4=A2=E5=85=B3?= =?UTF-8?q?=E9=94=AE=E8=AF=8D=E5=8C=B9=E9=85=8D=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 - dist-electron/main.js | 34 ++- dist-electron/preload.mjs | 3 +- electron/main.ts | 2 + electron/preload.ts | 3 +- electron/qzpController.ts | 1 + package-lock.json | 10 + package.json | 1 + src/main/pluginSystem.ts | 43 ++- src/renderer/components/TopBar.vue | 11 + src/renderer/main.ts | 7 +- src/renderer/stores/player.ts | 2 +- src/renderer/types/electron.d.ts | 1 + src/renderer/utils/songUtils.ts | 25 ++ src/renderer/views/Search.vue | 472 +++++++++++++++++++++++++++++ vite.config.ts | 2 + 16 files changed, 610 insertions(+), 8 deletions(-) create mode 100644 src/renderer/utils/songUtils.ts create mode 100644 src/renderer/views/Search.vue diff --git a/.gitignore b/.gitignore index fa87b47..e8ceb4d 100644 --- a/.gitignore +++ b/.gitignore @@ -22,5 +22,4 @@ dist-ssr *.njsproj *.sln *.sw? -core/mpv.exe .gitignore diff --git a/dist-electron/main.js b/dist-electron/main.js index a0ac640..bbdfa91 100644 --- a/dist-electron/main.js +++ b/dist-electron/main.js @@ -66,6 +66,7 @@ class QzpController extends EventEmitter { this.send(["observe_property", 3, "duration"]); this.send(["observe_property", 4, "idle-active"]); this.send(["observe_property", 5, "eof-reached"]); + this.send(["set_property", "volume", 50]); }); this.socket.on("data", (data) => { this.handleData(data); @@ -148,6 +149,27 @@ class PluginSystem { this.pluginId = pluginId; this.loadPlugin(); } + async search(query, page, limit) { + var _a, _b; + if (!((_b = (_a = this.plugin) == null ? void 0 : _a.musicSearch) == null ? void 0 : _b.search)) { + return { + list: [], + total: 0, + allPage: 0, + error: "Search not implemented" + }; + } + try { + return await this.plugin.musicSearch.search(query, page, limit); + } catch (e) { + return { + list: [], + total: 0, + allPage: 0, + error: e.message || "Plugin search error" + }; + } + } loadPlugin() { try { const pluginPath = path.join( @@ -175,7 +197,17 @@ class PluginSystem { }; } try { - return await this.plugin.getUrl(id, quality); + const url = await this.plugin.getUrl(id, quality); + if (typeof url !== "string" || !url.startsWith("http")) { + return { + success: false, + error: "Invalid URL scheme" + }; + } + return { + success: true, + url + }; } catch (e) { return { success: false, diff --git a/dist-electron/preload.mjs b/dist-electron/preload.mjs index 0318ff8..b548e37 100644 --- a/dist-electron/preload.mjs +++ b/dist-electron/preload.mjs @@ -19,7 +19,8 @@ electron.contextBridge.exposeInMainWorld("electronAPI", { }, // Plugin System plugin: { - call: (pluginId, method, args) => electron.ipcRenderer.invoke("plugin:call", pluginId, method, args) + call: (pluginId, method, args) => electron.ipcRenderer.invoke("plugin:call", pluginId, method, args), + search: (pluginId, query, page, limit) => electron.ipcRenderer.invoke("plugin:call", pluginId, "search", [query, page, limit]) }, // Cache Control getCacheInfo: () => electron.ipcRenderer.invoke("cache:getInfo"), diff --git a/electron/main.ts b/electron/main.ts index dcbdd81..d6a4c93 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -92,6 +92,8 @@ ipcMain.handle( } ) + + // Cache IPC Handlers ipcMain.handle('cache:getInfo', () => { const settings = loadSettings(); diff --git a/electron/preload.ts b/electron/preload.ts index 6290b06..20eb66d 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -21,7 +21,8 @@ contextBridge.exposeInMainWorld('electronAPI', { // Plugin System plugin: { - call: (pluginId: string, method: string, args: any[]) => ipcRenderer.invoke('plugin:call', pluginId, method, args) + call: (pluginId: string, method: string, args: any[]) => ipcRenderer.invoke('plugin:call', pluginId, method, args), + search: (pluginId: string, query: string, page: number, limit: number) => ipcRenderer.invoke('plugin:call', pluginId, 'search', [query, page, limit]), }, // Cache Control diff --git a/electron/qzpController.ts b/electron/qzpController.ts index 777220c..9ef4578 100644 --- a/electron/qzpController.ts +++ b/electron/qzpController.ts @@ -69,6 +69,7 @@ export class QzpController extends EventEmitter { this.send(['observe_property', 3, 'duration']); this.send(['observe_property', 4, 'idle-active']); this.send(['observe_property', 5, 'eof-reached']); + this.send(['set_property', 'volume', 50]) }); this.socket.on('data', (data) => { diff --git a/package-lock.json b/package-lock.json index 147e488..4c8e093 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "tdesign-vue-next": "^1.17.7", "url": "^0.11.4", "vite-plugin-electron-renderer": "^0.14.6", + "vite-plugin-wasm": "^3.5.0", "vue": "^3.4.21", "vue-router": "^4.6.4" }, @@ -8992,6 +8993,15 @@ "vite": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/vite-plugin-wasm": { + "version": "3.5.0", + "resolved": "https://registry.npmmirror.com/vite-plugin-wasm/-/vite-plugin-wasm-3.5.0.tgz", + "integrity": "sha512-X5VWgCnqiQEGb+omhlBVsvTfxikKtoOgAzQ95+BZ8gQ+VfMHIjSHr0wyvXFQCa0eKQ0fKyaL0kWcEnYqBac4lQ==", + "license": "MIT", + "peerDependencies": { + "vite": "^2 || ^3 || ^4 || ^5 || ^6 || ^7" + } + }, "node_modules/vm-browserify": { "version": "1.1.2", "resolved": "https://registry.npmmirror.com/vm-browserify/-/vm-browserify-1.1.2.tgz", diff --git a/package.json b/package.json index 9d2c690..73c55e9 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "tdesign-vue-next": "^1.17.7", "url": "^0.11.4", "vite-plugin-electron-renderer": "^0.14.6", + "vite-plugin-wasm": "^3.5.0", "vue": "^3.4.21", "vue-router": "^4.6.4" }, diff --git a/src/main/pluginSystem.ts b/src/main/pluginSystem.ts index a6c2776..47684ba 100644 --- a/src/main/pluginSystem.ts +++ b/src/main/pluginSystem.ts @@ -12,7 +12,10 @@ export interface UrlResponse { } type PluginModule = { - getUrl?: (id: string, quality: string) => Promise | UrlResponse + getUrl?: (id: string, quality: string) => Promise | string, + musicSearch?: { + search: (query: string, page: number, limit: number) => Promise | any + } } export class PluginSystem { @@ -22,6 +25,28 @@ export class PluginSystem { this.pluginId = pluginId this.loadPlugin() } + + async search(query: string, page: number, limit: number): Promise { + if (!this.plugin?.musicSearch?.search) { + return { + list: [], + total: 0, + allPage: 0, + error: 'Search not implemented' + } + } + try { + return await this.plugin.musicSearch.search(query, page, limit) + } catch (e: any) { + return { + list: [], + total: 0, + allPage: 0, + error: e.message || 'Plugin search error' + } + } + } + private loadPlugin() { try { const pluginPath = path.join( @@ -53,7 +78,21 @@ export class PluginSystem { } try { - return await this.plugin.getUrl(id, quality) + // New behavior: plugin returns raw url string or throws + const url = await this.plugin.getUrl(id, quality) + + if (typeof url !== 'string' || !url.startsWith('http')) { + return { + success: false, + error: 'Invalid URL scheme' + } + } + + return { + success: true, + url + } + } catch (e: any) { return { success: false, diff --git a/src/renderer/components/TopBar.vue b/src/renderer/components/TopBar.vue index 88008c6..922cba2 100644 --- a/src/renderer/components/TopBar.vue +++ b/src/renderer/components/TopBar.vue @@ -17,6 +17,8 @@ type="text" placeholder="搜索音乐、歌手、专辑..." class="search-input" + v-model="searchQuery" + @keydown.enter="handleSearch" /> @@ -55,10 +57,19 @@ import { Icon } from '@iconify/vue'; const router = useRouter(); const isMaximized = ref(false); +const searchQuery = ref(''); const goBack = () => router.back(); const goForward = () => router.forward(); +const handleSearch = () => { + if (!searchQuery.value.trim()) return; + router.push({ + name: 'Search', + query: { q: searchQuery.value } + }); +}; + // Settings const openSettings = inject<() => void>('openSettings', () => {}); diff --git a/src/renderer/main.ts b/src/renderer/main.ts index a8f293e..146458b 100644 --- a/src/renderer/main.ts +++ b/src/renderer/main.ts @@ -30,6 +30,11 @@ const router = createRouter({ path: '/recent', name: 'Recent', component: () => import('./views/Playlist.vue') + }, + { + path: '/search', + name: 'Search', + component: () => import('./views/Search.vue') } ] }) @@ -72,5 +77,5 @@ const testSong2: Song = { // Auto Play playerStore.playSong(testSong).then(() => { - playerStore.setPlaylist([testSong,testSong2]); + playerStore.setPlaylist([testSong, testSong2]); }); \ No newline at end of file diff --git a/src/renderer/stores/player.ts b/src/renderer/stores/player.ts index a8af89c..cd8ce18 100644 --- a/src/renderer/stores/player.ts +++ b/src/renderer/stores/player.ts @@ -24,7 +24,7 @@ export const usePlayerStore = defineStore('player', () => { // State const isPlaying = ref(false); const currentSong = ref(null); - const volume = ref(100); + const volume = ref(50); const duration = ref(0); const currentTime = ref(0); diff --git a/src/renderer/types/electron.d.ts b/src/renderer/types/electron.d.ts index 3101dd0..382e397 100644 --- a/src/renderer/types/electron.d.ts +++ b/src/renderer/types/electron.d.ts @@ -15,6 +15,7 @@ export interface IElectronAPI { }; plugin: { call: (pluginId: string, method: string, args: any[]) => Promise; + search: (pluginId: string, query: string, page: number, limit: number) => Promise; }; // Cache Control getCacheInfo: () => Promise<{ path: string; size: string; persistCache: boolean }>; diff --git a/src/renderer/utils/songUtils.ts b/src/renderer/utils/songUtils.ts new file mode 100644 index 0000000..d6f777c --- /dev/null +++ b/src/renderer/utils/songUtils.ts @@ -0,0 +1,25 @@ +import type { Song } from '../types/song'; + +export function formatDuration(ms: number): string { + const totalSeconds = Math.floor(ms / 1000); + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; +} + +export function transformSearchSong(raw: any): Song { + return { + id: String(raw.songmid), + name: raw.name, + artist: raw.singer, + picUrl: raw.img || raw.m_img || raw.s_img, + url: '', // Empty initially + duration: formatDuration(raw.interval ? Number(raw.interval) : 0), + source: raw.source, + albumId: raw.albumId ? String(raw.albumId) : null, + albumName: raw.albumName, + type: 'Remote', + quality: 'auto', + types: raw.types // Store raw types for quality selection later + }; +} diff --git a/src/renderer/views/Search.vue b/src/renderer/views/Search.vue new file mode 100644 index 0000000..4ec9e38 --- /dev/null +++ b/src/renderer/views/Search.vue @@ -0,0 +1,472 @@ + + + + + + diff --git a/vite.config.ts b/vite.config.ts index 5c7999f..ed59be6 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,6 +3,7 @@ import vueJsx from "@vitejs/plugin-vue-jsx"; import path from 'node:path' import electron from 'vite-plugin-electron/simple' import vue from '@vitejs/plugin-vue' +import wasm from "vite-plugin-wasm"; // https://vitejs.dev/config/ export default defineConfig({ @@ -14,6 +15,7 @@ export default defineConfig({ plugins: [ vue(), vueJsx(), + wasm(), electron({ main: { // Shortcut of `build.lib.entry`.