feat: game info page

This commit is contained in:
2024-10-23 00:23:22 +03:00
parent 76b6fb3da6
commit 37283e8344
16 changed files with 410 additions and 69 deletions

17
app.go
View File

@@ -131,6 +131,23 @@ func (a *App) GetGames() GamesResponse {
return GamesResponse{Games: games}
}
type GameInfoResponse struct {
Error string `json:"error,omitempty"`
Game steam.GameInfo `json:"game"`
}
func GameInfoError(err error) GameInfoResponse {
return GameInfoResponse{Error: err.Error()}
}
func (a *App) GetGameInfo(gameId string) GameInfoResponse {
gameInfo, err := steam.GetGameInfo(gameId)
if err != nil {
return GameInfoError(err)
}
return GameInfoResponse{Game: gameInfo}
}
func (a *App) OnWindowResize() {
config := config.GetConfig()

View File

@@ -11,6 +11,7 @@
},
"dependencies": {
"@fontsource-variable/nunito": "^5.1.0",
"@radix-ui/react-accordion": "^1.2.1",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-slot": "^1.1.0",

117
frontend/pnpm-lock.yaml generated
View File

@@ -11,6 +11,9 @@ importers:
'@fontsource-variable/nunito':
specifier: ^5.1.0
version: 5.1.0
'@radix-ui/react-accordion':
specifier: ^1.2.1
version: 1.2.1(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-dialog':
specifier: ^1.1.2
version: 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -429,6 +432,45 @@ packages:
'@radix-ui/primitive@1.1.0':
resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==}
'@radix-ui/react-accordion@1.2.1':
resolution: {integrity: sha512-bg/l7l5QzUjgsh8kjwDFommzAshnUsuVMV5NM56QVCm+7ZckYdd9P/ExR8xG/Oup0OajVxNLaHJ1tb8mXk+nzQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-collapsible@1.1.1':
resolution: {integrity: sha512-1///SnrfQHJEofLokyczERxQbWfCGQlQ2XsCZMucVs6it+lq9iw4vXy+uDn1edlb58cOZOWSldnfPAYcT4O/Yg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-collection@1.1.0':
resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-compose-refs@1.1.0':
resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==}
peerDependencies:
@@ -438,6 +480,15 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-context@1.1.0':
resolution: {integrity: sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-context@1.1.1':
resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==}
peerDependencies:
@@ -460,6 +511,15 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-direction@1.1.0':
resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-dismissable-layer@1.1.1':
resolution: {integrity: sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==}
peerDependencies:
@@ -2484,12 +2544,63 @@ snapshots:
'@radix-ui/primitive@1.1.0': {}
'@radix-ui/react-accordion@1.2.1(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.0
'@radix-ui/react-collapsible': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.11)(react@18.3.1)
'@radix-ui/react-context': 1.1.1(@types/react@18.3.11)(react@18.3.1)
'@radix-ui/react-direction': 1.1.0(@types/react@18.3.11)(react@18.3.1)
'@radix-ui/react-id': 1.1.0(@types/react@18.3.11)(react@18.3.1)
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.11)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.11
'@types/react-dom': 18.3.1
'@radix-ui/react-collapsible@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.0
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.11)(react@18.3.1)
'@radix-ui/react-context': 1.1.1(@types/react@18.3.11)(react@18.3.1)
'@radix-ui/react-id': 1.1.0(@types/react@18.3.11)(react@18.3.1)
'@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.11)(react@18.3.1)
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.11)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.11
'@types/react-dom': 18.3.1
'@radix-ui/react-collection@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.11)(react@18.3.1)
'@radix-ui/react-context': 1.1.0(@types/react@18.3.11)(react@18.3.1)
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-slot': 1.1.0(@types/react@18.3.11)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.11
'@types/react-dom': 18.3.1
'@radix-ui/react-compose-refs@1.1.0(@types/react@18.3.11)(react@18.3.1)':
dependencies:
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.11
'@radix-ui/react-context@1.1.0(@types/react@18.3.11)(react@18.3.1)':
dependencies:
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.11
'@radix-ui/react-context@1.1.1(@types/react@18.3.11)(react@18.3.1)':
dependencies:
react: 18.3.1
@@ -2518,6 +2629,12 @@ snapshots:
'@types/react': 18.3.11
'@types/react-dom': 18.3.1
'@radix-ui/react-direction@1.1.0(@types/react@18.3.11)(react@18.3.1)':
dependencies:
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.11
'@radix-ui/react-dismissable-layer@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.0

View File

@@ -3,11 +3,13 @@ import { MainSidebar } from './components/MainSidebar/MainSidebar'
import React, { useEffect, useState } from 'react'
import { HashRouter, Route, Routes } from 'react-router-dom'
import { GetLibraryInfo, OnWindowResize } from '$app'
import { ScreenshotsHomePage } from './pages/Screenshots/ScreenshotsHomePage'
import { useApi } from './common/api'
import { AppContext } from './common/app_context'
import { LoadingContainer } from './components/Loader/LoadingContainer'
import { GamesPage } from './pages/Games/GamesPage'
import { GamesHomePage } from './pages/Games/GamesHomePage'
import ScreenshotsGamePage from './pages/Screenshots/ScreenshotsGamePage'
import { ScreenshotsHome } from './pages/Screenshots/ScreenshotsHomePage'
import { GameInfoPage } from './pages/Games/GameInfoPage'
function App() {
const [queryClient] = useState(() => new QueryClient())
@@ -23,9 +25,11 @@ function App() {
<MainSidebar className="min-w-64 w-64" />
<div className="max-h-screen overflow-y-auto w-full">
<Routes>
<Route path="/" element={<GamesPage />} />
<Route path="/games/*" element={<GamesPage />} />
<Route path="/screenshots/*" element={<ScreenshotsHomePage />} />
<Route path="/" element={<GamesHomePage />} />
<Route path="/games/:gameId" element={<GameInfoPage />} />
<Route path="/games/" element={<GamesHomePage />} />
<Route path="/screenshots/:gameId" element={<ScreenshotsGamePage />} />
<Route path="/screenshots/" element={<ScreenshotsHome />} />
</Routes>
</div>
</div>

View File

@@ -0,0 +1,55 @@
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDownIcon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
const Accordion = AccordionPrimitive.Root
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
))
AccordionItem.displayName = "AccordionItem"
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDownIcon className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }

View File

@@ -0,0 +1,48 @@
import { GetGameInfo } from '$app'
import { useApi } from '@/common/api'
import { LoadingContainer } from '@/components/Loader/LoadingContainer'
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/ui/accordion'
import { useParams } from 'react-router-dom'
function useGame(gameId: string) {
return useApi(() => GetGameInfo(gameId), ['game', gameId], {
debug: true,
initialData: {} as never,
})
}
export function GameInfoPage() {
const { gameId } = useParams()
const { data, isFetching } = useGame(gameId!)
const game = data.game ?? {}
return (
<div>
<LoadingContainer loading={isFetching}>
<div className="p-4 pb-3">
<h1 className="text-2xl">{game.name}</h1>
</div>
<div className="flex flex-col gap-4 p-4 pt-0">
<div dangerouslySetInnerHTML={{ __html: game.shortDescription }} />
<div>
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="details">
<AccordionTrigger>See full description</AccordionTrigger>
<AccordionContent>
<div className="p-4" dangerouslySetInnerHTML={{ __html: game.description }} />
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</div>
</LoadingContainer>
</div>
)
}

View File

@@ -14,7 +14,7 @@ function useGames() {
})
}
export function GamesPage() {
export function GamesHomePage() {
const { data, isFetching } = useGames()
return (
<div>

View File

@@ -1,9 +1,8 @@
import { Link, Route, Routes } from 'react-router-dom'
import { Link } from 'react-router-dom'
import { GetScreenshots, NativeOpen } from '$app'
import { useApi } from '@/common/api'
import { LoadingContainer } from '@/components/Loader/LoadingContainer'
import { Button } from '@/components/ui/button'
import ScreenshotsGamePage from './ScreenshotsGamePage'
import { ScreenshotImg } from '@/components/ScreenshotImg/ScreenshotImg'
import { Dialog } from '@/components/ui/dialog'
import { useCallback, useState } from 'react'
@@ -21,16 +20,7 @@ function useScreenshotsDirs() {
}
}
export function ScreenshotsHomePage() {
return (
<Routes>
<Route path="/:gameId" element={<ScreenshotsGamePage />} />
<Route path="/" element={<ScreenshotsHome />} />
</Routes>
)
}
function ScreenshotsHome() {
export function ScreenshotsHome() {
const { screenshots, isFetching } = useScreenshotsDirs()
const [modalIndex, setModalIndex] = useState<number | null>(null)
const [modalScreenshots, setModalScreenshots] = useState<screenshots.ScreenshotEntry[]>([])

View File

@@ -1,3 +1,5 @@
/* eslint-disable no-undef */
/* eslint-disable @typescript-eslint/no-require-imports */
/** @type {import('tailwindcss').Config} */
let colors = {
'great-blue': {
@@ -28,48 +30,70 @@ export default {
foreground: 'hsl(var(--foreground))',
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
foreground: 'hsl(var(--card-foreground))',
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
foreground: 'hsl(var(--popover-foreground))',
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
foreground: 'hsl(var(--primary-foreground))',
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
foreground: 'hsl(var(--secondary-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
foreground: 'hsl(var(--accent-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
foreground: 'hsl(var(--destructive-foreground))',
},
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
chart: {
'1': 'hsl(var(--chart-1))',
'2': 'hsl(var(--chart-2))',
'3': 'hsl(var(--chart-3))',
'4': 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))'
}
1: 'hsl(var(--chart-1))',
2: 'hsl(var(--chart-2))',
3: 'hsl(var(--chart-3))',
4: 'hsl(var(--chart-4))',
5: 'hsl(var(--chart-5))',
},
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
}
sm: 'calc(var(--radius) - 4px)',
},
keyframes: {
'accordion-down': {
from: {
height: '0',
},
to: {
height: 'var(--radix-accordion-content-height)',
},
},
'accordion-up': {
from: {
height: 'var(--radix-accordion-content-height)',
},
to: {
height: '0',
},
},
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
},
},
fontFamily: {
nunito: [
@@ -85,8 +109,8 @@ export default {
'Fira Sans',
'Droid Sans',
'sans-serif',
]
}
],
},
},
plugins: [require("tailwindcss-animate")],
plugins: [require('tailwindcss-animate')],
}

View File

@@ -2,6 +2,8 @@
// This file is automatically generated. DO NOT EDIT
import {main} from '../models';
export function GetGameInfo(arg1:string):Promise<main.GameInfoResponse>;
export function GetGames():Promise<main.GamesResponse>;
export function GetLibraryInfo():Promise<main.LibraryInfo>;

View File

@@ -2,6 +2,10 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function GetGameInfo(arg1) {
return window['go']['main']['App']['GetGameInfo'](arg1);
}
export function GetGames() {
return window['go']['main']['App']['GetGames']();
}

View File

@@ -1,5 +1,37 @@
export namespace main {
export class GameInfoResponse {
error?: string;
game: steam.GameInfo;
static createFrom(source: any = {}) {
return new GameInfoResponse(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.error = source["error"];
this.game = this.convertValues(source["game"], steam.GameInfo);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class GamesResponse {
error?: string;
games: steam.GameInfo[];
@@ -158,6 +190,12 @@ export namespace steam {
id: string;
name: string;
installDir: string;
description: string;
shortDescription: string;
website: string;
backgroundImage: string;
capsuleImage: string;
categories: string[];
static createFrom(source: any = {}) {
return new GameInfo(source);
@@ -168,6 +206,12 @@ export namespace steam {
this.id = source["id"];
this.name = source["name"];
this.installDir = source["installDir"];
this.description = source["description"];
this.shortDescription = source["shortDescription"];
this.website = source["website"];
this.backgroundImage = source["backgroundImage"];
this.capsuleImage = source["capsuleImage"];
this.categories = source["categories"];
}
}

9
go.mod
View File

@@ -1,10 +1,13 @@
module github.com/chenasraf/stimvisor
go 1.21
go 1.22
toolchain go1.23.0
require github.com/wailsapp/wails/v2 v2.9.2
require (
github.com/Goldziher/go-utils v1.8.1
github.com/wailsapp/wails/v2 v2.9.2
)
require (
github.com/bep/debounce v1.2.1 // indirect
@@ -30,7 +33,7 @@ require (
github.com/wailsapp/go-webview2 v1.0.16 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect

6
go.sum
View File

@@ -1,3 +1,5 @@
github.com/Goldziher/go-utils v1.8.1 h1:iBNHw65e46OKcMeVzoBq44Hk0I40OmPIIYsmk2m8JUo=
github.com/Goldziher/go-utils v1.8.1/go.mod h1:2rj4vjpcGJl3kz7ujjuR2NxdzHbo9P8Iikp8Prf6lSo=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -65,8 +67,8 @@ github.com/wailsapp/wails/v2 v2.9.2 h1:Xb5YRTos1w5N7DTMyYegWaGukCP2fIaX9WF21kPPF
github.com/wailsapp/wails/v2 v2.9.2/go.mod h1:uehvlCwJSFcBq7rMCGfk4rxca67QQGsbg5Nm4m9UnBs=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=

View File

@@ -40,7 +40,11 @@ func NewScreenshotsDirFromPath(path string, limit int) ScreenshotCollection {
s := ScreenshotCollection{}
s.Dir = path
s.GameId = filepath.Base(getDir(path, 1))
s.GameName = steam.GetGameName(s.GameId)
info, err := steam.GetGameInfo(s.GameId)
if err != nil {
return s
}
s.GameName = info.Name
s.UserId = filepath.Base(getDir(path, 4))
files, err := dir.Readdir(0)

View File

@@ -10,13 +10,21 @@ import (
"github.com/chenasraf/stimvisor/config"
"github.com/chenasraf/stimvisor/dirs"
"github.com/Goldziher/go-utils/sliceutils"
)
// GameInfo represents the information about a game.
type GameInfo struct {
Id string `json:"id"`
Name string `json:"name"`
InstallDir string `json:"installDir"`
Id string `json:"id"`
Name string `json:"name"`
InstallDir string `json:"installDir"`
Description string `json:"description"`
ShortDescription string `json:"shortDescription"`
Website string `json:"website"`
BackgroundImage string `json:"backgroundImage"`
CapsuleImage string `json:"capsuleImage"`
Categories []string `json:"categories"`
}
// GetGameInfo retrieves the information of a game given its ID.
@@ -25,28 +33,55 @@ func GetGameInfo(gameId string) (GameInfo, error) {
if err != nil {
return GameInfo{}, err
}
gameName := GetGameName(gameId)
raw, err := loadGameInfo(gameId)
if err != nil {
return GameInfo{}, err
}
website := ""
if raw["website"] != nil {
website = raw["website"].(string)
}
categories := []string{}
if raw["categories"] != nil {
categories = sliceutils.Map(
raw["categories"].([]interface{}),
func(v interface{}, i int, l []interface{}) string {
val, ok := v.(map[string]interface{})
if !ok {
return ""
}
return val["description"].(string)
})
}
return GameInfo{
Id: gameId,
Name: gameName,
InstallDir: gameDir,
Id: gameId,
Name: raw["name"].(string),
InstallDir: gameDir,
Description: raw["detailed_description"].(string),
ShortDescription: raw["short_description"].(string),
Website: website,
BackgroundImage: raw["background"].(string),
CapsuleImage: raw["capsule_image"].(string),
Categories: categories,
}, nil
}
// GetGameInfoCacheDir returns the directory path for caching game information.
func GetGameInfoCacheDir() string {
configDir := config.GetConfigDir()
return filepath.Join(configDir, ".cache", "gameinfo")
return filepath.Join(configDir, "cache", "gameinfo")
}
// LoadGameInfo loads the game information from the cache or fetches it if not available.
func LoadGameInfo(gameId string) (map[string]interface{}, error) {
os.MkdirAll(GetGameInfoCacheDir(), 0755)
// loadGameInfo loads the game information from the cache or fetches it if not available.
func loadGameInfo(gameId string) (map[string]interface{}, error) {
info := make(map[string]interface{})
cachePath := filepath.Join(GetGameInfoCacheDir(), gameId+".json")
var err error
if _, err = os.Stat(cachePath); os.IsNotExist(err) {
info, err = FetchGameInfo(gameId)
info, err = fetchGameInfo(gameId)
if err == nil {
return info, nil
}
@@ -64,8 +99,9 @@ func LoadGameInfo(gameId string) (map[string]interface{}, error) {
const STEAM_API_URL = "https://store.steampowered.com/api/appdetails?appids=%s"
// FetchGameInfo fetches the game information from the Steam API and caches it.
func FetchGameInfo(gameId string) (map[string]interface{}, error) {
// fetchGameInfo fetches the game information from the Steam API and caches it.
func fetchGameInfo(gameId string) (map[string]interface{}, error) {
os.MkdirAll(GetGameInfoCacheDir(), 0755)
cachePath := filepath.Join(GetGameInfoCacheDir(), gameId+".json")
url := fmt.Sprintf(STEAM_API_URL, gameId)
fmt.Printf("Fetching game info for %s from %s", gameId, url)
@@ -79,6 +115,9 @@ func FetchGameInfo(gameId string) (map[string]interface{}, error) {
respJson := make(map[string]interface{})
json.Unmarshal(respBytes, &respJson)
if respJson[gameId] == nil {
return map[string]interface{}{}, fmt.Errorf("Failed to fetch game info for %s", gameId)
}
// extract result->gameId->data
respGame := respJson[gameId].(map[string]interface{})
@@ -98,16 +137,3 @@ func FetchGameInfo(gameId string) (map[string]interface{}, error) {
return respGameData, nil
}
// GetGameName retrieves the name of a game given its ID.
func GetGameName(gameId string) string {
os.MkdirAll(GetGameInfoCacheDir(), 0755)
info, err := LoadGameInfo(gameId)
if err != nil {
return gameId
}
if info["name"] == nil {
return gameId
}
return info["name"].(string)
}