-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Add game creation form & handlers. - Allow specifying empty account ID to create new vanilla account when launching. - Extract game listing function as hook. - Remove game list refresh button. - Separate monitor list and each instances. - Rename game process service.
Showing
18 changed files
with
668 additions
and
357 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,17 @@ | ||
import { VanillaAccount } from "@/main/auth/vanilla"; | ||
import { ipcMain } from "@/main/ipc/typed"; | ||
import { reg } from "@/main/registry/registry"; | ||
|
||
ipcMain.handle("gameAuth", (_, gameId) => { | ||
ipcMain.handle("gameAuth", async (_, gameId) => { | ||
const g = reg.games.get(gameId); | ||
const a = reg.accounts.get(g.launchHint.accountId); | ||
return a.refresh(); | ||
|
||
const a = g.launchHint.accountId ? reg.accounts.get(g.launchHint.accountId) : new VanillaAccount(); | ||
|
||
const success = await a.refresh(); | ||
|
||
if (success) { | ||
g.launchHint.accountId = a.uuid; | ||
} | ||
|
||
return success; | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import { useGameList } from "@/renderer/services/game"; | ||
import { Select, SelectItem, type SharedSelection } from "@heroui/react"; | ||
import { useTranslation } from "react-i18next"; | ||
|
||
interface ContainerSelectorProps { | ||
enabled: boolean; | ||
containerId?: string; | ||
onChange: (containerId?: string) => void; | ||
} | ||
|
||
export function ContainerSelector({ enabled, containerId, onChange }: ContainerSelectorProps) { | ||
const { t } = useTranslation("pages", { keyPrefix: "create-game" }); | ||
|
||
const games = useGameList(); | ||
|
||
const sid = games.find(g => g.launchHint.containerId === containerId)?.id; | ||
|
||
function handleSelectionChange(s: SharedSelection) { | ||
if (s instanceof Set) { | ||
const gid = [...s][0]; | ||
if (!gid) { | ||
onChange(undefined); | ||
} else { | ||
onChange(games.find(g => g.id === gid)?.launchHint.containerId); | ||
} | ||
} | ||
} | ||
|
||
return <Select | ||
isDisabled={!enabled} | ||
label={t("container-select-title")} | ||
placeholder={t("version-select-placeholder")} | ||
selectedKeys={sid ? [sid] : []} | ||
onSelectionChange={handleSelectionChange} | ||
> | ||
{games.map(g => <SelectItem key={g.id}>{g.name}</SelectItem>)} | ||
</Select>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
import { Alert } from "@components/Alert"; | ||
import { Radio, RadioGroup } from "@heroui/radio"; | ||
import { Button, Input } from "@heroui/react"; | ||
import { ContainerSelector } from "@pages/create-game/ContainerSelector"; | ||
import { VersionSelector } from "@pages/create-game/VersionSelector"; | ||
import React, { useState } from "react"; | ||
import { useTranslation } from "react-i18next"; | ||
import { toast } from "react-toastify"; | ||
import { useLocation } from "wouter"; | ||
|
||
|
||
/** | ||
* Create new game without the help of the wizard. | ||
*/ | ||
export function CreateGameView() { | ||
const { t } = useTranslation("pages", { keyPrefix: "create-game" }); | ||
|
||
const [gameName, setGameName] = useState(t("default-name", { date: new Date().toLocaleString() })); | ||
const [gameVersion, setGameVersion] = useState<string>(); | ||
const [containerId, setContainerId] = useState<string>(); | ||
const [shareContainer, setShareContainer] = useState(false); | ||
|
||
const [creating, setCreating] = useState(false); | ||
const [, nav] = useLocation(); | ||
|
||
|
||
const valid = gameVersion && (!shareContainer || (shareContainer && containerId)); | ||
|
||
async function handleCreate() { | ||
if (valid) { | ||
setCreating(true); | ||
// TODO add error handler | ||
await native.game.add({ | ||
name: gameName, | ||
containerId, | ||
profileId: gameVersion | ||
}); | ||
setCreating(false); | ||
toast(t("toast-created"), { type: "success" }); | ||
nav("/games"); | ||
} | ||
} | ||
|
||
return <div className="w-11/12 h-full mx-auto flex flex-col items-center gap-4"> | ||
<div className="font-bold text-2xl">{t("title")}</div> | ||
<div className="w-full grow flex flex-col justify-between mt-4"> | ||
<div className="flex flex-col gap-4"> | ||
<div className="font-bold text-xl">{t("name-input-title")}</div> | ||
<Input aria-label="Game Name" value={gameName} onValueChange={setGameName}/> | ||
</div> | ||
|
||
<div className="flex flex-col gap-4"> | ||
<div className="font-bold text-xl">{t("version-select-title")}</div> | ||
<VersionSelector version={gameVersion} onChange={setGameVersion}/> | ||
</div> | ||
|
||
|
||
<div className="flex flex-col gap-4"> | ||
<div className="font-bold text-xl">{t("storage-title")}</div> | ||
|
||
<RadioGroup | ||
orientation="horizontal" | ||
value={shareContainer ? "share" : "new"} | ||
onValueChange={v => setShareContainer(v === "share")} | ||
> | ||
<Radio value="new">{t("storage-policy.new")}</Radio> | ||
<Radio value="share">{t("storage-policy.share")}</Radio> | ||
</RadioGroup> | ||
|
||
<ContainerSelector enabled={shareContainer} containerId={containerId} onChange={setContainerId}/> | ||
</div> | ||
|
||
|
||
<div className="flex flex-col gap-4"> | ||
<div className="font-bold text-xl">{t("account-title")}</div> | ||
|
||
<Alert | ||
classNames={{ title: "font-bold" }} | ||
title={t("account-tip")} | ||
/> | ||
</div> | ||
|
||
<Button | ||
fullWidth | ||
color="primary" | ||
size="lg" | ||
isDisabled={!valid} | ||
onPress={handleCreate} | ||
isLoading={creating} | ||
> | ||
{t("create-btn")} | ||
</Button> | ||
</div> | ||
</div>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import type { VersionEntry } from "@/main/install/vanilla"; | ||
import { useVersionManifest } from "@/renderer/services/sources"; | ||
import grassBlock from "@assets/img/grass-block.webp"; | ||
import snowyGrassBlock from "@assets/img/snowy-grass-block.webp"; | ||
import { Checkbox, Select, SelectItem, type SharedSelection, Spinner } from "@heroui/react"; | ||
import { DotIcon } from "lucide-react"; | ||
import React, { useState } from "react"; | ||
import { useTranslation } from "react-i18next"; | ||
|
||
interface VersionSelectorProps { | ||
version?: string; | ||
onChange: (version?: string) => void; | ||
} | ||
|
||
export function VersionSelector({ version, onChange }: VersionSelectorProps) { | ||
const vm = useVersionManifest(); | ||
const { t } = useTranslation("pages", { keyPrefix: "create-game.version-select" }); | ||
const [showSnapshots, setShowSnapshots] = useState(false); | ||
|
||
if (!vm) return <VersionLoading/>; | ||
|
||
let versions = vm.versions; | ||
|
||
if (!showSnapshots) { | ||
versions = vm.versions.filter(v => v.type !== "snapshot"); | ||
} | ||
|
||
function handleSelectionChange(sel: SharedSelection) { | ||
if (sel instanceof Set) { | ||
const s = [...sel]; | ||
onChange(s[0]?.toString()); // Might be null when no selection | ||
} | ||
} | ||
|
||
return <div className="items-center flex gap-4"> | ||
<div className="grow"> | ||
<Select | ||
isVirtualized | ||
aria-label="Select Version" | ||
selectedKeys={version ? [version] : []} | ||
placeholder={t("placeholder")} | ||
onSelectionChange={handleSelectionChange} | ||
scrollShadowProps={{ | ||
style: { | ||
// @ts-expect-error Non-standard properties | ||
"--scroll-shadow-size": "0px" | ||
} | ||
}} | ||
itemHeight={64} | ||
> | ||
{ | ||
versions.map(v => { | ||
return <SelectItem key={v.id} textValue={v.id}> | ||
<VersionContent version={v}/> | ||
</SelectItem>; | ||
}) | ||
} | ||
</Select> | ||
</div> | ||
|
||
<div> | ||
<Checkbox checked={showSnapshots} onValueChange={setShowSnapshots}>包含快照</Checkbox> | ||
</div> | ||
|
||
</div>; | ||
} | ||
|
||
function VersionContent({ version: { id, type, sha1, releaseTime } }: { version: VersionEntry }) { | ||
const src = type === "release" ? grassBlock : snowyGrassBlock; | ||
|
||
return <div className="flex h-[64px] items-center gap-4 py-2"> | ||
<div className="h-full rounded-full bg-content2 p-2"> | ||
<img src={src} alt="version" className="w-full h-full object-contain"/> | ||
</div> | ||
|
||
<div className="flex flex-col"> | ||
<div className="font-bold text-lg">{id}</div> | ||
<div className="flex items-center text-foreground-400 text-sm"> | ||
{sha1} | ||
<DotIcon/> | ||
{new Date(releaseTime).toLocaleString()} | ||
</div> | ||
</div> | ||
</div>; | ||
} | ||
|
||
function VersionLoading() { | ||
const { t } = useTranslation("pages", { keyPrefix: "create-game" }); | ||
|
||
return <div className="flex gap-4 justify-center items-center"> | ||
<Spinner size="sm"/> | ||
<span className="text-foreground-400"> | ||
{t("loading-versions")} | ||
</span> | ||
</div>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import { type RemoteGameProcess, type RemoteGameStatus, useGameProcList } from "@/renderer/services/proc"; | ||
import { GameTypeImage } from "@components/GameTypeImage"; | ||
import { Button, Card, CardBody, Chip } from "@heroui/react"; | ||
import { ArrowRightIcon } from "lucide-react"; | ||
import { useTranslation } from "react-i18next"; | ||
import { useLocation } from "wouter"; | ||
|
||
export function MonitorListView() { | ||
const procs = useGameProcList(); | ||
|
||
return <div className="w-full h-full overflow-y-auto"> | ||
<div className="grid grid-cols-2 gap-4 w-full"> | ||
{ | ||
procs.map(p => <MonitorItem proc={p} key={p.id}/>) | ||
} | ||
</div> | ||
</div>; | ||
} | ||
|
||
const statusColors = { | ||
running: "success", | ||
crashed: "danger", | ||
exited: "default" | ||
} as const; | ||
|
||
function StatusChip({ status }: { status: RemoteGameStatus }) { | ||
const { t } = useTranslation("pages", { keyPrefix: "monitor.status" }); | ||
|
||
return <Chip | ||
variant="dot" | ||
color={statusColors[status]} | ||
> | ||
{t(status)} | ||
</Chip>; | ||
} | ||
|
||
function MonitorItem({ proc }: { proc: RemoteGameProcess }) { | ||
const { detail: { modLoader, stable, name, versionId }, status } = proc; | ||
const [, nav] = useLocation(); | ||
|
||
function revealProc() { | ||
nav(`/monitor/${proc.id}`); | ||
} | ||
|
||
return <Card> | ||
<CardBody> | ||
<div className="flex gap-4 items-center h-16 px-3"> | ||
<div className="h-full p-3 bg-content2 rounded-full"> | ||
<GameTypeImage loader={modLoader} stable={stable}/> | ||
</div> | ||
|
||
<div className="flex flex-col gap-1"> | ||
<div className="font-bold text-xl">{name}</div> | ||
<div className="text-foreground-400">{versionId}</div> | ||
</div> | ||
|
||
<div className="ml-auto flex gap-2 items-center"> | ||
<StatusChip status={status}/> | ||
</div> | ||
|
||
|
||
<div className="ml-4"> | ||
<Button isIconOnly color="primary" onPress={revealProc}> | ||
<ArrowRightIcon/> | ||
</Button> | ||
</div> | ||
</div> | ||
</CardBody> | ||
</Card>; | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,76 +1,265 @@ | ||
import { type RemoteGameProcess, type RemoteGameStatus, useGameProcList } from "@/renderer/lib/remote-game"; | ||
import type { GameProcessLog } from "@/main/launch/log-parser"; | ||
import { type RemoteGameProcess, type RemoteGameStatus, useGameProc } from "@/renderer/services/proc"; | ||
import { GameTypeImage } from "@components/GameTypeImage"; | ||
import { Button, Card, CardBody, Chip } from "@heroui/react"; | ||
import { Monitor } from "@pages/monitor/Monitor"; | ||
import { ArrowRightIcon } from "lucide-react"; | ||
import { Button, Card, CardBody, Popover, PopoverContent, PopoverTrigger, Tab, Tabs } from "@heroui/react"; | ||
import { clsx } from "clsx"; | ||
import { | ||
ArrowLeftIcon, | ||
ArrowRightIcon, | ||
CircleHelpIcon, | ||
CpuIcon, | ||
FileClockIcon, | ||
FolderArchive, | ||
OctagonXIcon | ||
} from "lucide-react"; | ||
import React, { useContext, useEffect, useRef, useState } from "react"; | ||
import { useTranslation } from "react-i18next"; | ||
import { VList, type VListHandle } from "virtua"; | ||
import { useLocation, useParams } from "wouter"; | ||
|
||
export function MonitorView() { | ||
const { procId } = useParams<{ procId: string }>(); | ||
return <Monitor procId={procId} key={procId}/>; | ||
} | ||
|
||
export function MonitorListView() { | ||
const procs = useGameProcList(); | ||
const GameProcessContext = React.createContext<RemoteGameProcess | null>(null); | ||
|
||
return <div className="w-full h-full overflow-y-auto"> | ||
<div className="grid grid-cols-2 gap-4 w-full"> | ||
{ | ||
procs.map(p => <MonitorItem proc={p} key={p.id}/>) | ||
function Monitor({ procId }: { procId: string }) { | ||
const proc = useGameProc(procId, 200); | ||
|
||
return <GameProcessContext.Provider value={proc}> | ||
<div className="w-full h-full flex gap-4 mx-auto"> | ||
<div className="basis-1/4"> | ||
<ControlPanel/> | ||
</div> | ||
<div className="grow flex flex-col"> | ||
<ContentArea/> | ||
</div> | ||
</div> | ||
</GameProcessContext.Provider>; | ||
} | ||
|
||
function ContentArea() { | ||
const { t } = useTranslation("pages", { keyPrefix: "monitor" }); | ||
|
||
return <Tabs radius="full"> | ||
<Tab | ||
className="h-full" | ||
key="logs" | ||
title={ | ||
<div className="flex gap-1 items-center"> | ||
<FileClockIcon/> | ||
{t("log-view")} | ||
</div> | ||
} | ||
> | ||
<LogsDisplay/> | ||
</Tab> | ||
<Tab | ||
className="h-full" | ||
key="stats" | ||
title={ | ||
<div className="flex gap-1 items-center"> | ||
<CpuIcon/> | ||
{t("perf-view")} | ||
</div> | ||
} | ||
> | ||
</Tab> | ||
</Tabs>; | ||
} | ||
|
||
function LogsDisplay() { | ||
const autoScroll = useRef(true); | ||
const prevOffset = useRef(0); | ||
const ref = useRef<VListHandle | null>(null); | ||
const { id: procId, logs } = useContext(GameProcessContext)!; | ||
|
||
function scrollToBottom() { | ||
ref.current?.scrollToIndex(Number.MAX_SAFE_INTEGER, { smooth: true }); | ||
} | ||
|
||
useEffect(() => { | ||
if (autoScroll.current) { | ||
scrollToBottom(); | ||
} | ||
}, [logs]); | ||
|
||
function updateAutoScroll(offset: number) { | ||
if (ref.current) { | ||
if (offset < prevOffset.current) { | ||
// Cancel auto scroll when scrolling up | ||
autoScroll.current = false; | ||
return; | ||
} else if (ref.current.scrollOffset - ref.current.scrollSize + ref.current.viewportSize >= -10) { | ||
// Enable auto scroll when not scrolling up & reached the bottom | ||
// But does not cancel it if not at the bottom (due to smooth scrolling) | ||
autoScroll.current = true; | ||
} | ||
|
||
prevOffset.current = offset; | ||
} | ||
} | ||
|
||
return <VList | ||
ref={ref} | ||
className="h-full p-2" | ||
onScroll={updateAutoScroll} | ||
onScrollEnd={() => { | ||
// Prevent misjudged scroll up when layout shifts | ||
prevOffset.current = 0; | ||
}} | ||
> | ||
{ | ||
logs.map(log => | ||
// The key includes process ID to correctly handle props changes when switching between monitors | ||
<LogLineMemo log={log} key={procId + ":" + log.index}/> | ||
) | ||
} | ||
</VList>; | ||
} | ||
|
||
const LogLineMemo = React.memo(LogLine, (prevProps, nextProps) => { | ||
// Log objects are not changed once parsed | ||
// By comparing props via index we save lots of re-renders | ||
return nextProps.log.index === prevProps.log.index; | ||
}); | ||
|
||
function LogLine({ log }: { log: GameProcessLog }) { | ||
const { time, level, message, throwable } = log; | ||
|
||
return <div className="flex flex-col"> | ||
<div className="flex gap-3"> | ||
<div className="text-foreground-400"> | ||
{new Date(time).toLocaleTimeString()} | ||
</div> | ||
<div | ||
className={ | ||
clsx("break-all", { | ||
"font-bold text-danger": level === "FATAL" || level === "ERROR", | ||
"font-bold text-warning": level === "WARN", | ||
"text-foreground-400": level === "DEBUG" | ||
}) | ||
} | ||
> | ||
{message} | ||
<pre className="whitespace-pre-wrap">{throwable}</pre> | ||
</div> | ||
</div> | ||
</div>; | ||
} | ||
|
||
const statusColors = { | ||
running: "success", | ||
crashed: "danger", | ||
exited: "default" | ||
} as const; | ||
function ControlPanel() { | ||
const proc = useContext(GameProcessContext)!; | ||
const { detail: { name, modLoader, stable }, status } = proc; | ||
|
||
function StatusChip({ status }: { status: RemoteGameStatus }) { | ||
const { t } = useTranslation("pages", { keyPrefix: "monitor.status" }); | ||
return <div className="w-full h-full flex flex-col gap-4"> | ||
<div className="grow"> | ||
<GameInfoCardMemo | ||
name={name} | ||
modLoader={modLoader} | ||
stable={stable} | ||
status={status} | ||
/> | ||
</div> | ||
<MonitorActionsMemo procId={proc.id} gameId={proc.detail.id} status={proc.status}/> | ||
</div>; | ||
} | ||
|
||
return <Chip | ||
variant="dot" | ||
color={statusColors[status]} | ||
> | ||
{t(status)} | ||
</Chip>; | ||
interface GameInfoCardProps { | ||
name: string; | ||
modLoader: string; | ||
stable: boolean; | ||
status: RemoteGameStatus; | ||
} | ||
|
||
function MonitorItem({ proc }: { proc: RemoteGameProcess }) { | ||
const { detail: { modLoader, stable, name, versionId }, status } = proc; | ||
const [, nav] = useLocation(); | ||
const GameInfoCardMemo = React.memo(GameInfoCard); | ||
|
||
function revealProc() { | ||
nav(`/monitor/${proc.id}`); | ||
} | ||
function GameInfoCard({ name, modLoader, stable, status }: GameInfoCardProps) { | ||
const { t } = useTranslation("pages", { keyPrefix: "monitor.status" }); | ||
|
||
return <Card> | ||
return <Card className="h-full"> | ||
<CardBody> | ||
<div className="flex gap-4 items-center h-16 px-3"> | ||
<div className="h-full p-3 bg-content2 rounded-full"> | ||
<div className="flex flex-col p-4 gap-2 items-center my-auto"> | ||
<div | ||
className={ | ||
clsx("max-w-40 m-8 p-5 bg-content2 rounded-full outline outline-4 outline-offset-8", { | ||
"outline-success": status === "running", | ||
"outline-default": status === "exited", | ||
"outline-danger": status === "crashed" | ||
}) | ||
} | ||
> | ||
<GameTypeImage loader={modLoader} stable={stable}/> | ||
</div> | ||
<div className="font-bold text-2xl">{name}</div> | ||
<div className="text-lg text-foreground-400">{t(status)}</div> | ||
</div> | ||
</CardBody> | ||
</Card>; | ||
} | ||
|
||
<div className="flex flex-col gap-1"> | ||
<div className="font-bold text-xl">{name}</div> | ||
<div className="text-foreground-400">{versionId}</div> | ||
</div> | ||
interface MonitorActionsProps { | ||
procId: string; | ||
gameId: string; | ||
status: RemoteGameStatus; | ||
} | ||
|
||
<div className="ml-auto flex gap-2 items-center"> | ||
<StatusChip status={status}/> | ||
</div> | ||
const MonitorActionsMemo = React.memo(MonitorActions); | ||
|
||
function MonitorActions({ procId, gameId, status }: MonitorActionsProps) { | ||
const { t } = useTranslation("pages", { keyPrefix: "monitor.actions" }); | ||
const [stopConfirmOpen, setStopConfirmOpen] = useState(false); | ||
const [, nav] = useLocation(); | ||
|
||
function handleStopAction() { | ||
native.launcher.stop(procId); | ||
setStopConfirmOpen(false); | ||
} | ||
|
||
|
||
<div className="ml-4"> | ||
<Button isIconOnly color="primary" onPress={revealProc}> | ||
<ArrowRightIcon/> | ||
const stopDisabled = status !== "running"; | ||
|
||
return <div className="flex flex-col gap-4"> | ||
<Button startContent={<ArrowLeftIcon/>} onPress={() => nav("/monitor")}> | ||
{t("back-to-list")} | ||
</Button> | ||
<Button startContent={<FolderArchive/>} onPress={() => native.game.reveal(gameId, "resourcepacks")}> | ||
{t("reveal-rsp")} | ||
</Button> | ||
<Popover | ||
placement="top" | ||
color="foreground" | ||
isOpen={stopConfirmOpen} | ||
onOpenChange={setStopConfirmOpen} | ||
> | ||
<PopoverTrigger> | ||
<Button | ||
isDisabled={stopDisabled} | ||
color="danger" | ||
startContent={<OctagonXIcon/>} | ||
> | ||
{t("stop")} | ||
</Button> | ||
</PopoverTrigger> | ||
<PopoverContent> | ||
<div className="flex flex-col gap-2 py-2 px-4 items-center"> | ||
<div className="flex items-center gap-2"> | ||
<CircleHelpIcon/> | ||
<div className="text-lg font-bold">{t("stop-title")}</div> | ||
</div> | ||
|
||
<div className="text-sm text-foreground-400">{t("stop-sub")}</div> | ||
<Button | ||
size="sm" | ||
color="danger" | ||
startContent={<ArrowRightIcon/>} | ||
onPress={handleStopAction} | ||
> | ||
{t("stop-confirm")} | ||
</Button> | ||
</div> | ||
</div> | ||
</CardBody> | ||
</Card>; | ||
</PopoverContent> | ||
</Popover> | ||
</div>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import type { GameProfile } from "@/main/game/spec"; | ||
import { useEffect, useState } from "react"; | ||
|
||
export function useGameList(): GameProfile[] { | ||
const [games, setGames] = useState<GameProfile[]>([]); | ||
|
||
useEffect(() => { | ||
native.game.list().then(setGames); | ||
}, []); | ||
|
||
return games; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import type { VersionManifest } from "@/main/install/vanilla"; | ||
import { useEffect, useState } from "react"; | ||
|
||
export function useVersionManifest(): VersionManifest | null { | ||
const [versionManifest, setVersionManifest] = useState<VersionManifest | null>(null); | ||
|
||
useEffect(() => { | ||
native.sources.getVersionManifest().then(setVersionManifest); | ||
}, []); | ||
|
||
return versionManifest; | ||
} |