mirror of
https://github.com/chenasraf/stimvisor.git
synced 2026-05-17 17:38:11 +00:00
feat: shadcn, react-icons, update screenshots page
This commit is contained in:
66
app.go
66
app.go
@@ -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
20
frontend/components.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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: '^_' },
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1 +1 @@
|
||||
b1d76f0648fca50e50128421ac094802
|
||||
0d0c98b49fd99dd2c835289a5056e043
|
||||
1063
frontend/pnpm-lock.yaml
generated
1063
frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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}
|
||||
|
||||
57
frontend/src/components/ui/button.tsx
Normal file
57
frontend/src/components/ui/button.tsx
Normal 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 }
|
||||
6
frontend/src/lib/utils.ts
Normal file
6
frontend/src/lib/utils.ts
Normal 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))
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")],
|
||||
}
|
||||
|
||||
@@ -7,6 +7,12 @@
|
||||
"DOM.Iterable",
|
||||
"ESNext"
|
||||
],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
},
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
|
||||
@@ -3,7 +3,13 @@
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts"
|
||||
|
||||
@@ -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',
|
||||
|
||||
10
frontend/wailsjs/go/main/App.d.ts
vendored
10
frontend/wailsjs/go/main/App.d.ts
vendored
@@ -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>;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
20
native/native.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user