feat: Bundled NW.js with appimages

feat: Implemented self update
feat: Added rclone saves for emulators
fix: Fixed auto focus in builds
feat: Added helper cards on empty library
This commit is contained in:
Simeon Radivoev 2026-04-26 03:26:15 +03:00
parent 587956c792
commit 813785f4f3
Signed by: simeonradivoev
GPG key ID: C16C2132A7660C8E
59 changed files with 1210 additions and 480 deletions

View file

@ -1,7 +1,7 @@
import z from "zod";
import { IJob, JobContext } from "../task-queue";
import { ActiveGameSchema, ActiveGameType } from "@/bun/types/typesc.schema";
import { db, events, plugins } from "../app";
import { config, db, events, plugins } from "../app";
import * as appSchema from "@schema/app";
import { eq } from "drizzle-orm";
import { spawn } from 'node:child_process';
@ -51,6 +51,7 @@ export class LaunchGameJob implements IJob<z.infer<typeof LaunchGameJob.dataSche
command: this.validCommand,
changedSaveFiles: Array.from(this.changedSaveFiles.values()),
validChangedSaveFiles: {},
saveFolderSlots: this.saveSlots,
gameInfo
}).catch(e =>
{
@ -129,31 +130,41 @@ export class LaunchGameJob implements IJob<z.infer<typeof LaunchGameJob.dataSche
if (Array.isArray(this.validCommand.command))
{
const bunGame = Bun.spawn(this.validCommand.command, {
let command = this.validCommand.command;
if (process.env.FLATPAK_BUILD) command = ['flatpak-spawn', '--host', `--directory=${config.get('downloadPath')}`, ...command];
const bunGame = Bun.spawn(command, {
cwd: this.validCommand.startDir,
signal: context.abortSignal,
env: {
...process.env,
...this.validCommand.env
}
},
onExit (subprocess, exitCode, signalCode, error)
{
if (error)
{
console.error(error);
reject(error);
} else
{
resolve(true);
}
},
});
context.setProgress(0, "playing");
bunGame.exited.then(e =>
{
resolve(true);
}).catch(e =>
{
console.error(e);
reject(e);
});
game = bunGame;
} else
{
let command = this.validCommand.command;
if (process.env.FLATPAK_BUILD) command = `flatpak-spawn --host --directory=${config.get('downloadPath')} ${command}`;
// ES-DE commands require shell execution. Some emulators fail otherwise.
const spawnGame = spawn(this.validCommand.command, {
const spawnGame = spawn(command, {
shell: this.validCommand.shell ?? true,
cwd: this.validCommand.startDir,
signal: context.abortSignal,
@ -178,7 +189,6 @@ export class LaunchGameJob implements IJob<z.infer<typeof LaunchGameJob.dataSche
game = spawnGame;
}
}
else if (this.validCommand.metadata.emulatorBin)
{
@ -186,14 +196,28 @@ export class LaunchGameJob implements IJob<z.infer<typeof LaunchGameJob.dataSche
await this.prePlay(context.setProgress.bind(context), { platformSlug: gameInfo?.platformSlug });
let command = [this.validCommand.metadata.emulatorBin, ...commandArgs.args];
if (process.env.FLATPAK_BUILD) command = ['flatpak-spawn', '--host', `--directory=${config.get('downloadPath')}`, ...command];
// We have full control over launching integrated emulators better to use bun spawn
const bunGame = Bun.spawn([this.validCommand.metadata.emulatorBin, ...commandArgs.args], {
const bunGame = Bun.spawn(command, {
cwd: this.validCommand.startDir,
signal: context.abortSignal,
env: {
...process.env,
...commandArgs.env
}
},
onExit (subprocess, exitCode, signalCode, error)
{
if (error)
{
console.error(error);
reject(error);
} else
{
resolve(true);
}
},
});
context.setProgress(0, "playing");
@ -219,15 +243,6 @@ export class LaunchGameJob implements IJob<z.infer<typeof LaunchGameJob.dataSche
});
}*/
bunGame.exited.then(e =>
{
resolve(true);
}).catch(e =>
{
console.error(e);
reject(e);
});
game = bunGame;
} else

View file

@ -0,0 +1,118 @@
import z from "zod";
import { IJob, JobContext } from "../task-queue";
import { cleanPromise, cleanup, events, plugins } from "../app";
import fs from 'fs/promises';
import { Downloader } from "@/bun/utils/downloader";
import path from 'node:path';
import os from "node:os";
import winUpdateScript from '@/bun/utils/update-gameflow-windows.bat' with { type: "text" };
import linuxUpdateScript from '@/bun/utils/update-gameflow-linux.sh' with { type: "text" };
import mustache from "mustache";
import pkg from '~/package.json';
import { sleep } from "bun";
export default class SelfUpdateJob implements IJob<never, string>
{
static id = "self-update-job" as const;
static dataSchema = z.never();
group = "self-update";
async downloadUpdate (url: URL, dest: string | undefined, filename: string, ctx: JobContext<IJob<never, string>, never, string>)
{
const downloader = new Downloader('update',
[{
url: url,
file_path: "",
file_name: filename
}],
dest,
{
onProgress (stats)
{
ctx.setProgress(stats.progress, "Downloading Update");
},
});
return downloader.start();
}
async start (context: JobContext<IJob<never, string>, never, string>)
{
context.setProgress(0, "Downloading Update");
await sleep(1000);
const latest = await fetch('https://api.github.com/repos/simeonradivoev/gameflow-deck/releases/latest');
if (latest.ok)
{
const data = await latest.json();
let validAsset: any | undefined;
switch (process.platform)
{
case "win32":
validAsset = data.assets.find((e: any) => new Bun.Glob(`Gameflow-${process.platform}-${process.arch}.zip`).match(e.name));
break;
case "linux":
validAsset = data.assets.find((e: any) => new Bun.Glob(`Gameflow-${process.platform}-${process.arch}.AppImage`).match(e.name));
if (!validAsset)
{
validAsset = data.assets.find((e: any) => new Bun.Glob(`*.AppImage`).match(e.name));
}
break;
default:
events.emit('notification', { message: "Unsupported Platfrom", title: 'Failed Update', type: "error" });
return;
}
if (!validAsset)
{
events.emit('notification', { message: "Could not find download", title: 'Failed Update', type: "error" });
return;
}
console.log("Found Download", validAsset.browser_download_url);
console.log("Starting Download");
switch (process.platform)
{
case "linux":
const appimage = process.env.APPIMAGE;
if (!appimage)
{
events.emit('notification', {
message: "Only AppImage supported",
title: 'Failed Update',
type: 'error'
});
return;
}
const linuxDownloads = await this.downloadUpdate(new URL(validAsset.browser_download_url), undefined, path.basename(appimage), context);
if (!linuxDownloads) return;
const shPath = path.join(os.tmpdir(), "update-gameflow.sh");
await Bun.write(shPath, mustache.render(linuxUpdateScript, {
tempFile: linuxDownloads[0],
appImagePath: appimage
}));
context.setProgress(0, "Restarting App To Update");
events.emit('exitapp');
Bun.spawn(["bash", shPath], { detached: true });
process.exit(0);
case "win32":
const winDownloads = await this.downloadUpdate(new URL(validAsset.browser_download_url), undefined, "Gameflow-update.zip", context);
if (!winDownloads) return;
const batPath = path.join(os.tmpdir(), "update-gameflow.bat");
await Bun.write(batPath, mustache.render(winUpdateScript, {
tempFile: winDownloads[0],
extractDir: path.dirname(process.execPath),
exePath: `${pkg.bin}.exe`
}));
context.setProgress(0, "Restarting App To Update");
await cleanup();
events.emit('exitapp');
Bun.spawn(["cmd", "/c", batPath], { detached: true });
process.exit(0);
}
} else
{
events.emit('notification', { message: latest.statusText, title: 'Failed Update', type: "error" });
}
}
}