mirror of
https://github.com/chenasraf/stimvisor.git
synced 2026-05-17 17:38:11 +00:00
feat: game info page
This commit is contained in:
17
app.go
17
app.go
@@ -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()
|
||||
|
||||
|
||||
@@ -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
117
frontend/pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
55
frontend/src/components/ui/accordion.tsx
Normal file
55
frontend/src/components/ui/accordion.tsx
Normal 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 }
|
||||
48
frontend/src/pages/Games/GameInfoPage.tsx
Normal file
48
frontend/src/pages/Games/GameInfoPage.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -14,7 +14,7 @@ function useGames() {
|
||||
})
|
||||
}
|
||||
|
||||
export function GamesPage() {
|
||||
export function GamesHomePage() {
|
||||
const { data, isFetching } = useGames()
|
||||
return (
|
||||
<div>
|
||||
@@ -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[]>([])
|
||||
|
||||
@@ -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')],
|
||||
}
|
||||
|
||||
2
frontend/wailsjs/go/main/App.d.ts
vendored
2
frontend/wailsjs/go/main/App.d.ts
vendored
@@ -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>;
|
||||
|
||||
@@ -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']();
|
||||
}
|
||||
|
||||
@@ -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
9
go.mod
@@ -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
6
go.sum
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user