feat: added update notes and moved update to own tab
feat: added update info for emulators
This commit is contained in:
parent
813785f4f3
commit
cf84f40a17
14 changed files with 318 additions and 34 deletions
|
|
@ -20,9 +20,12 @@ import SelfUpdateJob from "./jobs/self-update-job";
|
|||
async function checkUpdate (force?: boolean)
|
||||
{
|
||||
const latest = await getOrCachedGithubRelease('simeonradivoev/gameflow-deck', force);
|
||||
if (!latest || !latest.tag_name) return { hasUpdate: 0, version: getAppVersion() };
|
||||
if (!latest || !latest.tag_name) return {
|
||||
hasUpdate: 0,
|
||||
version: getAppVersion()
|
||||
};
|
||||
const hasUpdate = semver.order(latest.tag_name, getAppVersion());
|
||||
return { hasUpdate, version: latest.tag_name };
|
||||
return { hasUpdate, version: latest.tag_name, info: latest.body };
|
||||
}
|
||||
|
||||
export const system = new Elysia({ prefix: '/api/system' })
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ async function shutdown (code: number)
|
|||
|
||||
process.on("SIGINT", () => shutdown(0));
|
||||
process.on("SIGTERM", () => shutdown(0));
|
||||
process.on('SIGUSR1', () => shutdown(3));
|
||||
|
||||
if (process.env.HEADLESS)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ function UpdateStatus ()
|
|||
{
|
||||
const handleSelect = () =>
|
||||
{
|
||||
navigate({ to: '/settings/about' });
|
||||
navigate({ to: '/settings/update' });
|
||||
};
|
||||
const hasUnread = false;
|
||||
const navigate = useNavigate();
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { Route as rootRouteImport } from './../routes/__root'
|
|||
import { Route as GamesRouteImport } from './../routes/games'
|
||||
import { Route as SettingsRouteRouteImport } from './../routes/settings/route'
|
||||
import { Route as IndexRouteImport } from './../routes/index'
|
||||
import { Route as SettingsUpdateRouteImport } from './../routes/settings/update'
|
||||
import { Route as SettingsPluginsRouteImport } from './../routes/settings/plugins'
|
||||
import { Route as SettingsInterfaceRouteImport } from './../routes/settings/interface'
|
||||
import { Route as SettingsEmulatorsRouteImport } from './../routes/settings/emulators'
|
||||
|
|
@ -45,6 +46,11 @@ const IndexRoute = IndexRouteImport.update({
|
|||
path: '/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const SettingsUpdateRoute = SettingsUpdateRouteImport.update({
|
||||
id: '/update',
|
||||
path: '/update',
|
||||
getParentRoute: () => SettingsRouteRoute,
|
||||
} as any)
|
||||
const SettingsPluginsRoute = SettingsPluginsRouteImport.update({
|
||||
id: '/plugins',
|
||||
path: '/plugins',
|
||||
|
|
@ -142,6 +148,7 @@ export interface FileRoutesByFullPath {
|
|||
'/settings/emulators': typeof SettingsEmulatorsRoute
|
||||
'/settings/interface': typeof SettingsInterfaceRoute
|
||||
'/settings/plugins': typeof SettingsPluginsRoute
|
||||
'/settings/update': typeof SettingsUpdateRoute
|
||||
'/collection/$source/$id': typeof CollectionSourceIdRoute
|
||||
'/embedded/$source/$id': typeof EmbeddedSourceIdRoute
|
||||
'/game/$source/$id': typeof GameSourceIdRoute
|
||||
|
|
@ -163,6 +170,7 @@ export interface FileRoutesByTo {
|
|||
'/settings/emulators': typeof SettingsEmulatorsRoute
|
||||
'/settings/interface': typeof SettingsInterfaceRoute
|
||||
'/settings/plugins': typeof SettingsPluginsRoute
|
||||
'/settings/update': typeof SettingsUpdateRoute
|
||||
'/collection/$source/$id': typeof CollectionSourceIdRoute
|
||||
'/embedded/$source/$id': typeof EmbeddedSourceIdRoute
|
||||
'/game/$source/$id': typeof GameSourceIdRoute
|
||||
|
|
@ -186,6 +194,7 @@ export interface FileRoutesById {
|
|||
'/settings/emulators': typeof SettingsEmulatorsRoute
|
||||
'/settings/interface': typeof SettingsInterfaceRoute
|
||||
'/settings/plugins': typeof SettingsPluginsRoute
|
||||
'/settings/update': typeof SettingsUpdateRoute
|
||||
'/collection/$source/$id': typeof CollectionSourceIdRoute
|
||||
'/embedded/$source/$id': typeof EmbeddedSourceIdRoute
|
||||
'/game/$source/$id': typeof GameSourceIdRoute
|
||||
|
|
@ -210,6 +219,7 @@ export interface FileRouteTypes {
|
|||
| '/settings/emulators'
|
||||
| '/settings/interface'
|
||||
| '/settings/plugins'
|
||||
| '/settings/update'
|
||||
| '/collection/$source/$id'
|
||||
| '/embedded/$source/$id'
|
||||
| '/game/$source/$id'
|
||||
|
|
@ -231,6 +241,7 @@ export interface FileRouteTypes {
|
|||
| '/settings/emulators'
|
||||
| '/settings/interface'
|
||||
| '/settings/plugins'
|
||||
| '/settings/update'
|
||||
| '/collection/$source/$id'
|
||||
| '/embedded/$source/$id'
|
||||
| '/game/$source/$id'
|
||||
|
|
@ -253,6 +264,7 @@ export interface FileRouteTypes {
|
|||
| '/settings/emulators'
|
||||
| '/settings/interface'
|
||||
| '/settings/plugins'
|
||||
| '/settings/update'
|
||||
| '/collection/$source/$id'
|
||||
| '/embedded/$source/$id'
|
||||
| '/game/$source/$id'
|
||||
|
|
@ -301,6 +313,13 @@ declare module '@tanstack/react-router' {
|
|||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/settings/update': {
|
||||
id: '/settings/update'
|
||||
path: '/update'
|
||||
fullPath: '/settings/update'
|
||||
preLoaderRoute: typeof SettingsUpdateRouteImport
|
||||
parentRoute: typeof SettingsRouteRoute
|
||||
}
|
||||
'/settings/plugins': {
|
||||
id: '/settings/plugins'
|
||||
path: '/plugins'
|
||||
|
|
@ -430,6 +449,7 @@ interface SettingsRouteRouteChildren {
|
|||
SettingsEmulatorsRoute: typeof SettingsEmulatorsRoute
|
||||
SettingsInterfaceRoute: typeof SettingsInterfaceRoute
|
||||
SettingsPluginsRoute: typeof SettingsPluginsRoute
|
||||
SettingsUpdateRoute: typeof SettingsUpdateRoute
|
||||
SettingsPluginSourceRoute: typeof SettingsPluginSourceRoute
|
||||
}
|
||||
|
||||
|
|
@ -440,6 +460,7 @@ const SettingsRouteRouteChildren: SettingsRouteRouteChildren = {
|
|||
SettingsEmulatorsRoute: SettingsEmulatorsRoute,
|
||||
SettingsInterfaceRoute: SettingsInterfaceRoute,
|
||||
SettingsPluginsRoute: SettingsPluginsRoute,
|
||||
SettingsUpdateRoute: SettingsUpdateRoute,
|
||||
SettingsPluginSourceRoute: SettingsPluginSourceRoute,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
@import "tailwindcss";
|
||||
@import 'animate.css';
|
||||
@plugin "daisyui";
|
||||
@plugin "@tailwindcss/typography";
|
||||
|
||||
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
|
||||
@custom-variant light (&:where([data-theme=light], [data-theme=light] *));
|
||||
|
|
|
|||
|
|
@ -16,15 +16,7 @@ function RouteComponent ()
|
|||
{
|
||||
const { data: systemInfo } = useQuery(systemInfoQuery);
|
||||
const { ref, focusKey } = useFocusable({ focusKey: 'about-section' });
|
||||
const { data: hasUpdate, refetch: refetchHasUpdate } = useQuery(hasUpdateQuery);
|
||||
const update = useMutation(updateMutation);
|
||||
const forceCheckUpdate = useMutation({
|
||||
...checkUpdateMutation,
|
||||
onSuccess (data, variables, onMutateResult, context)
|
||||
{
|
||||
refetchHasUpdate();
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
return <table ref={ref} className="table">
|
||||
|
||||
|
|
@ -34,17 +26,6 @@ function RouteComponent ()
|
|||
<th>Version</th>
|
||||
<td>{systemInfo?.data?.version}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Update</th>
|
||||
<td className='flex flex-flex-wrap gap-2'>
|
||||
{
|
||||
hasUpdate && hasUpdate.hasUpdate > 0 ?
|
||||
<Button className='gap-3' style='warning' id='update-btn' onAction={() => update.mutate()}><CircleFadingArrowUp /> Update to {hasUpdate?.version}</Button> :
|
||||
<Button className='gap-3' id='update-btn' onAction={() => forceCheckUpdate.mutate()}>{forceCheckUpdate.isPending ? <span className="loading loading-spinner loading-lg"></span> : <RefreshCcw />}Check for Update</Button>
|
||||
}
|
||||
{<Button className='gap-3' id='force-update-btn' onAction={() => update.mutate()}><CircleFadingArrowUp /> Force Update</Button>}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Agent</th>
|
||||
<td>{navigator.userAgent}</td>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import
|
|||
Joystick,
|
||||
MonitorCog,
|
||||
Puzzle,
|
||||
RefreshCcw,
|
||||
} from "lucide-react";
|
||||
import { JSX, useMemo } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
|
@ -166,6 +167,12 @@ function SettingsMenu (data: {})
|
|||
label="Directories"
|
||||
icon={<HardDrive />}
|
||||
/>
|
||||
<MenuItem
|
||||
focusSelect
|
||||
route="/settings/update"
|
||||
label="Updates"
|
||||
icon={<RefreshCcw />}
|
||||
/>
|
||||
<MenuItem
|
||||
focusSelect
|
||||
route="/settings/about"
|
||||
|
|
|
|||
75
src/mainview/routes/settings/update.tsx
Normal file
75
src/mainview/routes/settings/update.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { AutoFocus } from '@/mainview/components/AutoFocus';
|
||||
import DotsLoading from '@/mainview/components/backgrounds/dots';
|
||||
import { Button } from '@/mainview/components/options/Button';
|
||||
import { checkUpdateMutation, hasUpdateQuery, updateMutation } from '@/mainview/scripts/queries/system';
|
||||
import { FocusContext, useFocusable } from '@noriginmedia/norigin-spatial-navigation';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import { CircleFadingArrowUp, RefreshCcw } from 'lucide-react';
|
||||
import { MarkdownAsync } from 'react-markdown';
|
||||
|
||||
export const Route = createFileRoute('/settings/update')({
|
||||
component: RouteComponent,
|
||||
pendingComponent: Loading,
|
||||
async loader (ctx)
|
||||
{
|
||||
const data = await ctx.context.queryClient.fetchQuery(hasUpdateQuery);
|
||||
return { data: data };
|
||||
},
|
||||
});
|
||||
|
||||
function Loading ()
|
||||
{
|
||||
const { ref, focusSelf } = useFocusable({ focusKey: 'updates' });
|
||||
return <>
|
||||
<DotsLoading ref={ref} />
|
||||
<AutoFocus focus={focusSelf} />
|
||||
</>;
|
||||
}
|
||||
|
||||
function RouteComponent ()
|
||||
{
|
||||
const { data } = Route.useLoaderData();
|
||||
const navigate = useNavigate();
|
||||
const update = useMutation(updateMutation);
|
||||
const forceCheckUpdate = useMutation({
|
||||
...checkUpdateMutation,
|
||||
onSuccess (data, variables, onMutateResult, context)
|
||||
{
|
||||
context.client.invalidateQueries(hasUpdateQuery);
|
||||
navigate({ to: '/settings/update', replace: true });
|
||||
},
|
||||
});
|
||||
const { ref, focusKey } = useFocusable({ focusKey: 'updates' });
|
||||
return <div ref={ref}>
|
||||
<FocusContext value={focusKey}>
|
||||
<h1 className='text-2xl text-center'>Version: {data.version}</h1>
|
||||
<div className='flex flex-flex-wrap gap-2'>
|
||||
{
|
||||
data.hasUpdate > 0 ?
|
||||
<Button className='gap-3' style='warning' id='update-btn' onAction={() => update.mutate()}><CircleFadingArrowUp /> Update to {data.version}</Button> :
|
||||
<Button className='gap-3' id='update-btn' onAction={() => forceCheckUpdate.mutate()}>{forceCheckUpdate.isPending ? <span className="loading loading-spinner loading-lg"></span> : <RefreshCcw />}Check for Update</Button>
|
||||
}
|
||||
{<Button className='gap-3' id='force-update-btn' onAction={() => update.mutate()}><CircleFadingArrowUp /> Force Update</Button>}
|
||||
</div>
|
||||
<div className="divider">Version Info</div>
|
||||
<div className="prose lg:prose-xl">
|
||||
<MarkdownAsync components={{
|
||||
a ({ node, children, ...props })
|
||||
{
|
||||
try
|
||||
{
|
||||
new URL(props.href ?? "");
|
||||
// If we don't get an error, then it's an absolute URL.
|
||||
|
||||
props.target = "_blank";
|
||||
props.rel = "noopener noreferrer";
|
||||
} catch (e) { }
|
||||
|
||||
return <a {...props}>{children}</a>;
|
||||
},
|
||||
}} >{data.info}</MarkdownAsync>
|
||||
</div>
|
||||
</FocusContext>
|
||||
</div>;
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { useRef } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import
|
||||
{
|
||||
useFocusable,
|
||||
|
|
@ -27,6 +27,8 @@ import { deleteBiosMutation, downloadBiosMutation, installEmulatorMutation, stor
|
|||
import { gamesRecommendedBasedOnEmulatorQuery } from "@queries/romm";
|
||||
import FocusTooltip from "@/mainview/components/FocusTooltip";
|
||||
import { AutoFocus } from "@/mainview/components/AutoFocus";
|
||||
import { FilterUI } from "@/mainview/components/Filters";
|
||||
import Markdown from "react-markdown";
|
||||
|
||||
export const Route = createFileRoute('/store/details/emulator/$id')({
|
||||
component: RouteComponent,
|
||||
|
|
@ -330,6 +332,16 @@ const capabilityIconMap: Record<string, any> = {
|
|||
batch: <Terminal />
|
||||
};
|
||||
|
||||
function InfoTabs (data: { tabs: Record<string, FilterOption>, selectTab: (v: string) => void; })
|
||||
{
|
||||
const { ref, focusKey } = useFocusable({ focusKey: 'emulator-info-tabs-section', onFocus: (l, p, d) => scrollIntoViewHandler({ block: 'start' })(focusKey, ref.current, d) });
|
||||
return <div ref={ref} className="divider scroll-mt-48">
|
||||
<FocusContext value={focusKey}>
|
||||
<FilterUI id="emulator-info-tabs" options={data.tabs} setSelected={v => data.selectTab(v)} />
|
||||
</FocusContext>
|
||||
</div>;
|
||||
}
|
||||
|
||||
export function RouteComponent ()
|
||||
{
|
||||
const { id } = Route.useParams();
|
||||
|
|
@ -343,6 +355,7 @@ export function RouteComponent ()
|
|||
const { data: emulator, isPending: isEmulatorPending } = useQuery(storeEmulatorDetailsQuery(id));
|
||||
const { data: recommendedEmulators } = useQuery(storeEmulatorsRecommendedQuery(id));
|
||||
const { data: recommendedGames } = useQuery(gamesRecommendedBasedOnEmulatorQuery(id));
|
||||
const [infoTab, setInfoTab] = useState("stats");
|
||||
|
||||
useShortcuts(focusKey, () => [{
|
||||
label: "Return",
|
||||
|
|
@ -389,7 +402,15 @@ export function RouteComponent ()
|
|||
stats.push({
|
||||
label: "Bios", content: emulator.bios && emulator.bios.length > 0 ? emulator.bios : <div className="text-warning font-semibold">Missing</div>
|
||||
});
|
||||
}
|
||||
|
||||
const infoTabs: Record<string, FilterOption> = {
|
||||
stats: { label: "Stats", selected: infoTab === 'stats', icon: <Info /> },
|
||||
};
|
||||
|
||||
if (emulator?.storeDownloadInfo?.hasUpdate)
|
||||
{
|
||||
infoTabs.update = { label: "Update", icon: <CircleFadingArrowUp />, selected: infoTab === 'update' };
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -411,8 +432,9 @@ export function RouteComponent ()
|
|||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col bg-base-100 py-4 gap-12 z-10">
|
||||
<div className="divider"> <Info className="size-12" /> Stats</div>
|
||||
<StatList id="emulator-details-stats" stats={stats} onFocus={scrollIntoViewHandler({ block: 'center' })} />
|
||||
<InfoTabs tabs={infoTabs} selectTab={setInfoTab} />
|
||||
{infoTab === 'stats' && <StatList id="emulator-details-stats" stats={stats} focusable={false} />}
|
||||
{infoTab === 'update' && <Markdown>{emulator?.storeDownloadInfo?.description}</Markdown>}
|
||||
{recommendedEmulators && <div className="relative bg-base-200">
|
||||
<div className="bg-dots z-0"></div>
|
||||
<EmulatorsSection
|
||||
|
|
|
|||
2
src/shared/types..d.ts
vendored
2
src/shared/types..d.ts
vendored
|
|
@ -38,7 +38,7 @@ declare interface FrontEndEmulatorDetailed extends FrontEndEmulator
|
|||
screenshots: string[];
|
||||
biosRequirement?: "required" | "optional";
|
||||
bios?: string[];
|
||||
storeDownloadInfo?: { hasUpdate: boolean; version?: string, type: string; };
|
||||
storeDownloadInfo?: { hasUpdate: boolean; version?: string, type: string; description?: string; };
|
||||
}
|
||||
|
||||
declare interface FrontEndGameTypeDetailedAchievement
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue