feat: update search page and improve details

This commit is contained in:
2026-04-26 01:08:10 +08:00
parent 6415200f34
commit 4fc4b070dc
19 changed files with 1265 additions and 222 deletions
+9 -2
View File
@@ -4,12 +4,12 @@ import { games } from "@/lib/db/schema"
import { eq } from "drizzle-orm"
interface SteamAppDetails {
type?: string
steam_appid: number
name: string
developers?: string[]
publishers?: string[]
header_image?: string
capsule_imagev5?: string
genres?: { id: string; description: string }[]
website?: string
}
@@ -50,6 +50,13 @@ export const gameStubRoutes = new Elysia({ prefix: "/games" }).post(
details = entry.data
}
}
// Reject non-games (DLCs, soundtracks, demos, etc.)
if (details?.type && details.type !== "game") {
set.status = 400
return { error: `Not a game (type: ${details.type})` }
}
} catch (err) {
console.error("Failed to fetch Steam appdetails:", err)
}
@@ -60,7 +67,7 @@ export const gameStubRoutes = new Elysia({ prefix: "/games" }).post(
const genres = details?.genres?.map((g) => g.description) || []
const headerImage = details?.header_image || null
const capsuleImage =
details?.capsule_imagev5 || details?.header_image || null
`https://cdn.akamai.steamstatic.com/steam/apps/${body.steamAppId}/library_600x900.jpg`
const [game] = await db
.insert(games)
+94 -5
View File
@@ -1,6 +1,12 @@
import { Elysia, t } from "elysia"
import { db } from "@/lib/db/index"
import { games, gameVersions, performanceEntries, communityPresets, gameComments } from "@/lib/db/schema"
import {
games,
gameVersions,
performanceEntries,
communityPresets,
gameComments,
} from "@/lib/db/schema"
import { ilike, or, sql, eq, inArray } from "drizzle-orm"
interface SteamSearchItem {
@@ -8,7 +14,9 @@ interface SteamSearchItem {
name: string
tiny_image: string
metascore: string
price?: { currency: string; initial: number; final: number }
platforms: { windows: boolean; mac: boolean; linux: boolean }
controller_support?: string
}
interface SteamSearchResponse {
@@ -44,6 +52,42 @@ export const searchUnifiedRoutes = new Elysia({ prefix: "/search" }).get(
localGames.map((g) => g.steamAppId).filter(Boolean),
)
// Fetch platform support + anti-cheat for local games
let platformSupportMap = new Map<
string,
{
isSupported: boolean
protonStatus: string
antiCheatRelevant: boolean
antiCheatName: string | null
antiCheatStatus: string
}
>()
if (localGameIds.length > 0) {
const { gamePlatformSupport } = await import("@/lib/db/schema")
const supportRows = await db
.select({
gameId: gamePlatformSupport.gameId,
isSupported: gamePlatformSupport.isSupported,
protonStatus: gamePlatformSupport.protonStatus,
antiCheatRelevant: gamePlatformSupport.antiCheatRelevant,
antiCheatName: gamePlatformSupport.antiCheatName,
antiCheatStatus: gamePlatformSupport.antiCheatStatus,
})
.from(gamePlatformSupport)
.where(inArray(gamePlatformSupport.gameId, localGameIds))
for (const row of supportRows) {
platformSupportMap.set(row.gameId, {
isSupported: row.isSupported,
protonStatus: row.protonStatus,
antiCheatRelevant: row.antiCheatRelevant,
antiCheatName: row.antiCheatName,
antiCheatStatus: row.antiCheatStatus,
})
}
}
// ── 2. Count related data for local games ───────────────────────
let benchmarkCounts: { gameId: string; count: number }[] = []
let presetCounts: { gameId: string; count: number }[] = []
@@ -115,19 +159,40 @@ export const searchUnifiedRoutes = new Elysia({ prefix: "/search" }).get(
})
if (res.ok) {
const data = (await res.json()) as SteamSearchResponse
steamItems = data.items || []
steamItems = (data.items || []).filter((item) => {
const name = item.name.toLowerCase()
const exclude = [
"soundtrack",
" original soundtrack",
" ost",
" - ost",
"dlc",
"expansion",
"season pass",
" deluxe edition",
" ultimate edition",
" premium edition",
" demo",
" trial",
" playtest",
" beta",
" artbook",
" soundtrack bundle",
]
return !exclude.some((kw) => name.includes(kw))
})
}
} catch {
// Steam search failure is non-fatal
}
// ── 4. Build unified results ────────────────────────────────────
// Local games first (they have data), then Steam-only results
const results = []
// Add local games
for (const g of localGames) {
const counts = countMap.get(g.id)!
const platform = platformSupportMap.get(g.id)
results.push({
kind: "local" as const,
id: g.id,
@@ -136,23 +201,47 @@ export const searchUnifiedRoutes = new Elysia({ prefix: "/search" }).get(
image: g.capsuleImage || g.headerImage,
developer: g.developer,
publisher: g.publisher,
description: g.description,
genres: g.genres,
source: g.source,
counts,
platformSupport: platform
? {
isSupported: platform.isSupported,
protonStatus: platform.protonStatus,
antiCheatRelevant: platform.antiCheatRelevant,
antiCheatName: platform.antiCheatName,
antiCheatStatus: platform.antiCheatStatus,
}
: null,
})
}
// Add Steam-only games (deduplicated against local steamAppIds)
// Add Steam-only games
for (const item of steamItems) {
if (localSteamAppIds.has(item.id)) continue
results.push({
kind: "steam" as const,
appId: item.id,
title: item.name,
image: item.tiny_image,
image: `https://cdn.akamai.steamstatic.com/steam/apps/${item.id}/library_600x900.jpg`,
developer: null,
publisher: null,
description: null,
genres: null,
source: "steam" as const,
counts: null,
platformSupport: null,
metascore: item.metascore || null,
price: item.price
? {
currency: item.price.currency,
initial: item.price.initial,
final: item.price.final,
}
: null,
platforms: item.platforms,
controllerSupport: item.controller_support || null,
})
}
+27 -2
View File
@@ -38,11 +38,36 @@ export const steamSearchRoutes = new Elysia({ prefix: "/search" })
const data = await res.json()
const items: SteamSearchItem[] = (data.items || []).filter(
(item: SteamSearchItem) => {
const name = item.name.toLowerCase()
const exclude = [
"soundtrack",
" original soundtrack",
" ost",
" - ost",
"dlc",
"expansion",
"season pass",
" deluxe edition",
" ultimate edition",
" premium edition",
" demo",
" trial",
" playtest",
" beta",
" artbook",
" soundtrack bundle",
]
return !exclude.some((kw) => name.includes(kw))
},
)
return {
items: (data.items || []).map((item: SteamSearchItem) => ({
items: items.map((item) => ({
appId: item.id,
title: item.name,
image: item.tiny_image,
image: `https://cdn.akamai.steamstatic.com/steam/apps/${item.id}/library_600x900.jpg`,
platforms: item.platforms,
metascore: item.metascore,
})),
+1 -2
View File
@@ -8,7 +8,6 @@ interface SteamAppDetails {
developers?: string[]
publishers?: string[]
header_image?: string
capsule_imagev5?: string
genres?: { id: string; description: string }[]
website?: string
short_description?: string
@@ -59,7 +58,7 @@ export async function syncSteamGame(steamAppId: number): Promise<void> {
description: d.short_description || null,
genres: d.genres?.map((g) => g.description) || [],
headerImage: d.header_image || null,
capsuleImage: d.capsule_imagev5 || d.header_image || null,
capsuleImage: `https://cdn.akamai.steamstatic.com/steam/apps/${steamAppId}/library_600x900.jpg`,
storeUrl: `https://store.steampowered.com/app/${steamAppId}`,
lastSync: new Date(),
syncStatus: "synced",