diff --git a/.env.debug b/.env.debug index 0742a13a..637f41e3 100644 --- a/.env.debug +++ b/.env.debug @@ -1,3 +1,3 @@ VITE_APM_STORE_LOCAL_MODE=true -VITE_APM_STORE_BASE_URL=/local_amd64-apm +VITE_APM_STORE_BASE_URL=/local_amd64-store VITE_APM_STORE_STATS_BASE_URL=/local_stats diff --git a/.vscode/launch.json b/.vscode/launch.json index a02acbfe..99537bc7 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,6 +20,7 @@ } ], "configurations": [ + { "name": "Debug Main Process", "type": "node", diff --git a/AGENTS.md b/AGENTS.md index 94079ddd..713d8de0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -90,7 +90,7 @@ event.sender.send('install-complete', { id, success, ... }); **Exposed APIs in Preload:** - `window.ipcRenderer.on/off/send/invoke` - IPC communication -- `window.apm_store.arch` - System architecture detection (amd64-apm, arm64-apm) +- `window.apm_store.arch` - System architecture detection (amd64-store, arm64-store) ### 3. Installation Queue System @@ -345,10 +345,10 @@ const response = await axiosInstance.get( ```typescript server: { proxy: { - '/local_amd64-apm': { + '/local_amd64-store': { target: 'https://erotica.spark-app.store', changeOrigin: true, - rewrite: (path) => path.replace(/^\/local_amd64-apm/, ''), + rewrite: (path) => path.replace(/^\/local_amd64-store/, ''), } } } @@ -356,36 +356,54 @@ server: { --- -## 🎯 Deep Link Protocol +## 🎯 Deep Link Protocol (SPK URI Scheme) -**URL Scheme:** `apmstore://` +**URL Scheme:** `spk://` -### Supported Actions +### Supported SPK URI Format -1. **Install App:** `apmstore://install?pkg=` -2. **Show Updates:** `apmstore://update` -3. **Show Installed:** `apmstore://installed` +Format: `spk://search/{pkgname}` + +**Examples:** +- `spk://search/code` - Search for and open "code" app +- `spk://search/steam` - Search for and open "steam" app +- `spk://search/store.spark-app.hmcl` - Search for and open "HMCL" game ### Implementation Pattern ```typescript -// electron/main/deeplink.ts - Parse command line -export function handleCommandLine(argv: string[]) { - const deeplinkUrl = argv.find((arg) => arg.startsWith('apmstore://')); +// electron/main/deeplink.ts - Parse command line and route +export function handleCommandLine(commandLine: string[]) { + const deeplinkUrl = commandLine.find((arg) => + arg.startsWith('spk://') + ); if (!deeplinkUrl) return; - - const url = new URL(deeplinkUrl); - if (url.hostname === 'install') { - const pkg = url.searchParams.get('pkg'); - sendToRenderer('deep-link-install', pkg); + + try { + const url = new URL(deeplinkUrl); + const action = url.hostname; // 'search' + + if (action === 'search') { + // Format: spk://search/pkgname + // url.pathname will be '/pkgname' + const pkgname = url.pathname.split('/').filter(Boolean)[0]; + if (pkgname) { + listeners.emit('search', { pkgname }); + } + } + } catch (error) { + logger.error({ err: error }, 'Error parsing SPK URI'); } } // src/App.vue - Handle in renderer -window.ipcRenderer.on('deep-link-install', (_event, pkgname: string) => { - const target = apps.value.find((a) => a.pkgname === pkgname); - if (target) openDetail(target); -}); +window.ipcRenderer.on( + 'deep-link-search', + (_event: IpcRendererEvent, data: { pkgname: string }) => { + // Trigger search with the pkgname + searchQuery.value = data.pkgname; + } +); ``` --- diff --git a/electron/main/deeplink.ts b/electron/main/deeplink.ts index e6afdcbc..fc20cb2c 100644 --- a/electron/main/deeplink.ts +++ b/electron/main/deeplink.ts @@ -45,7 +45,7 @@ class ListenersMap { } } -const protocols = ["apmstore"]; +const protocols = ["spk"]; const listeners = new ListenersMap(); export const deepLink = { @@ -81,17 +81,27 @@ export function handleCommandLine(commandLine: string[]) { try { const url = new URL(target); - const action = url.hostname; + const action = url.hostname; // 'search' logger.info(`Deep link: action found: ${action}`); const query: Query = {}; - url.searchParams.forEach((value, key) => { - query[key] = value; - }); - logger.info(`Deep link: query found: ${JSON.stringify(query)}`); - const emitCount = listeners.emit(action, query); - logger.info(`Deep link: emitted for ${emitCount} listeners`); + if (action === "search") { + // Format: spk://search/pkgname + // url.pathname will be '/pkgname' + const pkgname = url.pathname.split("/").filter(Boolean)[0]; + if (pkgname) { + query.pkgname = pkgname; + logger.info(`Deep link: search query found: ${JSON.stringify(query)}`); + listeners.emit(action, query); + } else { + logger.warn( + `Deep link: invalid search format, expected /pkgname, got ${url.pathname}`, + ); + } + } else { + logger.warn(`Deep link: unknown action ${action}`); + } } catch (error) { logger.error(`Deep link: error parsing URL: ${error}`); } diff --git a/electron/main/handle-url-scheme.ts b/electron/main/handle-url-scheme.ts index 4a3f1e42..173ab927 100644 --- a/electron/main/handle-url-scheme.ts +++ b/electron/main/handle-url-scheme.ts @@ -80,3 +80,28 @@ deepLink.on("install", (query) => { pendingActions.push(action); } }); + +deepLink.on("search", (query) => { + logger.info( + `Deep link: event "search" fired with query: ${JSON.stringify(query)}`, + ); + + const action = () => { + const win = BrowserWindow.getAllWindows()[0]; + if (!win) return; + + if (query.pkgname) { + win.webContents.send("deep-link-search", { pkgname: query.pkgname }); + if (win.isMinimized()) win.restore(); + win.focus(); + } + }; + + logger.info(`isLoaded: ${isLoaded.value}`); + + if (isLoaded.value) { + action(); + } else { + pendingActions.push(action); + } +}); diff --git a/src/App.vue b/src/App.vue index 4c0f98b2..8941cd5e 100644 --- a/src/App.vue +++ b/src/App.vue @@ -781,6 +781,13 @@ onMounted(async () => { }, ); + window.ipcRenderer.on( + "deep-link-search", + (_event: IpcRendererEvent, data: { pkgname: string }) => { + searchQuery.value = data.pkgname; + }, + ); + window.ipcRenderer.on( "remove-complete", (_event: IpcRendererEvent, payload: ChannelPayload) => { diff --git a/vite.config.ts b/vite.config.ts index 21741c3d..2340da1b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -77,10 +77,10 @@ export default defineConfig(({ command }) => { host: url.hostname, port: +url.port, proxy: { - "/local_amd64-apm": { + "/local_amd64-store": { target: "https://erotica.spark-app.store", changeOrigin: true, - rewrite: (path) => path.replace(/^\/local_amd64-apm/, ""), + rewrite: (path) => path.replace(/^\/local_amd64-store/, ""), }, "/local_stats": { target: "https://feedback.spark-app.store", @@ -92,10 +92,10 @@ export default defineConfig(({ command }) => { } else { return { proxy: { - "/local_amd64-apm": { + "/local_amd64-store": { target: "https://erotica.spark-app.store", changeOrigin: true, - rewrite: (path) => path.replace(/^\/local_amd64-apm/, ""), + rewrite: (path) => path.replace(/^\/local_amd64-store/, ""), }, "/local_stats": { target: "https://feedback.spark-app.store",