feat: nunito, wip steam lib meta data

This commit is contained in:
2024-10-17 14:25:40 +03:00
parent c023cc3790
commit c482c39611
31 changed files with 447 additions and 260 deletions

54
app.go
View File

@@ -2,7 +2,8 @@ package main
import (
"context"
"encoding/json"
"fmt"
"path/filepath"
)
// App struct
@@ -21,22 +22,49 @@ func (a *App) startup(ctx context.Context) {
a.ctx = ctx
}
func JsonResponse(data interface{}, err error) string {
if err != nil {
res, _ := json.Marshal(map[string]interface{}{"error": err.Error()})
return string(res)
}
res, _ := json.Marshal(data)
return string(res)
func WrapError(err error) SteamLibraryMeta {
return SteamLibraryMeta{Error: err.Error()}
}
func (a *App) GetScreenshotsDirs() string {
type SteamLibraryMeta struct {
Error string `json:"error,omitempty"`
SteamDir string `json:"steamDir"`
UserDir string `json:"userDir"`
GameDirs []string `json:"gameDirs"`
ScreenshotsDirs []string `json:"screenshotsDirs"`
SyncDir string `json:"syncDir"`
}
func (a *App) GetSteamLibraryMeta() SteamLibraryMeta {
p, err := GetSteamDirectory()
if err != nil {
return JsonResponse(nil, err)
return WrapError(err)
}
userDir, err := GetSteamUserDirectory()
if err != nil {
return WrapError(err)
}
fmt.Printf("User Dir: %s\n", userDir)
userId := filepath.Base(userDir)
gd, err := GetGameDirectories(userId)
if err != nil {
return WrapError(err)
}
syncDir, err := GetSyncDirectory()
if err != nil {
return WrapError(err)
}
screenshotsDirs, err := GetScreenshotsDirs()
if err != nil {
return WrapError(err)
}
out := SteamLibraryMeta{
SteamDir: p,
GameDirs: gd,
UserDir: userDir,
SyncDir: syncDir,
ScreenshotsDirs: screenshotsDirs,
}
out := make(map[string]interface{})
out["steamDir"] = p
return JsonResponse(out, nil)
return out
}

85
dirs.go
View File

@@ -4,6 +4,8 @@ import (
"fmt"
"os"
"runtime"
"slices"
"strconv"
)
const (
@@ -42,24 +44,30 @@ func GetSteamDirectory() (string, error) {
return fmt.Sprintf(format, homedir), nil
}
func GetSteamUserdataDirectory() (string, error) {
steamDir, err := GetSteamDirectory()
func GetSteamUserDirectory() (string, error) {
userDirs, err := GetUsersDirectories()
if err != nil {
return "", err
}
return fmt.Sprintf("%s/userdata", steamDir), nil
if len(userDirs) == 0 {
return "", fmt.Errorf("Could not find any Steam users")
}
// TODO multiple users
return userDirs[0], nil
}
func GetSyncDirectory() (string, error) {
dataDir, err := GetSteamUserdataDirectory()
userDir, err := GetSteamUserDirectory()
if err != nil {
return "", err
}
return fmt.Sprintf("%s/760", dataDir), nil
return fmt.Sprintf("%s/760", userDir), nil
}
var STEAM_INTERNAL_IDS = []string{"7", "760"}
// screenshots: /Users/chen/Library/Application\ Support/Steam/userdata/37889173/760/remote/1086940/screenshots
func GetGameDirectories() ([]string, error) {
func GetUsersDirectories() ([]string, error) {
steamDir, err := GetSteamDirectory()
if err != nil {
return nil, err
@@ -69,13 +77,76 @@ func GetGameDirectories() ([]string, error) {
if err != nil {
return nil, err
}
var gameDirs []string
var userDirs []string
for _, entry := range entries {
if !entry.IsDir() {
continue
}
gameDir := fmt.Sprintf("%s/%s", steamUserData, entry.Name())
userDirs = append(userDirs, gameDir)
}
return userDirs, nil
}
func GetUserDirectory(userId string) (string, error) {
steamDir, err := GetSteamDirectory()
if err != nil {
return "", err
}
fmt.Printf("Get User Dir: %s/userdata/%s\n", steamDir, userId)
return fmt.Sprintf("%s/userdata/%s", steamDir, userId), nil
}
func GetGameDirectories(userId string) ([]string, error) {
userDir, err := GetUserDirectory(userId)
if err != nil {
return nil, err
}
entries, err := os.ReadDir(userDir)
if err != nil {
return nil, err
}
gameDirs := []string{}
for _, entry := range entries {
if !entry.IsDir() {
continue
}
if slices.Contains(STEAM_INTERNAL_IDS, entry.Name()) {
continue
}
if _, err := strconv.Atoi(entry.Name()); err != nil {
continue
}
gameDir := fmt.Sprintf("%s/%s", userDir, entry.Name())
gameDirs = append(gameDirs, gameDir)
}
return gameDirs, nil
}
func GetScreenshotsDirs() ([]string, error) {
syncDir, err := GetSyncDirectory()
if err != nil {
return nil, err
}
var dirs []string
remoteDir := fmt.Sprintf("%s/remote", syncDir)
entries, err := os.ReadDir(remoteDir)
if err != nil {
return nil, err
}
for _, entry := range entries {
fmt.Printf("Entry: %s\n", entry.Name())
if !entry.IsDir() {
continue
}
scrDir := fmt.Sprintf("%s/%s/screenshots", remoteDir, entry.Name())
fmt.Printf("Checking: %s\n", scrDir)
if _, err := os.Stat(scrDir); os.IsNotExist(err) {
continue
}
dirs = append(dirs, scrDir)
}
return dirs, nil
}

View File

@@ -1,13 +1,12 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<head>
<meta charset="UTF-8" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<title>stimvisor</title>
</head>
<body>
<div id="root"></div>
<script src="./src/main.tsx" type="module"></script>
</body>
</head>
<body class="font-nunito">
<div id="root"></div>
<script src="./src/main.tsx" type="module"></script>
</body>
</html>

View File

@@ -11,8 +11,10 @@
"dependencies": {
"@fontsource-variable/nunito": "^5.1.0",
"@tanstack/react-query": "^5.59.15",
"clsx": "^2.1.1",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react-dom": "^18.3.1",
"tailwind-merge": "^2.5.4"
},
"devDependencies": {
"@eslint/js": "^9.12.0",

View File

@@ -1 +1 @@
1e8656145625684f371b8ed4aff2036d
a2816a52857b069ddfc7f6d2622f2c9d

View File

@@ -14,12 +14,18 @@ importers:
'@tanstack/react-query':
specifier: ^5.59.15
version: 5.59.15(react@18.3.1)
clsx:
specifier: ^2.1.1
version: 2.1.1
react:
specifier: ^18.3.1
version: 18.3.1
react-dom:
specifier: ^18.3.1
version: 18.3.1(react@18.3.1)
tailwind-merge:
specifier: ^2.5.4
version: 2.5.4
devDependencies:
'@eslint/js':
specifier: ^9.12.0
@@ -725,6 +731,10 @@ packages:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
color-convert@1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
@@ -1313,6 +1323,9 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
tailwind-merge@2.5.4:
resolution: {integrity: sha512-0q8cfZHMu9nuYP/b5Shb7Y7Sh1B7Nnl5GqNr1U+n2p6+mybvRtayrQ+0042Z5byvTA8ihjlP8Odo8/VnHbZu4Q==}
tailwindcss@3.4.14:
resolution: {integrity: sha512-IcSvOcTRcUtQQ7ILQL5quRDg7Xs93PdJEk1ZLbhhvJc7uj/OAhYOnruEiwnGgBvUtaUAJ8/mhSw1o8L2jCiENA==}
engines: {node: '>=14.0.0'}
@@ -2057,6 +2070,8 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
clsx@2.1.1: {}
color-convert@1.9.3:
dependencies:
color-name: 1.1.3
@@ -2618,6 +2633,8 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
tailwind-merge@2.5.4: {}
tailwindcss@3.4.14:
dependencies:
'@alloc/quick-lru': 5.2.0

View File

@@ -5,11 +5,12 @@ import { useState } from 'react'
function App() {
const [queryClient] = useState(() => new QueryClient())
return (
<div id="App" className="bg-bg min-h-screen text-gray-400">
<QueryClientProvider client={queryClient}>
<Sidebar />
</QueryClientProvider>
</div>
<QueryClientProvider client={queryClient}>
<div id="App" className="min-h-screen flex">
<Sidebar className="w-64" />
<div />
</div>
</QueryClientProvider>
)
}

View File

@@ -7,17 +7,30 @@ type FullfilledUseQueryResult<TData = unknown, TError = Error> = Omit<
data: Exclude<TData, undefined>
}
type WrappedUseQueryOptions<TData, TError, TQueryFnData> = UseQueryOptions<
TQueryFnData,
TError,
TData
> & { debug?: boolean }
export function useApi<T>(
promise: () => Promise<unknown>,
options: Partial<UseQueryOptions<unknown, Error, T>> = {},
promise: () => Promise<T>,
options: Partial<WrappedUseQueryOptions<T, Error, T>> = {},
): FullfilledUseQueryResult<T, Error> {
const query = useQuery({
queryFn: () =>
promise()
.then((result) => JSON.parse(result as string))
.then((json) => (json.error ? Promise.reject(json.error) : Promise.resolve(json))),
queryFn: async () => {
if (options.debug) console.debug('useApi', promise)
const json = await promise()
if (options.debug) console.debug('useApi response:', json)
return await (isError(json) ? Promise.reject(json.error) : Promise.resolve(json))
},
queryKey: [],
...options,
})
return query as FullfilledUseQueryResult<T, Error>
}
function isError(json: unknown): json is { error: unknown } {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return Boolean((json as any).error)
}

View File

@@ -0,0 +1 @@
export type HtmlProps<T extends keyof JSX.IntrinsicElements> = JSX.IntrinsicElements[T]

View File

@@ -0,0 +1,5 @@
import { twMerge } from 'tailwind-merge'
import clsx, { ClassValue } from 'clsx'
export function cn(...classes: ClassValue[]): string {
return twMerge(clsx(...classes))
}

View File

@@ -1,34 +1,38 @@
import { useApi } from '../../common/api'
import { GetScreenshotsDirs } from '../../../wailsjs/go/main/App'
import { GetSteamLibraryMeta } from '../../../wailsjs/go/main/App'
import { HtmlProps } from '../../common/types'
import React from 'react'
import { cn } from '../../common/utils'
export function Sidebar() {
const { data } = useApi<{ steamDir: string }>(() => GetScreenshotsDirs(), { initialData: {} })
export function Sidebar({ className, ...rest }: HtmlProps<'div'>) {
const { data } = useApi(() => GetSteamLibraryMeta(), { initialData: {} as never, debug: true })
const { steamDir } = data
return (
<div>
<ul className="my-3">
<ListItem>Games</ListItem>
<ListItem>
<div>Screenshots</div>
<ul>
{[steamDir].map((item, i) => (
<ListItem key={i} data-key={item}>
{item}
</ListItem>
))}
</ul>
</ListItem>
<ListItem>Save Data</ListItem>
<div className={cn('py-3 bg-bg-800 h-screen overflow-y-auto', className)} {...rest}>
<ul>
<ListItem label="Games" />
<ListItem label="Screenshots" />
<ListItem label="Save Data" />
</ul>
<code>
<pre>{JSON.stringify(data, null, 2)}</pre>
</code>
</div>
)
}
function ListItem({ children }: React.PropsWithChildren<object>) {
function ListItem({ children, label, ...rest }: HtmlProps<'div'> & { label: React.ReactNode }) {
// TODO should be siblings under a fragment, use li instead of div for props ^
return (
<li className="h-10 flex items-center justify-start py-3 px-6 hover:bg-bg-800 cursor-pointer">
{children}
<li>
<div
className="min-h-10 flex items-center justify-start py-3 px-6 hover:bg-bg-700 cursor-pointer"
{...rest}
>
{label}
</div>
{children ? <ul>{children}</ul> : null}
</li>
)
}

53
frontend/src/fonts.css Normal file
View File

@@ -0,0 +1,53 @@
/* nunito-cyrillic-ext-wght-normal */
@font-face {
font-family: 'Nunito Variable';
font-style: normal;
font-display: swap;
font-weight: 200 1000;
src: url(fonts/nunito-cyrillic-ext-wght-normal.woff2) format('woff2-variations');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* nunito-cyrillic-wght-normal */
@font-face {
font-family: 'Nunito Variable';
font-style: normal;
font-display: swap;
font-weight: 200 1000;
src: url(fonts/nunito-cyrillic-wght-normal.woff2) format('woff2-variations');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* nunito-vietnamese-wght-normal */
@font-face {
font-family: 'Nunito Variable';
font-style: normal;
font-display: swap;
font-weight: 200 1000;
src: url(fonts/nunito-vietnamese-wght-normal.woff2) format('woff2-variations');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0,
U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* nunito-latin-ext-wght-normal */
@font-face {
font-family: 'Nunito Variable';
font-style: normal;
font-display: swap;
font-weight: 200 1000;
src: url(fonts/nunito-latin-ext-wght-normal.woff2) format('woff2-variations');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB,
U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* nunito-latin-wght-normal */
@font-face {
font-family: 'Nunito Variable';
font-style: normal;
font-display: swap;
font-weight: 200 1000;
src: url(fonts/nunito-latin-wght-normal.woff2) format('woff2-variations');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF,
U+FFFD;
}

View File

@@ -1,7 +1,9 @@
import React from 'react'
import { createRoot } from 'react-dom/client'
import './style.css'
import '@fontsource-variable/nunito'
// import './fonts.css'
import './reset.css'
import './style.css'
import App from './App'
const container = document.getElementById('root')

135
frontend/src/reset.css Normal file
View File

@@ -0,0 +1,135 @@
/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/
html,
body,
div,
span,
applet,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
canvas,
details,
embed,
figure,
figcaption,
footer,
header,
hgroup,
menu,
nav,
output,
ruby,
section,
summary,
time,
mark,
audio,
video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section {
display: block;
}
body {
line-height: 1;
}
ol,
ul {
list-style: none;
}
blockquote,
q {
quotes: none;
}
blockquote:before,
blockquote:after,
q:before,
q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}

View File

@@ -4,159 +4,14 @@
html {
@apply bg-bg;
color: white;
cursor: default;
}
body {
margin: 0;
color: white;
font-family:
'Nunito Variable',
-apple-system,
BlinkMacSystemFont,
'Helvetica Neue',
'Segoe UI',
'Roboto',
'Oxygen',
'Ubuntu',
'Cantarell',
'Fira Sans',
'Droid Sans',
sans-serif;
}
/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/
/* body { */
/* min-height: 100vh; */
/* } */
html,
body,
div,
span,
applet,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
canvas,
details,
embed,
figure,
figcaption,
footer,
header,
hgroup,
menu,
nav,
output,
ruby,
section,
summary,
time,
mark,
audio,
video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section {
display: block;
}
body {
line-height: 1;
}
ol,
ul {
list-style: none;
}
blockquote,
q {
quotes: none;
}
blockquote:before,
blockquote:after,
q:before,
q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
@apply text-gray-400;
}

View File

@@ -25,6 +25,22 @@ export default {
...colors,
},
},
fontFamily: {
nunito: [
'Nunito Variable',
'-apple-system',
'BlinkMacSystemFont',
'Helvetica Neue',
'Segoe UI',
'Roboto',
'Oxygen',
'Ubuntu',
'Cantarell',
'Fira Sans',
'Droid Sans',
'sans-serif',
],
},
},
plugins: [],
}

View File

@@ -1,4 +1,5 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
import {main} from '../models';
export function GetScreenshotsDirs():Promise<string>;
export function GetSteamLibraryMeta():Promise<main.SteamLibraryMeta>;

View File

@@ -2,6 +2,6 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function GetScreenshotsDirs() {
return window['go']['main']['App']['GetScreenshotsDirs']();
export function GetSteamLibraryMeta() {
return window['go']['main']['App']['GetSteamLibraryMeta']();
}

27
frontend/wailsjs/go/models.ts Executable file
View File

@@ -0,0 +1,27 @@
export namespace main {
export class SteamLibraryMeta {
error?: string;
steamDir: string;
userDir: string;
gameDirs: string[];
screenshotsDirs: string[];
syncDir: string;
static createFrom(source: any = {}) {
return new SteamLibraryMeta(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.error = source["error"];
this.steamDir = source["steamDir"];
this.userDir = source["userDir"];
this.gameDirs = source["gameDirs"];
this.screenshotsDirs = source["screenshotsDirs"];
this.syncDir = source["syncDir"];
}
}
}

View File

@@ -1,43 +0,0 @@
package main
import (
"encoding/json"
"fmt"
)
type Invoke struct {
Command string
}
type InvokeRunnable interface {
Run(args map[string]interface{})
}
//
type GetScreenshotsDirs struct {
Command string
}
func (g GetScreenshotsDirs) Run(args map[string]interface{}) {
p, err := GetSteamDirectory()
if err != nil {
fmt.Println(err)
return
}
out := make(map[string]interface{})
out["steam_dir"] = p
res, _ := json.Marshal(out)
fmt.Println(string(res))
}
//
func parseCommand(command string) (InvokeRunnable, error) {
switch command {
case "get_screenshots_dirs":
return GetScreenshotsDirs{}, nil
}
return nil, fmt.Errorf("unknown command %q", command)
}