feat: shadcn, react-icons, update screenshots page

This commit is contained in:
2024-10-21 02:00:29 +03:00
parent 175307aa22
commit 9076bc1c85
24 changed files with 1555 additions and 172 deletions

66
app.go
View File

@@ -6,6 +6,7 @@ import (
"github.com/chenasraf/stimvisor/config"
"github.com/chenasraf/stimvisor/dirs"
"github.com/chenasraf/stimvisor/native"
"github.com/chenasraf/stimvisor/screenshots"
"github.com/chenasraf/stimvisor/steam"
"github.com/wailsapp/wails/v2/pkg/runtime"
@@ -27,11 +28,11 @@ func (a *App) startup(ctx context.Context) {
a.ctx = ctx
}
func SteamLibraryMetaError(err error) SteamLibraryMeta {
return SteamLibraryMeta{Error: err.Error()}
func LibraryMetaInfo(err error) LibraryInfo {
return LibraryInfo{Error: err.Error()}
}
type SteamLibraryMeta struct {
type LibraryInfo struct {
Error string `json:"error,omitempty"`
SteamDir string `json:"steamDir"`
UserDir string `json:"userDir"`
@@ -39,26 +40,26 @@ type SteamLibraryMeta struct {
SyncDir string `json:"syncDir"`
}
func (a *App) GetSteamLibraryMeta() SteamLibraryMeta {
func (a *App) GetLibraryInfo() LibraryInfo {
p, err := dirs.GetSteamDirectory()
if err != nil {
return SteamLibraryMetaError(err)
return LibraryMetaInfo(err)
}
userDir, err := dirs.GetSteamUserDirectory()
if err != nil {
return SteamLibraryMetaError(err)
return LibraryMetaInfo(err)
}
// fmt.Printf("User Dir: %s\n", userDir)
userId := filepath.Base(userDir)
gd, err := dirs.GetGameDirectories(userId)
if err != nil {
return SteamLibraryMetaError(err)
return LibraryMetaInfo(err)
}
syncDir, err := dirs.GetSyncDirectory()
if err != nil {
return SteamLibraryMetaError(err)
return LibraryMetaInfo(err)
}
out := SteamLibraryMeta{
out := LibraryInfo{
SteamDir: p,
GameDirs: gd,
UserDir: userDir,
@@ -68,57 +69,48 @@ func (a *App) GetSteamLibraryMeta() SteamLibraryMeta {
return out
}
type ScreenshotsDirs struct {
Error string `json:"error,omitempty"`
ScreenshotsDirs []screenshots.ScreenshotsDir `json:"screenshotsDirs"`
type ScreenshotCollectionResponse struct {
Error string `json:"error,omitempty"`
ScreenshotCollections []screenshots.ScreenshotCollection `json:"screenshotCollections"`
}
func ScreenshotsDirsError(err error) ScreenshotsDirs {
return ScreenshotsDirs{Error: err.Error()}
func ScreenshotCollectionError(err error) ScreenshotCollectionResponse {
return ScreenshotCollectionResponse{Error: err.Error()}
}
const SHORT_SCREENSHOTS_LIMIT = 5
func (a *App) GetScreenshots() ScreenshotsDirs {
func (a *App) GetScreenshots() ScreenshotCollectionResponse {
screenshotsDirPaths, err := dirs.GetScreenshotsDirs()
if err != nil {
return ScreenshotsDirsError(err)
return ScreenshotCollectionError(err)
}
screenshotsDirs := []screenshots.ScreenshotsDir{}
screenshotsDirs := []screenshots.ScreenshotCollection{}
for _, path := range screenshotsDirPaths {
screenshotsDirs = append(screenshotsDirs, screenshots.NewScreenshotsDirFromPath(path, SHORT_SCREENSHOTS_LIMIT))
}
return ScreenshotsDirs{ScreenshotsDirs: screenshotsDirs}
return ScreenshotCollectionResponse{ScreenshotCollections: screenshotsDirs}
}
type ScreenshotsDir struct {
Error string `json:"error,omitempty"`
ScreenshotsDir screenshots.ScreenshotsDir `json:"screenshotsDir"`
}
func ScreenshotsDirError(err error) ScreenshotsDir {
return ScreenshotsDir{Error: err.Error()}
}
func (a *App) GetScreenshotsForGame(gameId string) ScreenshotsDir {
func (a *App) GetScreenshotsForGame(gameId string) ScreenshotCollectionResponse {
screenshotsDirPath, err := dirs.GetScreenshotsDir(gameId)
if err != nil {
return ScreenshotsDirError(err)
return ScreenshotCollectionError(err)
}
screenshotsDir := screenshots.NewScreenshotsDirFromPath(screenshotsDirPath, 0)
return ScreenshotsDir{ScreenshotsDir: screenshotsDir}
return ScreenshotCollectionResponse{ScreenshotCollections: []screenshots.ScreenshotCollection{screenshotsDir}}
}
type Games struct {
type GamesResponse struct {
Error string `json:"error,omitempty"`
Games []steam.GameInfo `json:"games"`
}
func GamesError(err error) Games {
return Games{Error: err.Error()}
func GamesError(err error) GamesResponse {
return GamesResponse{Error: err.Error()}
}
func (a *App) GetGames() Games {
func (a *App) GetGames() GamesResponse {
userId, err := dirs.GetUserId()
if err != nil {
return GamesError(err)
@@ -136,7 +128,7 @@ func (a *App) GetGames() Games {
}
games = append(games, gameInfo)
}
return Games{Games: games}
return GamesResponse{Games: games}
}
func (a *App) OnWindowResize() {
@@ -149,3 +141,7 @@ func (a *App) OnWindowResize() {
config.Save()
}
func (a *App) NativeOpen(path string) error {
return native.NativeOpen(path)
}

20
frontend/components.json Normal file
View File

@@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/style.css",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}

View File

@@ -1,11 +1,26 @@
import eslint from '@eslint/js'
import tseslint from 'typescript-eslint'
import reactPlugin from 'eslint-plugin-react'
import hooksPlugin from 'eslint-plugin-react-hooks'
export default [
...tseslint.config(eslint.configs.recommended, ...tseslint.configs.recommended),
...tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
reactPlugin.configs.flat.recommended,
),
{
plugins: {
'react-hooks': hooksPlugin,
},
rules: {
'react/react-in-jsx-scope': 'off',
...hooksPlugin.configs.recommended.rules,
},
ignores: ['*.test.tsx'],
},
{
rules: {
'no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
'@typescript-eslint/no-unused-vars': [
'warn',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },

View File

@@ -5,7 +5,7 @@
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<title>stimvisor</title>
</head>
<body class="font-nunito">
<body class="font-nunito dark">
<div id="root"></div>
<script src="./src/main.tsx" type="module"></script>
</body>

View File

@@ -11,16 +11,24 @@
},
"dependencies": {
"@fontsource-variable/nunito": "^5.1.0",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-slot": "^1.1.0",
"@tanstack/react-query": "^5.59.15",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"eslint-plugin-react": "^7.37.1",
"lucide-react": "^0.453.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^5.3.0",
"react-router-dom": "^6.27.0",
"tailwind-merge": "^2.5.4"
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@eslint/js": "^9.12.0",
"@tauri-apps/cli": "^2.0.3",
"@types/node": "^22.7.7",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.2",

View File

@@ -1 +1 @@
b1d76f0648fca50e50128421ac094802
0d0c98b49fd99dd2c835289a5056e043

1063
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { Sidebar } from './components/Sidebar/Sidebar'
import React, { useEffect, useState } from 'react'
import { HashRouter, Route, Routes } from 'react-router-dom'
import { GetSteamLibraryMeta, OnWindowResize } from '../wailsjs/go/main/App'
import { GetLibraryInfo, OnWindowResize } from '../wailsjs/go/main/App'
import { ScreenshotsPage } from './pages/Screenshots/ScreenshotsPage'
import { useApi } from './common/api'
import { AppContext } from './common/app_context'
@@ -21,7 +21,7 @@ function App() {
<AppContextProvider>
<div id="App" className="min-h-screen flex">
<Sidebar className="min-w-64 w-64" />
<div className="max-h-screen overflow-y-auto">
<div className="max-h-screen overflow-y-auto w-full">
<Routes>
<Route path="/" element={<GamesPage />} />
<Route path="/games" element={<GamesPage />} />
@@ -36,7 +36,7 @@ function App() {
}
function AppContextProvider({ children }: React.PropsWithChildren<object>) {
const { data: meta, isFetching } = useApi(GetSteamLibraryMeta, ['meta'], {
const { data: meta, isFetching } = useApi(GetLibraryInfo, ['meta'], {
initialData: {} as never,
debug: true,
})

View File

@@ -4,7 +4,7 @@ import { main } from '../../wailsjs/go/models'
export const AppContext = createContext<AppContext>({ meta: {} as never })
export type AppContext = {
meta: main.SteamLibraryMeta
meta: main.LibraryInfo
}
export function useAppContext() {

View File

@@ -1,33 +1,61 @@
import { HtmlProps } from '../../common/types'
import React from 'react'
import React, { useCallback, useMemo } from 'react'
import { cn } from '../../common/utils'
import { Link } from 'react-router-dom'
import { Link, useLocation } from 'react-router-dom'
import { cva } from 'class-variance-authority'
export function Sidebar({ className, ...rest }: HtmlProps<'div'>) {
return (
<div className={cn('py-3 bg-bg-800 h-screen overflow-y-auto', className)} {...rest}>
<div className={cn('py-3 bg-bg-950 h-screen overflow-y-auto', className)} {...rest}>
<ul>
<ListItem label="Games" to="/games" />
<ListItem label="Screenshots" to="/screenshots" />
<ListItem
label="Screenshots"
to="/screenshots"
match={(p) => p.startsWith('/screenshots')}
/>
<ListItem label="Save Data" to="/saves" />
</ul>
</div>
)
}
const liStyles = cva(
'min-h-10 flex items-center justify-start py-3 px-6 cursor-pointer transition-colors',
{
variants: {
active: {
true: 'bg-bg-800 hover:bg-bg-800',
false: 'hover:bg-bg-800',
},
},
},
)
function ListItem({
children,
label,
to,
match = true,
...rest
}: HtmlProps<'div'> & { label: React.ReactNode; to: string }) {
// TODO should be siblings under a fragment, use li instead of div for props ^
}: HtmlProps<'div'> & {
label: React.ReactNode
to: string
match?: boolean | string | ((_path: string) => boolean)
}) {
const path = useLocation().pathname
const defaultMatchFn = useCallback(
(path: string) => path === (match === true ? to : match),
[match, to],
)
const active = useMemo(() => {
const fnMatch = typeof match === 'function' ? match : defaultMatchFn
return fnMatch(path)
}, [defaultMatchFn, match, path])
const cls = liStyles({ active })
return (
<Link to={to}>
<div
className="min-h-10 flex items-center justify-start py-3 px-6 hover:bg-bg-700 cursor-pointer"
{...rest}
>
<div className={cls} {...rest}>
{label}
</div>
{children ? <ul>{children}</ul> : null}

View File

@@ -0,0 +1,57 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@@ -2,7 +2,7 @@ import React from 'react'
import { createRoot } from 'react-dom/client'
import '@fontsource-variable/nunito'
// import './fonts.css'
import './reset.css'
// import './reset.css'
import './style.css'
import App from './App'

View File

@@ -1,9 +1,11 @@
import { Link, Route, Routes, useParams } from 'react-router-dom'
import { GetScreenshots, GetScreenshotsForGame } from '../../../wailsjs/go/main/App'
import { GetScreenshots, GetScreenshotsForGame, NativeOpen } 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'
import { Button } from '@/components/ui/button'
import { FaAngleLeft } from 'react-icons/fa6'
function useScreenshotsDir(gameId: string) {
const { data: screenshots, ...rest } = useApi(
@@ -50,17 +52,27 @@ function ScreenshotsHome() {
<div>
<LoadingContainer loading={isFetching}>
<div className="flex flex-col gap-8">
{screenshots.screenshotsDirs?.map((dir) => (
<div key={dir.dir}>
{screenshots.screenshotCollections?.map((dir) => (
<div key={dir.dir} className="flex flex-col gap-4">
<div className="flex items-center gap-2 justify-between">
<h2 className="text-xl mb-2">{dir.gameName}</h2>
<div>
<Link to={`/screenshots/${dir.gameId}`}>View All ({dir.totalCount})</Link>
<h2 className="text-xl">{dir.gameName}</h2>
<div className="flex items-center gap-2">
<Button variant="outline" asChild>
<Link to={`/screenshots/${dir.gameId}`}>View All ({dir.totalCount})</Link>
</Button>
<Button variant="outline" onClick={() => NativeOpen(dir.dir)}>
Browse Folder
</Button>
</div>
</div>
<div className="flex items-start gap-4 flex-wrap max-w-full">
{dir.screenshots.map((file) => (
<img className="max-w-64 rounded-md" key={file} src={file} alt={file} />
<img
className="max-w-64 rounded-md"
key={file.path}
src={file.base64}
alt={file.name}
/>
))}
</div>
</div>
@@ -74,24 +86,33 @@ function ScreenshotsHome() {
function ScreenshotsGamePage() {
const { gameId } = useParams()
const { screenshots, isFetching } = useScreenshotsDir(gameId!)
console.debug('ScreenshotsGamePage', gameId, screenshots)
const dir = screenshots.screenshotsDir ?? { screenshots: [] }
const [dir] = screenshots.screenshotCollections ?? [{ screenshots: [] }]
return (
<div className={cn('p-4')}>
<h1 className="text-2xl mb-4">Screenshots</h1>
<div className="flex flex-col gap-2">
<div>
<Button variant="outline" size="sm" asChild>
<Link to="/screenshots">
<FaAngleLeft />
Back
</Link>
</Button>
</div>
<h1 className="text-2xl mb-4">Screenshots for {dir.gameName}</h1>
</div>
<div>
<LoadingContainer loading={isFetching}>
<div className="flex flex-col gap-8">
<div key={dir.dir}>
<div className="flex items-center gap-2 justify-between">
<h2 className="text-xl mb-2">{dir.gameName}</h2>
</div>
<div className="flex items-start gap-4 flex-wrap max-w-full">
{dir.screenshots.map((file) => (
<img className="max-w-64 rounded-md" key={file} src={file} alt={file} />
))}
</div>
<div className="flex items-start gap-4 flex-wrap max-w-full">
{dir.screenshots.map((file) => (
<img
className="max-w-64 rounded-md"
key={file.path}
src={file.base64}
alt={file.name}
/>
))}
</div>
</div>
</LoadingContainer>

View File

@@ -21,3 +21,70 @@ html,
body {
@apply text-gray-400;
}
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 221.2 83.2% 53.3%;
--radius: 0.5rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 224.3 76.3% 48%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -18,12 +18,58 @@ colors.bg = colors['great-blue']
colors.bg.DEFAULT = '#0C102E'
export default {
darkMode: ['class'],
content: ['index.html', 'src/**/*.{js,jsx,ts,tsx}'],
theme: {
extend: {
colors: {
...colors,
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
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))'
}
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
}
},
fontFamily: {
nunito: [
@@ -39,8 +85,8 @@ export default {
'Fira Sans',
'Droid Sans',
'sans-serif',
],
},
]
}
},
plugins: [],
plugins: [require("tailwindcss-animate")],
}

View File

@@ -7,6 +7,12 @@
"DOM.Iterable",
"ESNext"
],
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
},
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,

View File

@@ -3,7 +3,13 @@
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
"allowSyntheticDefaultImports": true,
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
}
},
"include": [
"vite.config.ts"

View File

@@ -1,9 +1,15 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
server: {
hmr: {
host: 'localhost',

View File

@@ -2,12 +2,14 @@
// This file is automatically generated. DO NOT EDIT
import {main} from '../models';
export function GetGames():Promise<main.Games>;
export function GetGames():Promise<main.GamesResponse>;
export function GetScreenshots():Promise<main.ScreenshotsDirs>;
export function GetLibraryInfo():Promise<main.LibraryInfo>;
export function GetScreenshotsForGame(arg1:string):Promise<main.ScreenshotsDir>;
export function GetScreenshots():Promise<main.ScreenshotCollectionResponse>;
export function GetSteamLibraryMeta():Promise<main.SteamLibraryMeta>;
export function GetScreenshotsForGame(arg1:string):Promise<main.ScreenshotCollectionResponse>;
export function NativeOpen(arg1:string):Promise<void>;
export function OnWindowResize():Promise<void>;

View File

@@ -6,6 +6,10 @@ export function GetGames() {
return window['go']['main']['App']['GetGames']();
}
export function GetLibraryInfo() {
return window['go']['main']['App']['GetLibraryInfo']();
}
export function GetScreenshots() {
return window['go']['main']['App']['GetScreenshots']();
}
@@ -14,8 +18,8 @@ export function GetScreenshotsForGame(arg1) {
return window['go']['main']['App']['GetScreenshotsForGame'](arg1);
}
export function GetSteamLibraryMeta() {
return window['go']['main']['App']['GetSteamLibraryMeta']();
export function NativeOpen(arg1) {
return window['go']['main']['App']['NativeOpen'](arg1);
}
export function OnWindowResize() {

View File

@@ -1,11 +1,11 @@
export namespace main {
export class Games {
export class GamesResponse {
error?: string;
games: steam.GameInfo[];
static createFrom(source: any = {}) {
return new Games(source);
return new GamesResponse(source);
}
constructor(source: any = {}) {
@@ -32,71 +32,7 @@ export namespace main {
return a;
}
}
export class ScreenshotsDir {
error?: string;
screenshotsDir: screenshots.ScreenshotsDir;
static createFrom(source: any = {}) {
return new ScreenshotsDir(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.error = source["error"];
this.screenshotsDir = this.convertValues(source["screenshotsDir"], screenshots.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;
}
}
export class ScreenshotsDirs {
error?: string;
screenshotsDirs: screenshots.ScreenshotsDir[];
static createFrom(source: any = {}) {
return new ScreenshotsDirs(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.error = source["error"];
this.screenshotsDirs = this.convertValues(source["screenshotsDirs"], screenshots.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;
}
}
export class SteamLibraryMeta {
export class LibraryInfo {
error?: string;
steamDir: string;
userDir: string;
@@ -104,7 +40,7 @@ export namespace main {
syncDir: string;
static createFrom(source: any = {}) {
return new SteamLibraryMeta(source);
return new LibraryInfo(source);
}
constructor(source: any = {}) {
@@ -116,21 +52,73 @@ export namespace main {
this.syncDir = source["syncDir"];
}
}
export class ScreenshotCollectionResponse {
error?: string;
screenshotCollections: screenshots.ScreenshotCollection[];
static createFrom(source: any = {}) {
return new ScreenshotCollectionResponse(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.error = source["error"];
this.screenshotCollections = this.convertValues(source["screenshotCollections"], screenshots.ScreenshotCollection);
}
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 namespace screenshots {
export class ScreenshotsDir {
export class ScreenshotEntry {
dir: string;
path: string;
name: string;
base64: string;
mimeType: string;
static createFrom(source: any = {}) {
return new ScreenshotEntry(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.dir = source["dir"];
this.path = source["path"];
this.name = source["name"];
this.base64 = source["base64"];
this.mimeType = source["mimeType"];
}
}
export class ScreenshotCollection {
dir: string;
userId: string;
gameId: string;
gameName: string;
screenshots: string[];
screenshots: ScreenshotEntry[];
totalCount: number;
static createFrom(source: any = {}) {
return new ScreenshotsDir(source);
return new ScreenshotCollection(source);
}
constructor(source: any = {}) {
@@ -139,9 +127,27 @@ export namespace screenshots {
this.userId = source["userId"];
this.gameId = source["gameId"];
this.gameName = source["gameName"];
this.screenshots = source["screenshots"];
this.screenshots = this.convertValues(source["screenshots"], ScreenshotEntry);
this.totalCount = source["totalCount"];
}
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;
}
}
}

20
native/native.go Normal file
View File

@@ -0,0 +1,20 @@
package native
import (
"fmt"
"os/exec"
"runtime"
)
func NativeOpen(path string) error {
switch runtime.GOOS {
case "windows":
return exec.Command("cmd", "/c", "start", path).Start()
case "darwin":
return exec.Command("open", path).Start()
case "linux":
return exec.Command("xdg-open", path).Start()
default:
return fmt.Errorf("Unsupported platform: %s", runtime.GOOS)
}
}

View File

@@ -11,25 +11,33 @@ import (
)
// screenshots: /Users/chen/Library/Application\ Support/Steam/userdata/USER_ID/760/remote/GAME_ID/screenshots
type ScreenshotsDir struct {
Dir string `json:"dir"`
UserId string `json:"userId"`
GameId string `json:"gameId"`
GameName string `json:"gameName"`
Screenshots []string `json:"screenshots"`
TotalCount int `json:"totalCount"`
type ScreenshotCollection struct {
Dir string `json:"dir"`
UserId string `json:"userId"`
GameId string `json:"gameId"`
GameName string `json:"gameName"`
Screenshots []ScreenshotEntry `json:"screenshots"`
TotalCount int `json:"totalCount"`
}
func NewScreenshotsDirFromPath(path string, limit int) ScreenshotsDir {
type ScreenshotEntry struct {
Dir string `json:"dir"`
Path string `json:"path"`
Name string `json:"name"`
Base64 string `json:"base64"`
MimeType string `json:"mimeType"`
}
func NewScreenshotsDirFromPath(path string, limit int) ScreenshotCollection {
dir, err := os.Open(path)
if os.IsNotExist(err) {
return ScreenshotsDir{}
return ScreenshotCollection{}
}
if err != nil {
panic(err)
}
defer dir.Close()
s := ScreenshotsDir{}
s := ScreenshotCollection{}
s.Dir = path
s.GameId = filepath.Base(getDir(path, 1))
s.GameName = steam.GetGameName(s.GameId)
@@ -70,7 +78,15 @@ func NewScreenshotsDirFromPath(path string, limit int) ScreenshotsDir {
continue
}
b64 += base64.StdEncoding.EncodeToString(bytes)
s.Screenshots = append(s.Screenshots, b64)
entry := ScreenshotEntry{
Dir: filepath.Dir(path),
Path: path,
Name: f.Name(),
Base64: b64,
MimeType: mimeType,
}
s.Screenshots = append(s.Screenshots, entry)
}
return s
}