From c4864607d4183af443c770c84dc6a630c1d071d2 Mon Sep 17 00:00:00 2001 From: Chen Asraf Date: Fri, 18 Oct 2024 02:58:11 +0300 Subject: [PATCH] feat: add games page + game info fetching --- app.go | 31 ++++- config.go => config/config.go | 2 +- dirs/dirs.go | 23 ++++ dirs/screenshots.go | 3 + frontend/package.json.md5 | 2 +- frontend/src/App.tsx | 12 +- frontend/src/components/Loader/Loader.tsx | 10 ++ .../components/Loader/LoadingContainer.tsx | 12 ++ frontend/src/pages/Games/GamesPage.tsx | 40 ++++++ .../src/pages/Screenshots/ScreenshotsPage.tsx | 7 +- frontend/src/style.css | 6 + frontend/wailsjs/go/main/App.d.ts | 2 + frontend/wailsjs/go/main/App.js | 4 + frontend/wailsjs/go/models.ts | 73 +++++++++++ main.go | 3 +- steam/games.go | 122 ++++++++++++++++++ 16 files changed, 342 insertions(+), 10 deletions(-) rename config.go => config/config.go (99%) create mode 100644 frontend/src/components/Loader/Loader.tsx create mode 100644 frontend/src/components/Loader/LoadingContainer.tsx create mode 100644 frontend/src/pages/Games/GamesPage.tsx create mode 100644 steam/games.go diff --git a/app.go b/app.go index e9bfb5b..d74a3a1 100644 --- a/app.go +++ b/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) diff --git a/config.go b/config/config.go similarity index 99% rename from config.go rename to config/config.go index d5ab823..b27d239 100644 --- a/config.go +++ b/config/config.go @@ -1,4 +1,4 @@ -package main +package config import ( "encoding/json" diff --git a/dirs/dirs.go b/dirs/dirs.go index 05bb861..1492532 100644 --- a/dirs/dirs.go +++ b/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 +} diff --git a/dirs/screenshots.go b/dirs/screenshots.go index b2d9618..f6d17b3 100644 --- a/dirs/screenshots.go +++ b/dirs/screenshots.go @@ -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) } diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index add0558..b237ea9 100755 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -b5a6a3f23890ae54b24b78d0237d9543 \ No newline at end of file +b1d76f0648fca50e50128421ac094802 \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 89c94ec..2ad10b0 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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() {
- } /> + } /> + } /> } />
@@ -37,8 +40,11 @@ function AppContextProvider({ children }: React.PropsWithChildren) { initialData: {} as never, debug: true, }) - if (!meta || isFetching) return
Loading...
- return {children} + return ( + + {children} + + ) } export default App diff --git a/frontend/src/components/Loader/Loader.tsx b/frontend/src/components/Loader/Loader.tsx new file mode 100644 index 0000000..87f4f19 --- /dev/null +++ b/frontend/src/components/Loader/Loader.tsx @@ -0,0 +1,10 @@ +import { HtmlProps } from '../../common/types' +import { cn } from '../../common/utils' + +export function Loader({ className, ...rest }: HtmlProps<'div'>) { + return ( +
+ Loading... +
+ ) +} diff --git a/frontend/src/components/Loader/LoadingContainer.tsx b/frontend/src/components/Loader/LoadingContainer.tsx new file mode 100644 index 0000000..a1ab7fb --- /dev/null +++ b/frontend/src/components/Loader/LoadingContainer.tsx @@ -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 ? : children +} diff --git a/frontend/src/pages/Games/GamesPage.tsx b/frontend/src/pages/Games/GamesPage.tsx new file mode 100644 index 0000000..3496069 --- /dev/null +++ b/frontend/src/pages/Games/GamesPage.tsx @@ -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 ( +
+

Games

+ +
+ + {data.games?.map((dir) => ( +
+

{dir.name}

+
+ ))} +
+
+ +
+ Meta + +
{JSON.stringify(meta, null, 2)}
+
+
+
+ ) +} diff --git a/frontend/src/pages/Screenshots/ScreenshotsPage.tsx b/frontend/src/pages/Screenshots/ScreenshotsPage.tsx index e477c85..bebe381 100644 --- a/frontend/src/pages/Screenshots/ScreenshotsPage.tsx +++ b/frontend/src/pages/Screenshots/ScreenshotsPage.tsx @@ -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() {

Screenshots

- {isFetching - ? 'Loading...' - : screenshots.screenshotsDirs.map((dir) => ( + + {screenshots.screenshotsDirs?.map((dir) => (

{dir.gameId}

@@ -35,6 +35,7 @@ export function ScreenshotsPage() {
))} +
diff --git a/frontend/src/style.css b/frontend/src/style.css index 72ebeac..ee8ab02 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -5,6 +5,12 @@ html { @apply bg-bg; cursor: default; + user-select: none; +} + +input, +textfield { + user-select: auto; } /* body { */ diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts index 2e05a0a..e250cd3 100755 --- a/frontend/wailsjs/go/main/App.d.ts +++ b/frontend/wailsjs/go/main/App.d.ts @@ -2,6 +2,8 @@ // This file is automatically generated. DO NOT EDIT import {main} from '../models'; +export function GetGames():Promise; + export function GetScreenshots():Promise; export function GetSteamLibraryMeta():Promise; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js index f60390a..3f585d8 100755 --- a/frontend/wailsjs/go/main/App.js +++ b/frontend/wailsjs/go/main/App.js @@ -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'](); } diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index 125699d..5ddb5f7 100755 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -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; + } + } + +} + diff --git a/main.go b/main.go index ea36c91..83b3b60 100644 --- a/main.go +++ b/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{ diff --git a/steam/games.go b/steam/games.go new file mode 100644 index 0000000..70edfcc --- /dev/null +++ b/steam/games.go @@ -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) +}