mirror of
https://github.com/chenasraf/stimvisor.git
synced 2026-05-17 17:38:11 +00:00
feat: add games page + game info fetching
This commit is contained in:
31
app.go
31
app.go
@@ -5,7 +5,9 @@ import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/chenasraf/stimvisor/config"
|
||||
"github.com/chenasraf/stimvisor/dirs"
|
||||
"github.com/chenasraf/stimvisor/steam"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
@@ -87,8 +89,35 @@ func (a *App) GetScreenshots() ScreenshotsDirs {
|
||||
return ScreenshotsDirs{ScreenshotsDirs: screenshotsDirs}
|
||||
}
|
||||
|
||||
type Games struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
Games []steam.GameInfo `json:"games"`
|
||||
}
|
||||
|
||||
func GamesError(err error) Games {
|
||||
return Games{Error: err.Error()}
|
||||
}
|
||||
|
||||
func (a *App) GetGames() Games {
|
||||
userId, err := dirs.GetUserId()
|
||||
if err != nil {
|
||||
return GamesError(err)
|
||||
}
|
||||
gameDirPaths, err := dirs.GetGameDirectories(userId)
|
||||
if err != nil {
|
||||
return GamesError(err)
|
||||
}
|
||||
games := []steam.GameInfo{}
|
||||
for _, path := range gameDirPaths {
|
||||
gameId := filepath.Base(path)
|
||||
gameInfo := steam.GetGameInfo(gameId)
|
||||
games = append(games, gameInfo)
|
||||
}
|
||||
return Games{Games: games}
|
||||
}
|
||||
|
||||
func (a *App) OnWindowResize() {
|
||||
config := GetConfig()
|
||||
config := config.GetConfig()
|
||||
|
||||
w, h := runtime.WindowGetSize(a.ctx)
|
||||
fmt.Printf("OnWindowResize: %d, %d\n", w, h)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
23
dirs/dirs.go
23
dirs/dirs.go
@@ -3,6 +3,7 @@ package dirs
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
@@ -44,6 +45,11 @@ func GetSteamDirectory() (string, error) {
|
||||
return fmt.Sprintf(format, homedir), nil
|
||||
}
|
||||
|
||||
func GetUserId() (string, error) {
|
||||
userDir, err := GetSteamUserDirectory()
|
||||
return filepath.Base(userDir), err
|
||||
}
|
||||
|
||||
func GetSteamUserDirectory() (string, error) {
|
||||
userDirs, err := GetUsersDirectories()
|
||||
if err != nil {
|
||||
@@ -124,6 +130,15 @@ func GetGameDirectories(userId string) ([]string, error) {
|
||||
return gameDirs, nil
|
||||
}
|
||||
|
||||
func GetGameDirectory(gameId string) (string, error) {
|
||||
userDir, err := GetSteamUserDirectory()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s/%s", userDir, gameId), nil
|
||||
}
|
||||
|
||||
func GetScreenshotsDirs() ([]string, error) {
|
||||
syncDir, err := GetSyncDirectory()
|
||||
if err != nil {
|
||||
@@ -149,3 +164,11 @@ func GetScreenshotsDirs() ([]string, error) {
|
||||
}
|
||||
return dirs, nil
|
||||
}
|
||||
|
||||
func GetScreenshotsDir(gameId string) (string, error) {
|
||||
syncDir, err := GetSyncDirectory()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%s/remote/%s/screenshots", syncDir, gameId), nil
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@ type ScreenshotsDir struct {
|
||||
|
||||
func NewScreenshotsDirFromPath(path string) ScreenshotsDir {
|
||||
dir, err := os.Open(path)
|
||||
if os.IsNotExist(err) {
|
||||
return ScreenshotsDir{}
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
b5a6a3f23890ae54b24b78d0237d9543
|
||||
b1d76f0648fca50e50128421ac094802
|
||||
@@ -6,6 +6,8 @@ import { GetSteamLibraryMeta, OnWindowResize } from '../wailsjs/go/main/App'
|
||||
import { ScreenshotsPage } from './pages/Screenshots/ScreenshotsPage'
|
||||
import { useApi } from './common/api'
|
||||
import { AppContext } from './common/app_context'
|
||||
import { LoadingContainer } from './components/Loader/LoadingContainer'
|
||||
import { GamesPage } from './pages/Games/GamesPage'
|
||||
|
||||
function App() {
|
||||
const [queryClient] = useState(() => new QueryClient())
|
||||
@@ -21,7 +23,8 @@ function App() {
|
||||
<Sidebar className="min-w-64 w-64" />
|
||||
<div className="max-h-screen overflow-y-auto">
|
||||
<Routes>
|
||||
<Route path="/" element={<div />} />
|
||||
<Route path="/" element={<GamesPage />} />
|
||||
<Route path="/games" element={<GamesPage />} />
|
||||
<Route path="/screenshots/*" element={<ScreenshotsPage />} />
|
||||
</Routes>
|
||||
</div>
|
||||
@@ -37,8 +40,11 @@ function AppContextProvider({ children }: React.PropsWithChildren<object>) {
|
||||
initialData: {} as never,
|
||||
debug: true,
|
||||
})
|
||||
if (!meta || isFetching) return <div>Loading...</div>
|
||||
return <AppContext.Provider value={{ meta }}>{children}</AppContext.Provider>
|
||||
return (
|
||||
<LoadingContainer loading={isFetching}>
|
||||
<AppContext.Provider value={{ meta }}>{children}</AppContext.Provider>
|
||||
</LoadingContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
|
||||
10
frontend/src/components/Loader/Loader.tsx
Normal file
10
frontend/src/components/Loader/Loader.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { HtmlProps } from '../../common/types'
|
||||
import { cn } from '../../common/utils'
|
||||
|
||||
export function Loader({ className, ...rest }: HtmlProps<'div'>) {
|
||||
return (
|
||||
<div className={cn('', className)} {...rest}>
|
||||
Loading...
|
||||
</div>
|
||||
)
|
||||
}
|
||||
12
frontend/src/components/Loader/LoadingContainer.tsx
Normal file
12
frontend/src/components/Loader/LoadingContainer.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { HtmlProps } from '../../common/types'
|
||||
import { cn } from '../../common/utils'
|
||||
import { Loader } from './Loader'
|
||||
|
||||
export function LoadingContainer({
|
||||
className,
|
||||
loading,
|
||||
children,
|
||||
...rest
|
||||
}: HtmlProps<'div'> & { loading: boolean }) {
|
||||
return loading ? <Loader className={cn('', className)} {...rest} /> : children
|
||||
}
|
||||
40
frontend/src/pages/Games/GamesPage.tsx
Normal file
40
frontend/src/pages/Games/GamesPage.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { GetGames } from '../../../wailsjs/go/main/App'
|
||||
import { useApi } from '../../common/api'
|
||||
import { useAppContext } from '../../common/app_context'
|
||||
import { cn } from '../../common/utils'
|
||||
import { LoadingContainer } from '../../components/Loader/LoadingContainer'
|
||||
|
||||
function useGames() {
|
||||
return useApi(GetGames, ['games'], {
|
||||
debug: true,
|
||||
initialData: {} as never,
|
||||
})
|
||||
}
|
||||
|
||||
export function GamesPage() {
|
||||
const { meta } = useAppContext()
|
||||
const { data, isFetching } = useGames()
|
||||
console.debug('GamesPage', meta)
|
||||
return (
|
||||
<div className={cn('p-4')}>
|
||||
<h1 className="text-2xl">Games</h1>
|
||||
|
||||
<div>
|
||||
<LoadingContainer loading={isFetching}>
|
||||
{data.games?.map((dir) => (
|
||||
<div key={dir.id}>
|
||||
<h2>{dir.name}</h2>
|
||||
</div>
|
||||
))}
|
||||
</LoadingContainer>
|
||||
</div>
|
||||
|
||||
<details>
|
||||
<summary>Meta</summary>
|
||||
<code>
|
||||
<pre>{JSON.stringify(meta, null, 2)}</pre>
|
||||
</code>
|
||||
</details>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { GetScreenshots } from '../../../wailsjs/go/main/App'
|
||||
import { useApi } from '../../common/api'
|
||||
import { useAppContext } from '../../common/app_context'
|
||||
import { cn } from '../../common/utils'
|
||||
import { LoadingContainer } from '../../components/Loader/LoadingContainer'
|
||||
|
||||
function useScreenshotsDirs() {
|
||||
const { data: screenshots, ...rest } = useApi(GetScreenshots, ['screenshots'], {
|
||||
@@ -23,9 +24,8 @@ export function ScreenshotsPage() {
|
||||
<h1 className="text-2xl">Screenshots</h1>
|
||||
|
||||
<div>
|
||||
{isFetching
|
||||
? 'Loading...'
|
||||
: screenshots.screenshotsDirs.map((dir) => (
|
||||
<LoadingContainer loading={isFetching}>
|
||||
{screenshots.screenshotsDirs?.map((dir) => (
|
||||
<div key={dir.dir}>
|
||||
<h2>{dir.gameId}</h2>
|
||||
<div className="flex items-start gap-4 flex-wrap max-w-full">
|
||||
@@ -35,6 +35,7 @@ export function ScreenshotsPage() {
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</LoadingContainer>
|
||||
</div>
|
||||
|
||||
<details>
|
||||
|
||||
@@ -5,6 +5,12 @@
|
||||
html {
|
||||
@apply bg-bg;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
input,
|
||||
textfield {
|
||||
user-select: auto;
|
||||
}
|
||||
|
||||
/* body { */
|
||||
|
||||
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 GetGames():Promise<main.Games>;
|
||||
|
||||
export function GetScreenshots():Promise<main.ScreenshotsDirs>;
|
||||
|
||||
export function GetSteamLibraryMeta():Promise<main.SteamLibraryMeta>;
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function GetGames() {
|
||||
return window['go']['main']['App']['GetGames']();
|
||||
}
|
||||
|
||||
export function GetScreenshots() {
|
||||
return window['go']['main']['App']['GetScreenshots']();
|
||||
}
|
||||
|
||||
@@ -23,6 +23,38 @@ export namespace dirs {
|
||||
|
||||
export namespace main {
|
||||
|
||||
export class Games {
|
||||
error?: string;
|
||||
games: steam.GameInfo[];
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Games(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.error = source["error"];
|
||||
this.games = this.convertValues(source["games"], 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 ScreenshotsDirs {
|
||||
error?: string;
|
||||
screenshotsDirs: dirs.ScreenshotsDir[];
|
||||
@@ -78,3 +110,44 @@ export namespace main {
|
||||
|
||||
}
|
||||
|
||||
export namespace steam {
|
||||
|
||||
export class GameInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
installDir: string;
|
||||
screenshotsDir: dirs.ScreenshotsDir;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new GameInfo(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.id = source["id"];
|
||||
this.name = source["name"];
|
||||
this.installDir = source["installDir"];
|
||||
this.screenshotsDir = this.convertValues(source["screenshotsDir"], dirs.ScreenshotsDir);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
3
main.go
3
main.go
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"embed"
|
||||
|
||||
"github.com/chenasraf/stimvisor/config"
|
||||
"github.com/wailsapp/wails/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||
@@ -15,7 +16,7 @@ func main() {
|
||||
// Create an instance of the app structure
|
||||
app := NewApp()
|
||||
|
||||
config := GetConfig()
|
||||
config := config.GetConfig()
|
||||
|
||||
// Create application with options
|
||||
err := wails.Run(&options.App{
|
||||
|
||||
122
steam/games.go
Normal file
122
steam/games.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package steam
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/chenasraf/stimvisor/config"
|
||||
"github.com/chenasraf/stimvisor/dirs"
|
||||
)
|
||||
|
||||
type GameInfo struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
InstallDir string `json:"installDir"`
|
||||
ScreenshotsDir dirs.ScreenshotsDir `json:"screenshotsDir"`
|
||||
}
|
||||
|
||||
func GetGameInfo(gameId string) GameInfo {
|
||||
gameDir, err := dirs.GetGameDirectory(gameId)
|
||||
if err != nil {
|
||||
return GameInfo{}
|
||||
}
|
||||
gameName := GetGameName(gameId)
|
||||
screenshotsDirPath, err := dirs.GetScreenshotsDir(gameId)
|
||||
if err != nil {
|
||||
return GameInfo{}
|
||||
}
|
||||
screenshotsDir := dirs.NewScreenshotsDirFromPath(screenshotsDirPath)
|
||||
return GameInfo{
|
||||
Id: gameId,
|
||||
Name: gameName,
|
||||
InstallDir: gameDir,
|
||||
ScreenshotsDir: screenshotsDir,
|
||||
}
|
||||
}
|
||||
|
||||
func GetGameInfoCacheDir() string {
|
||||
configDir := config.GetConfigDir()
|
||||
return filepath.Join(configDir, ".cache", "gameinfo")
|
||||
}
|
||||
|
||||
func LoadGameInfo(gameId string) (map[string]interface{}, error) {
|
||||
os.MkdirAll(GetGameInfoCacheDir(), 0755)
|
||||
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)
|
||||
if err == nil {
|
||||
return info, nil
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return info, err
|
||||
}
|
||||
f, err := os.ReadFile(cachePath)
|
||||
if err != nil {
|
||||
return info, err
|
||||
}
|
||||
json.Unmarshal(f, &info)
|
||||
return info, nil
|
||||
}
|
||||
|
||||
const STEAM_API_URL = "https://store.steampowered.com/api/appdetails?appids=%s"
|
||||
|
||||
func FetchGameInfo(gameId string) (map[string]interface{}, error) {
|
||||
cachePath := filepath.Join(GetGameInfoCacheDir(), gameId+".json")
|
||||
url := fmt.Sprintf(STEAM_API_URL, gameId)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
cacheFile, err := os.Create(cachePath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer cacheFile.Close()
|
||||
|
||||
respBytes, err := io.ReadAll(resp.Body)
|
||||
respJson := make(map[string]interface{})
|
||||
json.Unmarshal(respBytes, &respJson)
|
||||
|
||||
// extract result->gameId->data
|
||||
respGame := respJson[gameId].(map[string]interface{})
|
||||
|
||||
if respGame["success"] == false || respGame["data"] == nil {
|
||||
return map[string]interface{}{}, fmt.Errorf("Failed to fetch game info for %s", gameId)
|
||||
}
|
||||
respGameData := respGame["data"].(map[string]interface{})
|
||||
partBytes, _ := json.Marshal(respGameData)
|
||||
|
||||
cacheFile.WriteString(string(partBytes))
|
||||
|
||||
return respGameData, nil
|
||||
}
|
||||
|
||||
func GetGameName(gameId string) string {
|
||||
os.MkdirAll(GetGameInfoCacheDir(), 0755)
|
||||
info := make(map[string]interface{})
|
||||
cachePath := filepath.Join(GetGameInfoCacheDir(), gameId+".json")
|
||||
if _, err := os.Stat(cachePath); os.IsNotExist(err) {
|
||||
info, err = FetchGameInfo(gameId)
|
||||
if err != nil {
|
||||
return gameId
|
||||
}
|
||||
} else {
|
||||
info, err = LoadGameInfo(gameId)
|
||||
if err != nil {
|
||||
return gameId
|
||||
}
|
||||
}
|
||||
fmt.Printf("INFO: %v\n", info)
|
||||
if info["name"] == nil {
|
||||
return gameId
|
||||
}
|
||||
return info["name"].(string)
|
||||
}
|
||||
Reference in New Issue
Block a user