|
1 | 1 | import { dialog } from 'electron'
|
2 | 2 | import { logError, logInfo, LogPrefix } from './logger/logger'
|
3 | 3 | import i18next from 'i18next'
|
4 |
| -import { getInfo } from './utils' |
5 | 4 | import { GameInfo, Runner } from 'common/types'
|
6 | 5 | import { getMainWindow, sendFrontendMessage } from './main_window'
|
7 | 6 | import { icon } from './constants'
|
8 | 7 | import { gameManagerMap } from './storeManagers'
|
9 | 8 | import { launchEventCallback } from './launcher'
|
| 9 | +import { z } from 'zod' |
10 | 10 |
|
11 |
| -type Command = 'ping' | 'launch' |
| 11 | +const RUNNERS = z.enum(['legendary', 'gog', 'nile', 'sideload']) |
12 | 12 |
|
13 |
| -const RUNNERS = ['legendary', 'gog', 'nile', 'sideload'] |
| 13 | +export function handleProtocol(args: string[]) { |
| 14 | + const urlStr = args.find((arg) => arg.startsWith('heroic://')) |
| 15 | + if (!urlStr) return |
14 | 16 |
|
15 |
| -/** |
16 |
| - * Handles a protocol request |
17 |
| - * @param args The args to search |
18 |
| - * @example |
19 |
| - * handleProtocol(['heroic://ping']) |
20 |
| - * // => 'Received ping! Arg: undefined' |
21 |
| - * handleProtocol(['heroic://launch/gog/123']) |
22 |
| - * // => 'Received launch! Runner: gog, Arg: 123' |
23 |
| - **/ |
24 |
| -export async function handleProtocol(args: string[]) { |
25 |
| - const mainWindow = getMainWindow() |
26 |
| - |
27 |
| - const url = getUrl(args) |
28 |
| - if (!url) { |
29 |
| - return |
30 |
| - } |
31 |
| - |
32 |
| - const [command, runner, arg = ''] = parseUrl(url) |
| 17 | + const url = new URL(urlStr) |
33 | 18 |
|
34 |
| - logInfo(`received '${url}'`, LogPrefix.ProtocolHandler) |
| 19 | + logInfo(['Received', url.href], LogPrefix.ProtocolHandler) |
35 | 20 |
|
36 |
| - switch (command) { |
| 21 | + switch (url.hostname) { |
37 | 22 | case 'ping':
|
38 |
| - return handlePing(arg) |
| 23 | + return handlePing(url) |
39 | 24 | case 'launch':
|
40 |
| - await handleLaunch(runner, arg, mainWindow) |
41 |
| - break |
| 25 | + return handleLaunch(url) |
42 | 26 | default:
|
43 | 27 | return
|
44 | 28 | }
|
45 | 29 | }
|
46 | 30 |
|
47 |
| -/** |
48 |
| - * Gets the url from the args |
49 |
| - * @param args The args to search |
50 |
| - * @returns The url if found, undefined otherwise |
51 |
| - * @example |
52 |
| - * getUrl(['heroic://ping']) |
53 |
| - * // => 'heroic://ping' |
54 |
| - * getUrl(['heroic://launch/gog/123']) |
55 |
| - * // => 'heroic://launch/gog/123' |
56 |
| - * getUrl(['heroic://launch/legendary/123']) |
57 |
| - * // => 'heroic://launch/legendary/123' |
58 |
| - **/ |
59 |
| -function getUrl(args: string[]): string | undefined { |
60 |
| - return args.find((arg) => arg.startsWith('heroic://')) |
| 31 | +function handlePing(url: URL) { |
| 32 | + logInfo(['Received ping! Args:', url.searchParams], LogPrefix.ProtocolHandler) |
61 | 33 | }
|
62 | 34 |
|
63 |
| -/** |
64 |
| - * Parses a url into a tuple of [Command, Runner?, string?] |
65 |
| - * @param url The url to parse |
66 |
| - * @returns A tuple of [Command, Runner?, string?] |
67 |
| - * @example |
68 |
| - * parseUrl('heroic://ping') |
69 |
| - * // => ['ping', undefined, undefined] |
70 |
| - * parseUrl('heroic://launch/gog/123') |
71 |
| - * // => ['launch', 'gog', '123'] |
72 |
| - * parseUrl('heroic://launch/123') |
73 |
| - * // => ['launch', '123'] |
74 |
| - **/ |
75 |
| -function parseUrl(url: string): [Command, Runner?, string?] { |
76 |
| - const [, fullCommand] = url.split('://') |
77 |
| - |
78 |
| - //check if the second param is a runner or not and adjust parts accordingly |
79 |
| - const hasRunner = RUNNERS.includes(fullCommand.split('/')[1] as Runner) |
80 |
| - if (hasRunner) { |
81 |
| - const [command, runner, arg] = fullCommand.split('/') |
82 |
| - return [command as Command, runner as Runner, arg] |
| 35 | +async function handleLaunch(url: URL) { |
| 36 | + let appName |
| 37 | + let runnerStr |
| 38 | + let args: string[] = [] |
| 39 | + |
| 40 | + if (url.pathname) { |
| 41 | + // Old-style pathname URLs: |
| 42 | + // - `heroic://launch/Quail` |
| 43 | + // - `heroic://launch/legendary/Quail` |
| 44 | + const splitPath = url.pathname.split('/').filter(Boolean) |
| 45 | + appName = splitPath.pop() |
| 46 | + runnerStr = splitPath.pop() |
83 | 47 | } else {
|
84 |
| - const [command, arg] = fullCommand.split('/') |
85 |
| - return [command as Command, undefined, arg] |
| 48 | + // New-style params URL: |
| 49 | + // `heroic://launch?appName=Quail&runner=legendary&arg=foo&arg=bar` |
| 50 | + appName = url.searchParams.get('appName') |
| 51 | + runnerStr = url.searchParams.get('runner') |
| 52 | + args = url.searchParams.getAll('arg') |
86 | 53 | }
|
87 |
| -} |
88 | 54 |
|
89 |
| -async function handlePing(arg: string) { |
90 |
| - return logInfo(['Received ping! Arg:', arg], LogPrefix.ProtocolHandler) |
91 |
| -} |
| 55 | + if (!appName) { |
| 56 | + logError('No appName in protocol URL', LogPrefix.ProtocolHandler) |
| 57 | + return |
| 58 | + } |
92 | 59 |
|
93 |
| -/** |
94 |
| - * Handles a launch command |
95 |
| - * @param runner The runner to launch the game with |
96 |
| - * @param arg The game to launch |
97 |
| - * @param mainWindow The main window |
98 |
| - * @example |
99 |
| - * handleLaunch('gog', '123') |
100 |
| - * // => 'Received launch! Runner: gog, Arg: 123' |
101 |
| - * handleLaunch('legendary', '123') |
102 |
| - * // => 'Received launch! Runner: legendary, Arg: 123' |
103 |
| - * handleLaunch('nile', '123') |
104 |
| - * // => 'Received launch! Runner: nile, Arg: 123' |
105 |
| - **/ |
106 |
| -async function handleLaunch( |
107 |
| - runner: Runner | undefined, |
108 |
| - arg: string | undefined, |
109 |
| - mainWindow?: Electron.BrowserWindow | null |
110 |
| -) { |
111 |
| - const game = await findGame(runner, arg) |
112 |
| - |
113 |
| - if (!game) { |
| 60 | + let runner: Runner | undefined |
| 61 | + const runnerParse = RUNNERS.safeParse(runnerStr) |
| 62 | + if (runnerParse.success) { |
| 63 | + runner = runnerParse.data |
| 64 | + } |
| 65 | + const gameInfo = findGame(appName, runner) |
| 66 | + if (!gameInfo) { |
114 | 67 | return logError(
|
115 |
| - `Could not receive game data for ${arg}!`, |
| 68 | + `Could not receive game data for ${appName}!`, |
116 | 69 | LogPrefix.ProtocolHandler
|
117 | 70 | )
|
118 | 71 | }
|
119 | 72 |
|
120 |
| - const { is_installed, title, app_name, runner: gameRunner } = game |
121 |
| - const settings = await gameManagerMap[gameRunner].getSettings(app_name) |
122 |
| - |
123 |
| - if (!is_installed) { |
124 |
| - logInfo(`"${title}" not installed.`, LogPrefix.ProtocolHandler) |
| 73 | + const { is_installed, title } = gameInfo |
| 74 | + const settings = await gameManagerMap[gameInfo.runner].getSettings(appName) |
125 | 75 |
|
126 |
| - if (!mainWindow) { |
127 |
| - return |
128 |
| - } |
129 |
| - |
130 |
| - const { response } = await dialog.showMessageBox(mainWindow, { |
131 |
| - buttons: [i18next.t('box.yes'), i18next.t('box.no')], |
132 |
| - cancelId: 1, |
133 |
| - message: `${title} ${i18next.t( |
134 |
| - 'box.protocol.install.not_installed', |
135 |
| - 'Is Not Installed, do you wish to Install it?' |
136 |
| - )}`, |
137 |
| - title: title, |
138 |
| - icon: icon |
| 76 | + if (is_installed) { |
| 77 | + return launchEventCallback({ |
| 78 | + appName: appName, |
| 79 | + runner: gameInfo.runner, |
| 80 | + skipVersionCheck: settings.ignoreGameUpdates, |
| 81 | + args |
139 | 82 | })
|
140 |
| - if (response === 0) { |
141 |
| - return sendFrontendMessage('installGame', app_name, gameRunner) |
142 |
| - } |
143 |
| - if (response === 1) { |
144 |
| - return logInfo('Not installing game', LogPrefix.ProtocolHandler) |
145 |
| - } |
146 | 83 | }
|
147 | 84 |
|
148 |
| - mainWindow?.hide() |
149 |
| - launchEventCallback({ |
150 |
| - appName: app_name, |
151 |
| - runner: gameRunner, |
152 |
| - skipVersionCheck: settings.ignoreGameUpdates |
| 85 | + logInfo(`"${title}" not installed.`, LogPrefix.ProtocolHandler) |
| 86 | + |
| 87 | + const mainWindow = getMainWindow() |
| 88 | + if (!mainWindow) return |
| 89 | + |
| 90 | + const { response } = await dialog.showMessageBox(mainWindow, { |
| 91 | + buttons: [i18next.t('box.yes'), i18next.t('box.no')], |
| 92 | + cancelId: 1, |
| 93 | + message: `${title} ${i18next.t( |
| 94 | + 'box.protocol.install.not_installed', |
| 95 | + 'Is Not Installed, do you wish to Install it?' |
| 96 | + )}`, |
| 97 | + title: title, |
| 98 | + icon: icon |
153 | 99 | })
|
| 100 | + if (response === 0) { |
| 101 | + sendFrontendMessage('installGame', appName, gameInfo.runner) |
| 102 | + } else if (response === 1) { |
| 103 | + logInfo('Not installing game', LogPrefix.ProtocolHandler) |
| 104 | + } |
154 | 105 | }
|
155 | 106 |
|
156 |
| -/** |
157 |
| - * Finds a game in the runners specified in runnersToSearch |
158 |
| - * @param runner The runner to search for the game |
159 |
| - * @param arg The game to search |
160 |
| - * @returns The game info if found, null otherwise |
161 |
| - * @example |
162 |
| - * findGame('gog', '123') |
163 |
| - * // => { app_name: '123', title: '123', is_installed: true, runner: 'gog' ...} |
164 |
| - * findGame('legendary', '123') |
165 |
| - * // => { app_name: '123', title: '123', is_installed: true, runner: 'legendary' ...} |
166 |
| - * findGame('nile', '123') |
167 |
| - * // => { app_name: '123', title: '123', is_installed: true, runner: 'nile' ...} |
168 |
| - **/ |
169 |
| -async function findGame( |
170 |
| - runner: Runner | undefined, |
171 |
| - arg = '' |
172 |
| -): Promise<GameInfo | null> { |
173 |
| - if (!arg) { |
174 |
| - return null |
175 |
| - } |
| 107 | +function findGame( |
| 108 | + appName?: string | null, |
| 109 | + runner?: Runner |
| 110 | +): GameInfo | undefined { |
| 111 | + if (!appName) return |
176 | 112 |
|
177 | 113 | // If a runner is specified, search for the game in that runner and return it (if found)
|
178 |
| - if (runner) { |
179 |
| - const gameInfo = getInfo(arg, runner) |
180 |
| - if (gameInfo.app_name) { |
181 |
| - return gameInfo |
182 |
| - } |
183 |
| - } |
| 114 | + if (runner) return gameManagerMap[runner].getGameInfo(appName) |
184 | 115 |
|
185 | 116 | // If no runner is specified, search for the game in all runners and return the first one found
|
186 |
| - for (const currentRunner of RUNNERS) { |
187 |
| - const run = (currentRunner as Runner) || 'legendary' |
188 |
| - |
189 |
| - const gameInfoOrSideload = getInfo(arg, run) |
190 |
| - if (gameInfoOrSideload.app_name) { |
191 |
| - return gameInfoOrSideload |
192 |
| - } |
| 117 | + for (const runner of RUNNERS.options) { |
| 118 | + const maybeGameInfo = gameManagerMap[runner].getGameInfo(appName) |
| 119 | + if (maybeGameInfo.app_name) return maybeGameInfo |
193 | 120 | }
|
194 |
| - return null |
| 121 | + return |
195 | 122 | }
|
0 commit comments