feat: Implemented AppImage building
This commit is contained in:
parent
d8f471dadc
commit
6a288f765e
38 changed files with 1036 additions and 147 deletions
7
.config/flatpak/com.simeonradivoev.gameflow-deck.desktop
Normal file
7
.config/flatpak/com.simeonradivoev.gameflow-deck.desktop
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[Desktop Entry]
|
||||
Name=GameFlow
|
||||
Comment=GameFlow Deck
|
||||
Exec=gameflow
|
||||
Icon=com.simeonradivoev.gameflow-deck
|
||||
Type=Application
|
||||
Categories=Game;
|
||||
96
.config/flatpak/com.simeonradivoev.gameflow-deck.json
Normal file
96
.config/flatpak/com.simeonradivoev.gameflow-deck.json
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
{
|
||||
"app-id": "com.simeonradivoev.gameflow-deck",
|
||||
"runtime": "org.kde.Platform",
|
||||
"runtime-version": "6.10",
|
||||
"sdk": "org.kde.Sdk",
|
||||
"command": "/app/bin/gameflow",
|
||||
"base": "io.qt.qtwebengine.BaseApp",
|
||||
"base-version": "6.10",
|
||||
"finish-args": [
|
||||
"--share=ipc",
|
||||
"--share=network",
|
||||
"--socket=pulseaudio",
|
||||
"--socket=wayland",
|
||||
"--socket=x11",
|
||||
"--device=all",
|
||||
"--filesystem=host",
|
||||
"--filesystem=home",
|
||||
"--env=PKG_CONFIG_LIBDIR=/app/lib",
|
||||
"--env=FLATPAK_BUILD=true",
|
||||
"--allow=devel"
|
||||
],
|
||||
"modules": [
|
||||
{
|
||||
"name": "gameflow-bun",
|
||||
"buildsystem": "simple",
|
||||
"build-commands": [
|
||||
"mkdir -p /app/bin",
|
||||
"mkdir -p /app/share/gameflow",
|
||||
"mkdir -p /app/lib",
|
||||
"install -Dm644 256x256.png /app/share/icons/hicolor/256x256/apps/com.simeonradivoev.gameflow-deck.png",
|
||||
"install -Dm644 com.simeonradivoev.gameflow-deck.desktop /app/share/applications/com.simeonradivoev.gameflow-deck.desktop",
|
||||
"mv libvips-cpp.so.* /app/lib",
|
||||
"mv * /app/share/gameflow/",
|
||||
"mv /app/share/gameflow/gameflow /app/bin",
|
||||
"mv /app/share/gameflow/bun /app/bin",
|
||||
"chmod +x /app/bin/gameflow",
|
||||
"chmod +x /app/bin/bun"
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"type": "dir",
|
||||
"path": "../build/linux"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"path": "../flatpak/com.simeonradivoev.gameflow-deck.desktop"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"path": "../src/mainview/assets/256x256.png"
|
||||
},
|
||||
{
|
||||
"type": "script",
|
||||
"dest-filename": "gameflow",
|
||||
"commands": [
|
||||
"cd /app/share/gameflow",
|
||||
"exec bun run index.js \"$@\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://github.com/oven-sh/bun/releases/download/bun-v1.3.9/bun-linux-x64.zip",
|
||||
"sha256": "4680e80e44e32aa718560ceae85d22ecfbf2efb8f3641782e35e4b7efd65a1aa",
|
||||
"only-arches": [
|
||||
"x86_64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://github.com/oven-sh/bun/releases/download/bun-v1.3.9/bun-linux-aarch64.zip",
|
||||
"sha256": "a2c2862bcc1fd1c0b3a8dcdc8c7efb5e2acd871eb20ed2f17617884ede81c844",
|
||||
"only-arches": [
|
||||
"aarch64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"path": "../node_modules/@img/sharp-libvips-linux-x64/lib/libvips-cpp.so.8.17.3",
|
||||
"only-arches": [
|
||||
"x86_64"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "webview",
|
||||
"buildsystem": "cmake-ninja",
|
||||
"sources": [
|
||||
{
|
||||
"type": "dir",
|
||||
"path": "../flatpak/webview"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
35
.config/flatpak/webview/CMakeLists.txt
Normal file
35
.config/flatpak/webview/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
project(SimpleWebView LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# Required for Qt WebEngine
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS
|
||||
Core
|
||||
Widgets
|
||||
WebEngineWidgets,
|
||||
Gamepad
|
||||
)
|
||||
|
||||
add_executable(webview
|
||||
main.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(webview PRIVATE
|
||||
Qt6::Core
|
||||
Qt6::Widgets
|
||||
Qt6::WebEngineWidgets,
|
||||
Qt6::Gamepad
|
||||
)
|
||||
|
||||
|
||||
# Install binary into Flatpak prefix (/app)
|
||||
install(TARGETS webview
|
||||
RUNTIME DESTINATION bin
|
||||
)
|
||||
14
.config/flatpak/webview/main.cpp
Normal file
14
.config/flatpak/webview/main.cpp
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
#include <QApplication>
|
||||
#include <QWebEngineView>
|
||||
#include <QWebEngineSettings>
|
||||
#include <QUrl>
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QApplication app(argc, argv);
|
||||
if (argc < 2) { return 1; }
|
||||
QWebEngineView view;
|
||||
view.setUrl(QUrl(argv[1]));
|
||||
view.showFullScreen();
|
||||
return app.exec();
|
||||
}
|
||||
|
|
@ -39,11 +39,11 @@ jobs:
|
|||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Build Canary
|
||||
run: bun run package:auto-prod
|
||||
run: bun run appimage:build:prod
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: canary-build
|
||||
path: build/linux
|
||||
path: build/Gameflow.AppImage
|
||||
retention-days: 7
|
||||
|
|
|
|||
27
.github/workflows/build.yml
vendored
27
.github/workflows/build.yml
vendored
|
|
@ -6,11 +6,28 @@ on:
|
|||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, ubuntu-latest]
|
||||
build-linux:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Bun Install
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Build Canary
|
||||
run: bun run appimage:build:prod
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: canary-build-${{ runner.os }}
|
||||
path: build/Gameflow.AppImage
|
||||
build-windows:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
|
|
|||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -24,3 +24,5 @@ settings.local.json
|
|||
artifacts
|
||||
trace
|
||||
downloads
|
||||
.flatpak-builder
|
||||
gameflow-deck.code-workspace
|
||||
19
.vscode/settings.json
vendored
19
.vscode/settings.json
vendored
|
|
@ -1,16 +1,23 @@
|
|||
{
|
||||
"files.readonlyInclude": {
|
||||
"**/*.gen.ts": true,
|
||||
"**/*.gen.*": true,
|
||||
"src/mainview/gen/*": true,
|
||||
},
|
||||
"files.watcherExclude": {
|
||||
"**/*.gen.ts": true,
|
||||
"**/*.gen.*": true,
|
||||
"src/mainview/gen/*": true,
|
||||
},
|
||||
"search.exclude": {
|
||||
"**/*.gen.ts": true,
|
||||
"**/*.gen.*": true,
|
||||
".flatpak-builder/**/*": true,
|
||||
"src/mainview/gen/*": true,
|
||||
},
|
||||
"npm.scriptRunner": "bun",
|
||||
"npm.exclude": [
|
||||
"**/.flatpak-builder/**/*",
|
||||
"**/build/flatpack/**",
|
||||
"**/flatpack/repo/**",
|
||||
],
|
||||
"editor.formatOnSave": true,
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features",
|
||||
|
|
@ -30,12 +37,6 @@
|
|||
"noriginmedia",
|
||||
"romm"
|
||||
],
|
||||
"terminal.integrated.env.linux": {
|
||||
"DISPLAY": ":0",
|
||||
"WAYLAND_DISPLAY": "wayland-0",
|
||||
"XDG_RUNTIME_DIR": "/run/user/1000",
|
||||
"GPG_TTY": "/dev/tty"
|
||||
},
|
||||
"editor.suggest.preview": true,
|
||||
"editor.suggest.snippetsPreventQuickSuggestions": false,
|
||||
"editor.wordWrap": "on"
|
||||
|
|
|
|||
18
.vscode/tasks.json
vendored
18
.vscode/tasks.json
vendored
|
|
@ -39,10 +39,6 @@
|
|||
"type": "shell",
|
||||
"command": "bun run dev:hmr",
|
||||
"isBackground": true,
|
||||
"options": {
|
||||
"env": {
|
||||
}
|
||||
},
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
|
|
@ -52,6 +48,20 @@
|
|||
"showReuseMessage": true,
|
||||
"clear": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Build Flatpak",
|
||||
"type": "shell",
|
||||
"command": "flatpak",
|
||||
"args": [
|
||||
"run",
|
||||
"com.simeonradivoev.gameflow-deck",
|
||||
"build/flatpak",
|
||||
"flatpak/com.simeonradivoev.gameflow-deck.json",
|
||||
"--force-clean"
|
||||
],
|
||||
"group": "build",
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
},
|
||||
{
|
||||
"path": "../../../../run/media/deck/a545d555-e643-4d7e-9a29-8103abc18328/gameflow"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"terminal.integrated.env.linux": {
|
||||
"DISPLAY": ":0",
|
||||
"WAYLAND_DISPLAY": "wayland-0",
|
||||
"XDG_RUNTIME_DIR": "/run/user/1000"
|
||||
}
|
||||
}
|
||||
}
|
||||
25
package.json
25
package.json
|
|
@ -3,6 +3,13 @@
|
|||
"displayName": "Gameflow",
|
||||
"version": "1.0.0",
|
||||
"description": "Game Launcher",
|
||||
"icon": "./src/mainview/assets/icon.svg",
|
||||
"main": "./src/bun/index.ts",
|
||||
"bin": "gameflow",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/simeonradivoev/gameflow-deck"
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "NODE_ENV=development bun run build && WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS='--remote-debugging-port=9222' bun run ./scripts/dev.ts",
|
||||
|
|
@ -11,14 +18,23 @@
|
|||
"build:pro": "NODE_ENV=production bun run build",
|
||||
"build:dev": "NODE_ENV=development bun run build",
|
||||
"package": "bun run build && bun run ./scripts/package-bun.ts",
|
||||
"package:auto-prod": "bun run build:pro && NODE_ENV=production bun run ./scripts/package-bun.ts",
|
||||
"package:linux": "bun run build && NODE_ENV=development TARGET=bun-linux-x64 bun run ./scripts/package-bun.ts",
|
||||
"package:auto-prod": "NODE_ENV=production bun run package",
|
||||
"package:auto-prod:dynamic": "NODE_ENV=production NON_COMPILED=true bun run package",
|
||||
"package:linux": "TARGET=bun-linux-x64 bun run package",
|
||||
"openapi-ts": "bun run ./scripts/romm/openapi-ts.ts",
|
||||
"run:build-action": "act workflow_dispatch --artifact-server-path artifacts --env ACTIONS_RUNTIME_TOKEN=foo -W .forgejo/workflows/build.yml",
|
||||
"hmr": "vite --port 5173",
|
||||
"drizzle:generate": "bunx drizzle-kit generate",
|
||||
"test": "bun test",
|
||||
"mappings:generate": "bun run drizzle-kit generate --dialect=sqlite --schema=./src/bun/api/schema/emulators.ts --out=./scripts/drizzle/es-de && bun run ./scripts/generate-es-de-mapping.ts"
|
||||
"mappings:generate": "bun run drizzle-kit generate --dialect=sqlite --schema=./src/bun/api/schema/emulators.ts --out=./scripts/drizzle/es-de && bun run ./scripts/generate-es-de-mapping.ts",
|
||||
"flatpak:generate-sources": "bun run ./scripts/generate-flatpak-sources.ts",
|
||||
"flatpak:override": "flatpak override org.flatpak.Builder --filesystem=host --device=all",
|
||||
"flatpak:restore": "flatpak override --reset --user org.flatpak.Builder",
|
||||
"flatpak:build": "flatpak run org.flatpak.Builder build/flatpak flatpak/com.simeonradivoev.gameflow-deck.json --repo=.config/flatpak/repo --force-clean",
|
||||
"flatpak:install": "bun run flatpak:build && flatpak --user install --reinstall \"$PWD/.config/flatpak/repo\" com.simeonradivoev.gameflow-deck",
|
||||
"appimage:build:prod": "bun run package:auto-prod && bun run ./scripts/build-appimage.ts",
|
||||
"appimage:build:dev": "bun run package && bun run ./scripts/build-appimage.ts",
|
||||
"version:generate": "standard-version --sign"
|
||||
},
|
||||
"dependencies": {
|
||||
"@auth/core": "^0.34.3",
|
||||
|
|
@ -32,12 +48,12 @@
|
|||
"elysia": "^1.4.22",
|
||||
"fs-extra": "^11.3.3",
|
||||
"get-folder-size": "^5.0.0",
|
||||
"jimp": "^1.6.0",
|
||||
"node-disk-info": "^1.3.0",
|
||||
"node-downloader-helper": "^2.1.10",
|
||||
"node-stream-zip": "^1.15.0",
|
||||
"open": "^11.0.0",
|
||||
"pathe": "^2.0.3",
|
||||
"sharp": "^0.34.5",
|
||||
"systeminformation": "^5.31.1",
|
||||
"tough-cookie": "^6.0.0",
|
||||
"tough-cookie-file-store": "^3.3.0",
|
||||
|
|
@ -65,6 +81,7 @@
|
|||
"@types/unzip-stream": "^0.3.4",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"animate.css": "^4.1.1",
|
||||
"app-builder-bin": "^5.0.0-alpha.13",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"classnames": "^2.5.1",
|
||||
"concurrently": "^9.2.1",
|
||||
|
|
|
|||
91
scripts/build-appimage.ts
Normal file
91
scripts/build-appimage.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import { $ } from "bun";
|
||||
import pkg from "../package.json";
|
||||
import fs from 'node:fs/promises';
|
||||
import { appBuilderPath, } from 'app-builder-bin';
|
||||
import path from 'node:path';
|
||||
import { ensureDir } from "fs-extra";
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// CONFIGURE THESE FOR YOUR APP
|
||||
// ─────────────────────────────────────────────
|
||||
const APP_DIR = "./build/linux";
|
||||
const BINARY_NAME = pkg.bin;
|
||||
const ICON = "./src/mainview/assets/256x256.png";
|
||||
const DESKTOP = "./flatpak/com.simeonradivoev.gameflow-deck.desktop";
|
||||
const TMP_FOLDER = ".";
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
const APP_NAME = pkg.displayName ?? pkg.name;
|
||||
const APP_ID = pkg.name;
|
||||
const APPDIR = path.resolve(path.join(TMP_FOLDER, `${APP_ID}.AppDir`));
|
||||
|
||||
console.log(`>>> Building AppImage for ${APP_NAME} (${APP_ID})...`);
|
||||
|
||||
await ensureDir(path.join(APPDIR, `usr`, 'bin'));
|
||||
await ensureDir("build");
|
||||
|
||||
// Copy app dir
|
||||
await fs.cp(`${APP_DIR}/.`, path.join(APPDIR, `usr`, 'share'), { recursive: true });
|
||||
await fs.rename(path.join(APPDIR, `usr`, 'share', BINARY_NAME), path.join(APPDIR, `usr`, 'bin', BINARY_NAME));
|
||||
|
||||
await fs.writeFile(path.join(APPDIR, `${APP_ID}.desktop`), `[Desktop Entry]
|
||||
Version=${pkg.version}
|
||||
X-AppImage-Name=${APP_NAME}
|
||||
X-AppImage-Version=${pkg.version}
|
||||
X-AppImage-Arch=${process.arch}
|
||||
Name=${APP_NAME}
|
||||
Comment=${pkg.description}
|
||||
Exec=${APP_ID}.AppImage
|
||||
Icon=.DirIcon
|
||||
Type=Application
|
||||
Categories=Game;
|
||||
`);
|
||||
|
||||
await Bun.write(path.join(APPDIR, "AppRun"), `#!/bin/bash
|
||||
APPDIR="$(dirname "$(readlink -f "$0")")"
|
||||
APPIMAGE=true
|
||||
exec "$APPDIR/usr/bin/${BINARY_NAME}" "$@"
|
||||
`);
|
||||
await $`chmod +x ${APPDIR}/AppRun`;
|
||||
|
||||
console.log(">>> Building AppImage...");
|
||||
const config = {
|
||||
productName: pkg.displayName,
|
||||
productFilename: pkg.name,
|
||||
executableName: BINARY_NAME,
|
||||
desktopEntry: DESKTOP,
|
||||
icons: [
|
||||
{
|
||||
file: ICON,
|
||||
size: 256
|
||||
}
|
||||
],
|
||||
fileAssociations: [
|
||||
|
||||
]
|
||||
};
|
||||
|
||||
const OUTPUT = path.resolve(path.join("build", `${APP_NAME}.AppImage`));
|
||||
const STAGE = path.resolve(path.join(TMP_FOLDER, `${APP_ID}.stage`));
|
||||
|
||||
await ensureDir(STAGE);
|
||||
|
||||
const proc = Bun.spawn([
|
||||
appBuilderPath,
|
||||
'appimage',
|
||||
`--app=${APPDIR}`,
|
||||
`--output=${OUTPUT}`,
|
||||
`--stage=${STAGE}`,
|
||||
`--arch=${process.arch}`,
|
||||
`--configuration=${JSON.stringify(config)}`
|
||||
], {
|
||||
stdout: "inherit",
|
||||
stderr: "inherit"
|
||||
});
|
||||
|
||||
const code = await proc.exited;
|
||||
await fs.rm(APPDIR, { recursive: true, force: true });
|
||||
await fs.rm(STAGE, { recursive: true, force: true });
|
||||
if (code !== 0) process.exit(code);
|
||||
|
||||
console.log(`\n✅ Done!`);
|
||||
|
|
@ -21,6 +21,10 @@ function spawnServer ()
|
|||
events.emit('exitapp');
|
||||
}
|
||||
},
|
||||
onExit (subprocess, exitCode, signalCode)
|
||||
{
|
||||
process.exit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -35,5 +39,5 @@ function spawnBrowser ()
|
|||
};
|
||||
}
|
||||
|
||||
spawnServer();
|
||||
spawnBrowser();
|
||||
const server = spawnServer();
|
||||
spawnBrowser()?.then(e => server.send({ type: 'exitapp' }));
|
||||
45
scripts/generate-flatpak-sources.ts
Normal file
45
scripts/generate-flatpak-sources.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { $ } from "bun";
|
||||
|
||||
const lockfile = Bun.argv[2] ?? "bun.lockb";
|
||||
const output = Bun.argv[3] ?? ".config/flatpak/sources.gen.json";
|
||||
|
||||
const text = await $`bun ./bun.lockb --hash: 0000000000000000-0000000000000000-0000000000000000-0000000000000000`.text();
|
||||
|
||||
interface FlatpakSource
|
||||
{
|
||||
type: "file";
|
||||
url: string;
|
||||
dest: string;
|
||||
"dest-filename": string;
|
||||
sha512?: string;
|
||||
sha256?: string;
|
||||
}
|
||||
|
||||
const sources: FlatpakSource[] = [];
|
||||
|
||||
for (const block of text.split("\n\n"))
|
||||
{
|
||||
const resolved = block.match(/\s+resolved "([^"]+)"/)?.[1];
|
||||
const integrity = block.match(/\s+integrity (\S+)/)?.[1];
|
||||
|
||||
if (!resolved || !integrity) continue;
|
||||
|
||||
const [algo, b64] = integrity.split("-");
|
||||
const hex = Buffer.from(b64, "base64").toString("hex");
|
||||
const url = new URL(resolved);
|
||||
const filename = url.pathname.split("/").pop()!;
|
||||
const dest = `flatpak-node/npm-cache${url.pathname.replace(filename, "")}`;
|
||||
|
||||
sources.push({
|
||||
type: "file",
|
||||
url: resolved,
|
||||
dest,
|
||||
"dest-filename": filename,
|
||||
...(algo === "sha512" ? { sha512: hex } : { sha256: hex }),
|
||||
});
|
||||
}
|
||||
|
||||
await Bun.write(output, JSON.stringify(sources, null, 2));
|
||||
console.log(`Wrote ${sources.length} entries to ${output}`);
|
||||
|
||||
export { };
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
import fs from "node:fs/promises";
|
||||
import path, { } from "node:path";
|
||||
import os from "node:os";
|
||||
import { Glob } from "bun";
|
||||
|
||||
const system = getPlatform();
|
||||
const buildSubDir = process.env.BUILD_DIR ?? `./build/${system.platform}`;
|
||||
|
|
@ -12,7 +11,7 @@ const compileOption: Bun.CompileBuildOptions = {
|
|||
autoloadTsconfig: true,
|
||||
autoloadPackageJson: true,
|
||||
autoloadDotenv: true,
|
||||
autoloadBunfig: true
|
||||
autoloadBunfig: true,
|
||||
};
|
||||
|
||||
if (process.env.TARGET)
|
||||
|
|
@ -23,7 +22,7 @@ if (process.env.TARGET)
|
|||
await Bun.build({
|
||||
entrypoints: ["./src/bun/index.ts", `./src/bun/webview/${system.platform}.ts`],
|
||||
metafile: true,
|
||||
compile: compileOption,
|
||||
compile: process.env.NON_COMPILED ? undefined : compileOption,
|
||||
outdir: buildSubDir,
|
||||
root: './src/bun',
|
||||
define: {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { migrate } from "drizzle-orm/bun-sqlite/migrator";
|
|||
import { drizzle } from "drizzle-orm/bun-sqlite";
|
||||
import Conf from "conf";
|
||||
import projectPackage from '~/package.json';
|
||||
import { Notification, SERVER_URL, SettingsSchema, SettingsType } from "@shared/constants";
|
||||
import { Notification, SettingsSchema, SettingsType } from "@shared/constants";
|
||||
import { client } from "@clients/romm/client.gen";
|
||||
import * as schema from "./schema/app";
|
||||
import * as emulatorSchema from "./schema/emulators";
|
||||
|
|
@ -18,14 +18,17 @@ import os from 'node:os';
|
|||
import { ActiveGame } from "../types/types";
|
||||
import EventEmitter from "node:events";
|
||||
import { ErrorLike } from "bun";
|
||||
import { getErrorMessage } from "../utils";
|
||||
import { appPath, getErrorMessage } from "../utils";
|
||||
import { DrizzleSqliteDODatabase } from "drizzle-orm/durable-sqlite";
|
||||
|
||||
export const config = new Conf<SettingsType>({
|
||||
projectName: projectPackage.name,
|
||||
projectSuffix: 'bun',
|
||||
schema: Object.fromEntries(Object.entries(SettingsSchema.shape).map(([key, schema]) => [key, schema.toJSONSchema() as any])) as any,
|
||||
defaults: SettingsSchema.parse({}),
|
||||
defaults: SettingsSchema.parse({
|
||||
downloadPath: path.join(os.homedir(), "gameflow"),
|
||||
windowSize: { width: 1280, height: 800 }
|
||||
} satisfies SettingsType),
|
||||
});
|
||||
export const customEmulators = new Conf<Record<string, string>>({
|
||||
projectName: projectPackage.name,
|
||||
|
|
@ -41,6 +44,7 @@ export const customEmulators = new Conf<Record<string, string>>({
|
|||
|
||||
console.log("Config Path Located At: ", config.path);
|
||||
console.log("Custom Emulator Paths Located At: ", customEmulators.path);
|
||||
console.log("App Directory is ", process.env.APPDIR);
|
||||
const fileCookieStore = new FileCookieStore(path.join(path.dirname(config.path), 'cookies.json'));
|
||||
console.log("Cookie Jar Path Located At: ", fileCookieStore.filePath);
|
||||
export const jar = new CookieJar(fileCookieStore);
|
||||
|
|
@ -48,8 +52,8 @@ await fs.mkdir(config.get('downloadPath'), { recursive: true });
|
|||
let sqlite: Database;
|
||||
export let db: DrizzleSqliteDODatabase<typeof schema>;
|
||||
await reloadDatabase();
|
||||
migrate(db!, { migrationsFolder: "./drizzle" });
|
||||
const emulatorsSqlite = new Database(`./vendors/es-de/emulators.${os.platform()}.${os.arch()}.sqlite`, { readonly: true });
|
||||
migrate(db!, { migrationsFolder: appPath("./drizzle") });
|
||||
const emulatorsSqlite = new Database(appPath(`./vendors/es-de/emulators.${os.platform()}.${os.arch()}.sqlite`), { readonly: true });
|
||||
export const emulatorsDb = drizzle(emulatorsSqlite, { schema: emulatorSchema });
|
||||
export const taskQueue = new TaskQueue();
|
||||
config.onDidChange('rommAddress', v => client.setConfig({ baseUrl: v }));
|
||||
|
|
|
|||
|
|
@ -13,10 +13,40 @@ import buildStatusResponse, { getValidLaunchCommandsForGame } from "./services/s
|
|||
import { errorToResponse } from "elysia/adapter/bun/handler";
|
||||
import { launchCommand } from "./services/launchGameService";
|
||||
import { getErrorMessage } from "@/bun/utils";
|
||||
import sharp from 'sharp';
|
||||
import { Jimp } from 'jimp';
|
||||
|
||||
async function processImage (img: string | Buffer | ArrayBuffer, { blur, width, height }: { blur?: number, width?: number, height?: number; })
|
||||
{
|
||||
if (blur)
|
||||
{
|
||||
const jimp = await Jimp.read(img);
|
||||
if (width)
|
||||
{
|
||||
jimp.resize({ w: width, h: height });
|
||||
}
|
||||
if (height)
|
||||
{
|
||||
jimp.resize({ w: width, h: height });
|
||||
}
|
||||
if (blur)
|
||||
{
|
||||
jimp.blur(blur);
|
||||
}
|
||||
|
||||
return jimp.getBuffer('image/png');
|
||||
}
|
||||
|
||||
if (typeof img === 'string')
|
||||
{
|
||||
const rommFetch = await fetch(img);
|
||||
return rommFetch;
|
||||
}
|
||||
|
||||
return img;
|
||||
}
|
||||
|
||||
export default new Elysia()
|
||||
.get('/game/local/:id/cover', async ({ params: { id }, query: { blur, width, height }, set }) =>
|
||||
.get('/game/local/:id/cover', async ({ params: { id }, query, set }) =>
|
||||
{
|
||||
const coverBlob = await db.query.games.findFirst({ columns: { cover: true, cover_type: true }, where: eq(schema.games.id, id) });
|
||||
if (!coverBlob || !coverBlob.cover)
|
||||
|
|
@ -28,22 +58,32 @@ export default new Elysia()
|
|||
set.headers["content-type"] = coverBlob.cover_type;
|
||||
}
|
||||
|
||||
return sharp(coverBlob.cover).resize({ width, height, withoutEnlargement: true }).blur(blur);
|
||||
return processImage(coverBlob.cover, query);
|
||||
/*return sharp(coverBlob.cover)
|
||||
.resize({ width, height, withoutEnlargement: true })
|
||||
.blur(blur)
|
||||
.toBuffer();*/
|
||||
}, {
|
||||
params: z.object({ id: z.coerce.number() }),
|
||||
query: z.object({ blur: z.coerce.number().optional(), width: z.coerce.number().optional(), height: z.coerce.number().optional() })
|
||||
})
|
||||
.get('/image/:source/*', async ({ params: { source, "*": path }, query: { blur, width, height } }) =>
|
||||
.get('/image/:source/*', async ({ params: { source, "*": path }, query }) =>
|
||||
{
|
||||
if (source === 'romm')
|
||||
{
|
||||
const rommAdress = config.get('rommAddress');
|
||||
return processImage(`${rommAdress}/${path}`, query);
|
||||
|
||||
/*
|
||||
const rommFetch = await fetch(`${rommAdress}/${path}`);
|
||||
return sharp(await rommFetch.arrayBuffer()).resize({ width, height, withoutEnlargement: true }).sharpen().blur(blur);
|
||||
return sharp(await rommFetch.arrayBuffer())
|
||||
.resize({ width, height, withoutEnlargement: true })
|
||||
.blur(blur)
|
||||
.toBuffer();*/
|
||||
}
|
||||
return status('Not Found');
|
||||
}, { query: z.object({ blur: z.coerce.number().optional(), width: z.coerce.number().optional(), height: z.coerce.number().optional() }) })
|
||||
.get('/screenshot/:id', async ({ params: { id }, query: { blur, width, height }, set }) =>
|
||||
.get('/screenshot/:id', async ({ params: { id }, query, set }) =>
|
||||
{
|
||||
const screenshot = await db.query.screenshots.findFirst({ where: eq(schema.screenshots.id, id), columns: { content: true, type: true } });
|
||||
if (screenshot)
|
||||
|
|
@ -52,8 +92,10 @@ export default new Elysia()
|
|||
{
|
||||
set.headers["content-type"] = screenshot.type;
|
||||
}
|
||||
return sharp(screenshot.content).resize({ width, height, withoutEnlargement: true }).blur(blur);
|
||||
|
||||
return processImage(screenshot.content, query);
|
||||
//return sharp(screenshot.content).resize({ width, height, withoutEnlargement: true }).blur(blur).toBuffer();
|
||||
//return screenshot.content;
|
||||
}
|
||||
|
||||
return status(404);
|
||||
|
|
@ -158,7 +200,7 @@ export default new Elysia()
|
|||
paths_screenshots: localGame.screenshots.map(s => `/api/romm/screenshot/${s.id}`),
|
||||
local: true,
|
||||
missing: !exists,
|
||||
platform_display_name: localGame.platform.name,
|
||||
platform_display_name: localGame.platform?.name,
|
||||
summary: localGame.summary,
|
||||
source: localGame.source,
|
||||
source_id: localGame.source_id,
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export const games = sqliteTable('games', {
|
|||
export const gamesRelations = relations(games, ({ many, one }) => ({
|
||||
screenshots: many(screenshots),
|
||||
platform: one(platforms, {
|
||||
fields: [games.id],
|
||||
fields: [games.platform_id],
|
||||
references: [platforms.id]
|
||||
})
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -34,6 +34,12 @@ export const system = new Elysia({ prefix: '/api/system' })
|
|||
})
|
||||
.get('/info', async () =>
|
||||
{
|
||||
let source = 'unknown';
|
||||
if (process.env.APPIMAGE === 'true')
|
||||
source = "AppImage";
|
||||
if (process.env.FLATPAK === 'true')
|
||||
source = "Flatpak";
|
||||
|
||||
return {
|
||||
homeDir: os.homedir(),
|
||||
user: os.userInfo().username,
|
||||
|
|
@ -42,6 +48,7 @@ export const system = new Elysia({ prefix: '/api/system' })
|
|||
hostname: os.hostname(),
|
||||
steamDeck: process.env.SteamDeck,
|
||||
machine: os.machine(),
|
||||
source
|
||||
};
|
||||
})
|
||||
.get('/notifications', ({ set }) =>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,15 @@ async function cleanup ()
|
|||
|
||||
if (Bun.env.HEADLESS)
|
||||
{
|
||||
// Called by outside force
|
||||
process.on('message', ({ type }) =>
|
||||
{
|
||||
if (type === 'exitapp')
|
||||
{
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
// Called by user
|
||||
events.on('exitapp', () =>
|
||||
{
|
||||
process.send?.({ type: 'exitapp' });
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { SERVER_PORT } from "../shared/constants";
|
|||
import path from 'node:path';
|
||||
import appInfo from '../../package.json';
|
||||
import { host } from "./utils/host";
|
||||
import { appPath } from "./utils";
|
||||
|
||||
export function RunBunServer ()
|
||||
{
|
||||
|
|
@ -10,9 +11,9 @@ export function RunBunServer ()
|
|||
port: SERVER_PORT,
|
||||
hostname: host,
|
||||
routes: {
|
||||
"/": Bun.file("./dist/index.html"),
|
||||
"/": Bun.file(appPath("./dist/index.html")),
|
||||
// Serve a file by lazily loading it into memory
|
||||
"/favicon.ico": Bun.file("./dist/favicon.ico"),
|
||||
"/favicon.ico": Bun.file(appPath("./dist/favicon.ico")),
|
||||
"/.well-known/appspecific/com.chrome.devtools.json": new Response(
|
||||
JSON.stringify({
|
||||
name: appInfo.name,
|
||||
|
|
@ -30,7 +31,7 @@ export function RunBunServer ()
|
|||
fetch: async (req) =>
|
||||
{
|
||||
const url = new URL(req.url);
|
||||
return new Response(Bun.file(`./${path.join('dist', url.pathname)}`));
|
||||
return new Response(Bun.file(appPath(`./${path.join('dist', url.pathname)}`)));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
|
||||
import { $ } from 'bun';
|
||||
import path from 'node:path';
|
||||
|
||||
export function checkRunning (pid: number)
|
||||
{
|
||||
|
|
@ -39,6 +40,19 @@ export async function isSteamDeck ()
|
|||
}
|
||||
}
|
||||
|
||||
export function appPath (input: string): string
|
||||
{
|
||||
if (path.isAbsolute(input))
|
||||
{
|
||||
return input;
|
||||
}
|
||||
if (process.env.APPDIR)
|
||||
{
|
||||
return path.join(process.env.APPDIR ?? '', 'usr', 'share', input);
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
export async function openExternal (target: string)
|
||||
{
|
||||
if (process.platform === "linux")
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ export async function BuildParams (data: { configPath: string; })
|
|||
args.push('--disabled-features=WindowControlsOverlay,navigationControls,Translate,msUndersideButton');
|
||||
args.push(`--profile-directory=Default`);
|
||||
|
||||
if (Bun.env.NODE_ENV !== 'production')
|
||||
if (Bun.env.NODE_ENV === 'development')
|
||||
{
|
||||
args.push('--auto-open-devtools-for-tabs');
|
||||
args.push('--remote-debugging-port=9222');
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import { SERVER_URL } from "@/shared/constants";
|
||||
import Webview from "@rcompat/webview";
|
||||
import { host } from "../utils/host";
|
||||
|
||||
export default function (webview: Webview)
|
||||
export default function (webview: { navigate: (url: string) => void; run: () => void; destroy: () => void; })
|
||||
{
|
||||
self.addEventListener('message', (e) =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,6 +2,33 @@ import Webview from "@rcompat/webview";
|
|||
import platform from "@rcompat/webview/linux-x64";
|
||||
import webviewWorkerBase from "./base";
|
||||
|
||||
if (process.env.FLATPAK_BUILD === "true")
|
||||
{
|
||||
let webview: Bun.Subprocess | undefined = undefined;
|
||||
let hostUrl: string | undefined = undefined;
|
||||
webviewWorkerBase({
|
||||
navigate: (url) =>
|
||||
{
|
||||
hostUrl = url;
|
||||
|
||||
}, destroy: () => webview?.kill(), run: () =>
|
||||
{
|
||||
webview = Bun.spawn(["webview", hostUrl ?? ''], {
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
env: {
|
||||
...process.env,
|
||||
},
|
||||
onExit ()
|
||||
{
|
||||
postMessage({ data: 'destroyed' });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else
|
||||
{
|
||||
console.log("Launching Webview");
|
||||
const webview = new Webview({ debug: import.meta.env.NODE_ENV === 'development', platform });
|
||||
webviewWorkerBase(webview);
|
||||
}
|
||||
BIN
src/mainview/assets/256x256.png
(Stored with Git LFS)
Normal file
BIN
src/mainview/assets/256x256.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/mainview/assets/favicon.ico
(Stored with Git LFS)
BIN
src/mainview/assets/favicon.ico
(Stored with Git LFS)
Binary file not shown.
BIN
src/mainview/assets/icon.svg
(Stored with Git LFS)
Normal file
BIN
src/mainview/assets/icon.svg
(Stored with Git LFS)
Normal file
Binary file not shown.
|
|
@ -71,7 +71,8 @@ export function AnimatedBackground (data: {
|
|||
backgroundSize: '100%',
|
||||
backgroundPositionY: 'bottom',
|
||||
backgroundPositionX: 'center',
|
||||
backgroundColor: "var(--color-base-300)",
|
||||
backgroundBlendMode: 'soft-light',
|
||||
backgroundColor: "var(--color-base-100)",
|
||||
} : {}}
|
||||
>
|
||||
{!data.scrolling && <div className='absolute top-0 left-0 overflow-hidden w-full h-full'>
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export default function GameCard (data: GameCardParams)
|
|||
onEnterPress: () => data.onAction?.(),
|
||||
onBlur: () => data.onBlur?.(data.id)
|
||||
});
|
||||
const { isPointer } = useActiveControl();
|
||||
const { isMouse, isPointer } = useActiveControl();
|
||||
|
||||
return (
|
||||
<li
|
||||
|
|
@ -69,7 +69,7 @@ export default function GameCard (data: GameCardParams)
|
|||
"overflow-hidden transition-all duration-200 drop-shadow-lg cursor-pointer",
|
||||
classNames({
|
||||
"focused animate-wiggle ring-7 bg-base-content text-base-300 drop-shadow-xl drop-shadow-black/30 scale-102 z-10": focused && !isPointer,
|
||||
"group hover:focused hover:animate-wiggle sm:hover:ring-4 md:hover:ring-7 hover:bg-base-content hover:text-base-300 hover:drop-shadow-xl hover:drop-shadow-black/30 hover:scale-102 hover:z-10": isPointer,
|
||||
"group hover:focused hover:animate-wiggle sm:hover:ring-4 md:hover:ring-7 hover:bg-base-content hover:text-base-300 hover:drop-shadow-xl hover:drop-shadow-black/30 hover:scale-102 hover:z-10": isMouse,
|
||||
"h-(--game-card-height)": typeof data.preview === "string"
|
||||
}),
|
||||
data.className
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export function GameList (data: GameListParams)
|
|||
const previewUrl = localStorage.getItem('background-blur') !== "false" ? coverUrl : screenshotUrl;
|
||||
previewUrl.searchParams.delete('ts');
|
||||
data.setBackground?.(previewUrl.href);
|
||||
queryClient.prefetchQuery(gameQuery(source ?? id.source, sourceId ?? id.id));
|
||||
//queryClient.prefetchQuery(gameQuery(source ?? id.source, sourceId ?? id.id));
|
||||
} catch
|
||||
{
|
||||
|
||||
|
|
|
|||
|
|
@ -286,7 +286,7 @@ export default function ConsoleHomeUI ()
|
|||
headerButtons.push({ id: "search", icon: <Search /> }, { id: "power-button", icon: <Power />, external: true, action: () => closeMutation.mutate() });
|
||||
|
||||
return (
|
||||
<AnimatedBackground animated ref={ref} backgroundKey="home-background" className="grid grid-cols-3 sm:landscape:grid-rows-[3rem_minmax(var(--game-card-height-safe),1fr)_4rem] md:landscape:grid-rows-[5rem_4rem_minmax(var(--game-card-height-safe),1fr)_6rem_6rem] gap-1 portrait:grid-rows-[3rem_4rem_minmax(var(--game-card-height-safe),1fr)] max-h-screen overflow-hidden">
|
||||
<AnimatedBackground animated ref={ref} backgroundKey="home-background" className="grid grid-cols-3 sm:landscape:grid-rows-[3rem_minmax(var(--game-card-height-safe),1fr)_4rem] md:landscape:grid-rows-[5rem_4rem_minmax(var(--game-card-height-safe),1fr)_6rem_6rem] gap-1 portrait:grid-rows-[3rem_4rem_minmax(var(--game-card-height-safe),1fr)] max-h-screen overflow-clip">
|
||||
<FocusContext.Provider value={focusKey}>
|
||||
<div className="sm:landscape:hidden md:landscape:inline sm:portrait:col-start-1 md:inline flex col-span-1 md:pl-2 md:pt-2">
|
||||
<HeaderAccounts />
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { rommApi, systemApi } from '@/mainview/scripts/clientApi';
|
||||
import { systemApi } from '@/mainview/scripts/clientApi';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
|
||||
export const Route = createFileRoute('/settings/about')({
|
||||
component: RouteComponent,
|
||||
|
|
@ -51,6 +50,10 @@ function RouteComponent ()
|
|||
<th>Machine</th>
|
||||
<td>{systemInfo?.data?.machine}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Source</th>
|
||||
<td>{systemInfo?.data?.source}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Steam Deck</th>
|
||||
<td>{systemInfo?.data?.steamDeck ?? 'false'}</td>
|
||||
|
|
|
|||
|
|
@ -62,12 +62,12 @@ window.addEventListener('touchcancel', handleTouchEnd);
|
|||
window.addEventListener("gamepadconnected", handleLoop);
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
import.meta.hot.dispose(() => window.removeEventListener('gamepaddisconnected', handleLoop));
|
||||
import.meta.hot.dispose(() => window.removeEventListener('mousemove', handleMouseMove));
|
||||
import.meta.hot.dispose(() => window.removeEventListener('keydown', handleKeyDown));
|
||||
import.meta.hot.dispose(() => window.removeEventListener('touchstart', handleTouchStart));
|
||||
import.meta.hot.dispose(() => window.removeEventListener('touchend', handleTouchEnd));
|
||||
import.meta.hot.dispose(() => window.removeEventListener('touchcancel', handleTouchEnd));
|
||||
import.meta.hot?.dispose(() => window.removeEventListener('gamepaddisconnected', handleLoop));
|
||||
import.meta.hot?.dispose(() => window.removeEventListener('mousemove', handleMouseMove));
|
||||
import.meta.hot?.dispose(() => window.removeEventListener('keydown', handleKeyDown));
|
||||
import.meta.hot?.dispose(() => window.removeEventListener('touchstart', handleTouchStart));
|
||||
import.meta.hot?.dispose(() => window.removeEventListener('touchend', handleTouchEnd));
|
||||
import.meta.hot?.dispose(() => window.removeEventListener('touchcancel', handleTouchEnd));
|
||||
|
||||
export default function useActiveControl ()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ const shortcutChangeDispatcher = setInterval(() =>
|
|||
window.dispatchEvent(new Event('shortcutsChanged'));
|
||||
isDirty = false;
|
||||
}, 100);
|
||||
import.meta.hot.dispose(() => clearInterval(shortcutChangeDispatcher));
|
||||
import.meta.hot?.dispose(() => clearInterval(shortcutChangeDispatcher));
|
||||
|
||||
function markDirtyThrottled ()
|
||||
{
|
||||
|
|
@ -50,7 +50,7 @@ function markDirtyThrottled ()
|
|||
}
|
||||
|
||||
window.addEventListener('focuschanged', markDirtyThrottled);
|
||||
import.meta.hot.dispose(() => window.removeEventListener('focuschanged', markDirtyThrottled));
|
||||
import.meta.hot?.dispose(() => window.removeEventListener('focuschanged', markDirtyThrottled));
|
||||
|
||||
export function useShortcutContext ()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ const handleResize = () =>
|
|||
settingsApi.api.settings({ id: 'windowSize' }).post({ value: { width: window.innerWidth, height: window.innerHeight } });
|
||||
};
|
||||
window.addEventListener("resize", handleResize);
|
||||
import.meta.hot.dispose(() => window.removeEventListener('resize', handleResize));
|
||||
import.meta.hot?.dispose(() => window.removeEventListener('resize', handleResize));
|
||||
|
||||
let lastWindowPosX: number = window.screenX;
|
||||
let lastWindowPosY: number = window.screenY;
|
||||
|
|
@ -19,4 +19,4 @@ var screenPositionInternal: NodeJS.Timeout = setInterval(() =>
|
|||
lastWindowPosX = window.screenX;
|
||||
lastWindowPosY = window.screenY;
|
||||
}, 1000);
|
||||
import.meta.hot.dispose(() => clearInterval(screenPositionInternal));
|
||||
import.meta.hot?.dispose(() => clearInterval(screenPositionInternal));
|
||||
|
|
@ -24,10 +24,9 @@ export interface GameMeta
|
|||
export const SettingsSchema = z.object({
|
||||
rommAddress: z.url().optional(),
|
||||
rommUser: z.string().default('admin').optional(),
|
||||
disableBlur: z.boolean().default(false),
|
||||
windowSize: z.object({ width: z.number(), height: z.number() }).default({ width: 1280, height: 800 }),
|
||||
windowSize: z.object({ width: z.number(), height: z.number() }).optional(),
|
||||
windowPosition: z.object({ x: z.number(), y: z.number() }).optional(),
|
||||
downloadPath: z.string().default('./downloads')
|
||||
downloadPath: z.string()
|
||||
});
|
||||
|
||||
export const GameListFilterSchema = z.object({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue