feat: add games page + game info fetching

This commit is contained in:
2024-10-18 02:58:11 +03:00
parent d5c78e42f8
commit c4864607d4
16 changed files with 342 additions and 10 deletions

31
app.go
View File

@@ -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)

View File

@@ -1,4 +1,4 @@
package main
package config
import (
"encoding/json"

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -1 +1 @@
b5a6a3f23890ae54b24b78d0237d9543
b1d76f0648fca50e50128421ac094802

View File

@@ -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

View 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>
)
}

View 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
}

View 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>
)
}

View File

@@ -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>

View File

@@ -5,6 +5,12 @@
html {
@apply bg-bg;
cursor: default;
user-select: none;
}
input,
textfield {
user-select: auto;
}
/* body { */

View File

@@ -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>;

View File

@@ -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']();
}

View File

@@ -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;
}
}
}

View File

@@ -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
View 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)
}