Skip to content

Commit 83f2b6f

Browse files
committed
support direct addon search and protocol handlers
1 parent 51a4840 commit 83f2b6f

File tree

14 files changed

+608
-51
lines changed

14 files changed

+608
-51
lines changed

apps/desktop/.electron-builder.config.cjs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,16 @@ module.exports = {
8181
name: "gdlauncher",
8282
role: "Viewer",
8383
schemes: ["gdlauncher"]
84+
},
85+
{
86+
name: "CurseForge",
87+
role: "Viewer",
88+
schemes: ["curseforge"]
89+
},
90+
{
91+
name: "Modrinth",
92+
role: "Viewer",
93+
schemes: ["modrinth"]
8494
}
8595
],
8696
win: {

apps/desktop/packages/main/index.ts

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -563,20 +563,33 @@ if ((app as any).overwolf) {
563563
// Set application name for Windows 10+ notifications
564564
if (process.platform === "win32") app.setAppUserModelId(app.getName())
565565

566-
if (process.defaultApp) {
567-
if (process.argv.length >= 2) {
568-
app.setAsDefaultProtocolClient("gdlauncher", process.execPath, [
569-
resolve(process.argv[1])
570-
])
566+
// Register protocol handlers for gdlauncher, curseforge, and modrinth
567+
const protocols = ["gdlauncher", "curseforge", "modrinth"]
568+
for (const protocol of protocols) {
569+
if (process.defaultApp) {
570+
if (process.argv.length >= 2) {
571+
app.setAsDefaultProtocolClient(protocol, process.execPath, [
572+
resolve(process.argv[1])
573+
])
574+
}
575+
} else {
576+
app.setAsDefaultProtocolClient(protocol)
571577
}
572-
} else {
573-
app.setAsDefaultProtocolClient("gdlauncher")
574578
}
575579

576580
let lastDisplay: Display | null = null
577581

578582
let isSpawningWindow = false
579583

584+
// Queue for protocol URLs received before window is ready
585+
let pendingProtocolUrl: string | null = null
586+
587+
// Helper to check if a URL is a supported protocol
588+
const isSupportedProtocol = (url: string) =>
589+
url.startsWith("gdlauncher://") ||
590+
url.startsWith("curseforge://") ||
591+
url.startsWith("modrinth://")
592+
580593
async function createWindow(): Promise<BrowserWindow> {
581594
console.log("Creating window...")
582595
if (isSpawningWindow) {
@@ -682,6 +695,16 @@ async function createWindow(): Promise<BrowserWindow> {
682695
isSpawningWindow = false
683696
console.log("Window is ready to show")
684697

698+
// Send any pending protocol URL that was received before window was ready
699+
if (pendingProtocolUrl) {
700+
console.log("Sending pending protocol URL:", pendingProtocolUrl)
701+
// Small delay to ensure renderer is fully initialized
702+
setTimeout(() => {
703+
win?.webContents.send("protocol-url", pendingProtocolUrl)
704+
pendingProtocolUrl = null
705+
}, 500)
706+
}
707+
685708
function upsertKeyValue(obj: any, keyToChange: string, value: any) {
686709
const keyToChangeLower = keyToChange.toLowerCase()
687710
for (const key of Object.keys(obj)) {
@@ -1000,7 +1023,7 @@ app.whenReady().then(async () => {
10001023

10011024
app.on("second-instance", (_e, argv) => {
10021025
// Handle protocol URLs on Windows (passed as command line arguments)
1003-
const protocolUrl = argv.find((arg) => arg.startsWith("gdlauncher://"))
1026+
const protocolUrl = argv.find((arg) => isSupportedProtocol(arg))
10041027

10051028
if (win && !win.isDestroyed()) {
10061029
// Focus on the main window if the user tried to open another
@@ -1013,14 +1036,11 @@ app.whenReady().then(async () => {
10131036
win.webContents.send("protocol-url", protocolUrl)
10141037
}
10151038
} else {
1016-
createWindow()
1017-
1018-
// If window was just created, wait a bit for it to be ready
1019-
if (protocolUrl && win) {
1020-
setTimeout(() => {
1021-
win?.webContents.send("protocol-url", protocolUrl)
1022-
}, 1000)
1039+
// Store URL and create window
1040+
if (protocolUrl) {
1041+
pendingProtocolUrl = protocolUrl
10231042
}
1043+
createWindow()
10241044
}
10251045
})
10261046

@@ -1107,8 +1127,8 @@ app.on("render-process-gone", (event, webContents, detailed) => {
11071127
app.on("open-url", (event, url) => {
11081128
console.log("Protocol URL received:", url)
11091129

1110-
// Handle gdlauncher:// protocol URLs
1111-
if (url.startsWith("gdlauncher://")) {
1130+
// Handle gdlauncher://, curseforge://, and modrinth:// protocol URLs
1131+
if (isSupportedProtocol(url)) {
11121132
event.preventDefault()
11131133

11141134
// Focus the window if minimized
@@ -1120,9 +1140,8 @@ app.on("open-url", (event, url) => {
11201140
win.webContents.send("protocol-url", url)
11211141
} else {
11221142
// Window not ready yet, store the URL for later
1123-
// This can happen if the app is launched via protocol before window is created
11241143
console.log("Window not ready, storing protocol URL for later")
1125-
// You could implement a queue here if needed
1144+
pendingProtocolUrl = url
11261145
}
11271146
}
11281147
})

apps/desktop/packages/mainWindow/src/components/EnhancedSearchBar.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,13 @@ export function EnhancedSearchBar() {
105105
/>
106106
</Show>
107107

108+
<Show when={isExpanded() && searchResults?.isDirectMode()}>
109+
<div class="bg-primary-600/20 text-primary-400 flex shrink-0 items-center gap-1 rounded px-2 py-0.5 text-xs">
110+
<div class="i-hugeicons:link-01 text-sm" />
111+
<span>{t("search:_trn_direct")}</span>
112+
</div>
113+
</Show>
114+
108115
<Show
109116
when={
110117
isExpanded() &&

apps/desktop/packages/mainWindow/src/global.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ declare global {
8989
listenToCoreModuleProgress: (
9090
cb: (event: Electron.IpcRendererEvent, progress: number) => void
9191
) => void
92+
onProtocolUrl: (cb: (url: string) => void) => void
9293
}
9394
}
9495

apps/desktop/packages/mainWindow/src/managers/NavigationManager/index.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import {
55
isNewsPath,
66
isNewsDetailPath
77
} from "@/utils/routes"
8+
import { parseSearchQuery } from "@/utils/searchQueryParser"
89
import { useLocation, useNavigate } from "@solidjs/router"
9-
import { JSX, createContext, createSignal, useContext } from "solid-js"
10+
import { JSX, createContext, createSignal, onMount, useContext } from "solid-js"
1011

1112
const getTransitionClassToApply = (from: string, to: string) => {
1213
if (isSearchPath(from) && isAddonPath(to)) {
@@ -49,6 +50,19 @@ export const NavigationManager = (props: { children: JSX.Element }) => {
4950
searchParams: location.search
5051
})
5152

53+
// Handle protocol URLs (curseforge://, modrinth://)
54+
onMount(() => {
55+
window.onProtocolUrl?.((url) => {
56+
console.log("Protocol URL received in renderer:", url)
57+
const parsed = parseSearchQuery(url)
58+
if (parsed.mode === "direct" && parsed.items.length > 0) {
59+
// Navigate to search page with the URL pre-filled
60+
// The search page will parse this and show the direct results
61+
navigate(`/search?q=${encodeURIComponent(url)}`)
62+
}
63+
})
64+
})
65+
5266
const shouldTransition = () =>
5367
!globalstore.settings.data?.reducedMotion && document.startViewTransition
5468

apps/desktop/packages/mainWindow/src/utils/platformSearch.ts

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
FEUnifiedBatchRequest,
23
FEUnifiedSearchParameters,
34
FEUnifiedSearchResult,
45
FEUnifiedSearchType
@@ -10,6 +11,7 @@ import { createAsyncEffect } from "./asyncEffect"
1011
import { createInfiniteQuery } from "@tanstack/solid-query"
1112
import { VirtualizerHandle } from "virtua/lib/solid"
1213
import { useSearchParams } from "@solidjs/router"
14+
import { parseSearchQuery, buildBatchRequest } from "./searchQueryParser"
1315

1416
const defaultSearchQuery: FEUnifiedSearchParameters = {
1517
searchQuery: "",
@@ -102,17 +104,28 @@ export const getSearchResults = (_opts?: SearchResultsOpts) => {
102104
}
103105
})
104106

107+
// Check for protocol URL in query params (from deep link)
108+
const initialSearchText = searchParams.q
109+
? decodeURIComponent(String(searchParams.q))
110+
: ""
111+
105112
const [searchQuery, _setSearchQuery] =
106113
createSignal<FEUnifiedSearchParameters>(
107114
{
108115
...defaultSearchQuery,
109-
...opts.defaultSearchQuery
116+
...opts.defaultSearchQuery,
117+
...(initialSearchText ? { searchQuery: initialSearchText } : {})
110118
},
111119
{
112120
equals: false
113121
}
114122
)
115123

124+
// Clear the q param from URL after reading (one-time use)
125+
if (searchParams.q) {
126+
_setSearchParams({ ...searchParams, q: undefined })
127+
}
128+
116129
const setSearchQuery = (
117130
value:
118131
| FEUnifiedSearchParameters
@@ -152,9 +165,30 @@ export const getSearchResults = (_opts?: SearchResultsOpts) => {
152165
return pageSize
153166
}
154167

168+
// Direct search mode - for URLs, protocols, and # prefix IDs
169+
// Must be defined before infinite queries so isDirectMode() is available
170+
const parsedQuery = createMemo(() =>
171+
parseSearchQuery(searchQuery().searchQuery || "")
172+
)
173+
174+
const isDirectMode = () => parsedQuery().mode === "direct"
175+
176+
const directBatchRequest = createMemo<FEUnifiedBatchRequest>(() =>
177+
buildBatchRequest(parsedQuery())
178+
)
179+
180+
const directSearchQuery = rspc.createQuery(() => ({
181+
queryKey: [
182+
"modplatforms.unifiedGetProjectsByIds",
183+
directBatchRequest()
184+
] as const,
185+
enabled: isDirectMode() && parsedQuery().items.length > 0
186+
}))
187+
155188
const cfInfiniteResults = createInfiniteQuery(() => ({
156189
queryKey: ["modplatforms.unifiedSearch.cf"],
157190
enabled:
191+
!isDirectMode() &&
158192
(searchQuery().searchQuery?.length || 0) > 0 &&
159193
(!searchQuery().searchApi || searchQuery().searchApi === "curseforge"),
160194
queryFn: (ctx) => {
@@ -188,6 +222,7 @@ export const getSearchResults = (_opts?: SearchResultsOpts) => {
188222
const mrInfiniteResults = createInfiniteQuery(() => ({
189223
queryKey: ["modplatforms.unifiedSearch.mr"],
190224
enabled:
225+
!isDirectMode() &&
191226
(searchQuery().searchQuery?.length || 0) > 0 &&
192227
(!searchQuery().searchApi || searchQuery().searchApi === "modrinth"),
193228
queryFn: (ctx) => {
@@ -242,6 +277,22 @@ export const getSearchResults = (_opts?: SearchResultsOpts) => {
242277
}, null)
243278

244279
const allRows = createMemo<SearchResultItem[]>(() => {
280+
// Direct mode - return results from batch query
281+
if (isDirectMode()) {
282+
const directResults = directSearchQuery.data?.results ?? []
283+
const items: SearchResultItem[] = directResults.map((item) => ({
284+
type: "value" as const,
285+
value: item
286+
}))
287+
288+
if (directSearchQuery.isFetching) {
289+
items.push({ type: "loader" })
290+
}
291+
292+
return items
293+
}
294+
295+
// Regular search mode
245296
const cfData =
246297
searchQuery().searchApi === "modrinth"
247298
? []
@@ -323,6 +374,9 @@ export const getSearchResults = (_opts?: SearchResultsOpts) => {
323374
}
324375

325376
const isInitialLoading = createMemo(() => {
377+
if (isDirectMode()) {
378+
return directSearchQuery.isLoading
379+
}
326380
if (searchQuery().searchApi === "curseforge") {
327381
return cfInfiniteResults.isLoading
328382
} else if (searchQuery().searchApi === "modrinth") {
@@ -332,6 +386,9 @@ export const getSearchResults = (_opts?: SearchResultsOpts) => {
332386
})
333387

334388
const isLoading = createMemo(() => {
389+
if (isDirectMode()) {
390+
return directSearchQuery.isLoading || directSearchQuery.isFetching
391+
}
335392
if (searchQuery().searchApi === "curseforge") {
336393
return cfInfiniteResults.isLoading || cfInfiniteResults.isFetching
337394
} else if (searchQuery().searchApi === "modrinth") {
@@ -364,7 +421,10 @@ export const getSearchResults = (_opts?: SearchResultsOpts) => {
364421
selectedInstance,
365422
selectedInstanceMods,
366423
setSelectedInstanceId,
367-
selectedInstanceId
424+
selectedInstanceId,
425+
// Direct search mode
426+
isDirectMode,
427+
parsedQuery
368428
}
369429
}
370430

0 commit comments

Comments
 (0)