feat: add Raw Performer badge and real data to search results

This commit is contained in:
2026-04-26 15:55:05 +08:00
parent 1f3e8b9201
commit e7e16d4772
2 changed files with 91 additions and 9 deletions
+26 -8
View File
@@ -39,6 +39,9 @@ interface UnifiedResult {
price?: { currency: string; initial: number; final: number } | null price?: { currency: string; initial: number; final: number } | null
platforms?: { windows: boolean; mac: boolean; linux: boolean } | null platforms?: { windows: boolean; mac: boolean; linux: boolean } | null
controllerSupport?: string | null controllerSupport?: string | null
isRawPerformer?: boolean
bestFps?: number | null
latestVersion?: string | null
} }
function SearchContent() { function SearchContent() {
@@ -86,8 +89,8 @@ function SearchContent() {
function handleClick(result: UnifiedResult) { function handleClick(result: UnifiedResult) {
const path = result.appId const path = result.appId
? `/game/${result.appId}` ? `/game/${result.appId}?sync=1`
: `/game/${result.id}` : `/game/${result.id}?sync=1`
router.push(path) router.push(path)
} }
@@ -244,9 +247,16 @@ function SearchResultCard({
{/* Row 1: Title + metascore/price row */} {/* Row 1: Title + metascore/price row */}
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
<div className="min-w-0"> <div className="min-w-0">
<h3 className="text-sm sm:text-base font-semibold text-text group-hover:text-primary transition-colors duration-200 truncate"> <div className="min-w-0 flex items-center gap-2 flex-wrap">
{result.title} <h3 className="text-sm sm:text-base font-semibold text-text group-hover:text-primary transition-colors duration-200 truncate">
</h3> {result.title}
</h3>
{result.isRawPerformer && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-green-500/10 border border-green-500/20 text-green-400 text-[10px] font-semibold shrink-0">
RAW PERFORMER
</span>
)}
</div>
{(result.developer || result.publisher) && ( {(result.developer || result.publisher) && (
<p className="text-[11px] text-text/45 mt-0.5 truncate"> <p className="text-[11px] text-text/45 mt-0.5 truncate">
{result.developer} {result.developer}
@@ -422,11 +432,19 @@ function SearchResultCard({
color={result.platformSupport ? protonColor(result.platformSupport.protonStatus) : undefined} color={result.platformSupport ? protonColor(result.platformSupport.protonStatus) : undefined}
/> />
{/* Avg FPS */} {/* Best FPS */}
<DataField label="Avg. FPS" value="—" bar /> <DataField
label="Best FPS"
value={result.bestFps != null ? String(Math.round(result.bestFps)) : "—"}
bar={result.bestFps != null}
color={result.bestFps != null && result.bestFps >= 60 ? "text-green-400" : undefined}
/>
{/* Version */} {/* Version */}
<DataField label="Version" value="—" /> <DataField
label="Version"
value={result.latestVersion ?? "—"}
/>
</div> </div>
</div> </div>
</motion.article> </motion.article>
+65 -1
View File
@@ -7,7 +7,7 @@ import {
communityPresets, communityPresets,
gameComments, gameComments,
} from "@/lib/db/schema" } from "@/lib/db/schema"
import { ilike, or, sql, eq, inArray } from "drizzle-orm" import { ilike, or, sql, eq, inArray, and } from "drizzle-orm"
interface SteamSearchItem { interface SteamSearchItem {
id: number id: number
@@ -146,6 +146,64 @@ export const searchUnifiedRoutes = new Elysia({ prefix: "/search" }).get(
countMap.get(c.gameId)!.comments = c.count countMap.get(c.gameId)!.comments = c.count
} }
// ── 2b. Raw Performer + best FPS ────────────────────────────────
let rawPerformerMap = new Map<string, boolean>()
let bestFpsMap = new Map<string, number>()
if (localGameIds.length > 0) {
const perfStats = await db
.select({
gameId: gameVersions.gameId,
bestFps: sql<number>`MAX(${performanceEntries.fpsAvg})::real`,
isRawPerformer: sql<boolean>`BOOL_OR(
${performanceEntries.fpsAvg} >= 60
AND ${performanceEntries.fsrVersion} = 'none'
AND ${performanceEntries.frameGenMethod} = 'none'
)`,
})
.from(performanceEntries)
.innerJoin(
gameVersions,
eq(performanceEntries.versionId, gameVersions.id),
)
.where(
and(
inArray(gameVersions.gameId, localGameIds),
eq(performanceEntries.isRemoved, false),
),
)
.groupBy(gameVersions.gameId)
for (const row of perfStats) {
bestFpsMap.set(row.gameId, row.bestFps)
rawPerformerMap.set(row.gameId, row.isRawPerformer)
}
}
// ── 2c. Latest version ──────────────────────────────────────────
let latestVersionMap = new Map<string, string>()
if (localGameIds.length > 0) {
const versionRows = await db
.select({
gameId: gameVersions.gameId,
versionString: gameVersions.versionString,
})
.from(gameVersions)
.where(
and(
inArray(gameVersions.gameId, localGameIds),
eq(gameVersions.isLatest, true),
),
)
for (const row of versionRows) {
if (row.versionString) {
latestVersionMap.set(row.gameId, row.versionString)
}
}
}
// ── 3. Search Steam ───────────────────────────────────────────── // ── 3. Search Steam ─────────────────────────────────────────────
let steamItems: SteamSearchItem[] = [] let steamItems: SteamSearchItem[] = []
try { try {
@@ -214,6 +272,9 @@ export const searchUnifiedRoutes = new Elysia({ prefix: "/search" }).get(
antiCheatStatus: platform.antiCheatStatus, antiCheatStatus: platform.antiCheatStatus,
} }
: null, : null,
isRawPerformer: rawPerformerMap.get(g.id) ?? false,
bestFps: bestFpsMap.get(g.id) ?? null,
latestVersion: latestVersionMap.get(g.id) ?? null,
}) })
} }
@@ -242,6 +303,9 @@ export const searchUnifiedRoutes = new Elysia({ prefix: "/search" }).get(
: null, : null,
platforms: item.platforms, platforms: item.platforms,
controllerSupport: item.controller_support || null, controllerSupport: item.controller_support || null,
isRawPerformer: false,
bestFps: null,
latestVersion: null,
}) })
} }