Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions src/modules/patcher/components/PatcherStatusPill.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Loader2, Square } from "lucide-react";
import { useEffect, useRef } from "react";
import { twMerge } from "tailwind-merge";

import { IconButton, Kbd, Tooltip } from "@/components";
import { usePatcherSessionStore } from "@/stores";

import { usePatcherStatus, useStopPatcher } from "../api";

export function PatcherStatusPill() {
const { data: status } = usePatcherStatus();
const testingProjects = usePatcherSessionStore((s) => s.testingProjects);
const clearTestingProjects = usePatcherSessionStore((s) => s.clearTestingProjects);
const stopPatcher = useStopPatcher();

const running = status?.running ?? false;
const building = status?.phase === "building";
const isIdle = !running && !building;

const wasActiveRef = useRef(false);
useEffect(() => {
if (!isIdle) {
wasActiveRef.current = true;
} else if (wasActiveRef.current && testingProjects.length > 0) {
clearTestingProjects();
wasActiveRef.current = false;
}
}, [isIdle, testingProjects, clearTestingProjects]);

if (isIdle) return null;

const testLabel =
testingProjects.length === 1
? testingProjects[0].displayName
: testingProjects.length > 1
? `${testingProjects.length} projects`
: null;

const label = building
? testLabel
? `Building ${testLabel}…`
: "Building overlay…"
: testLabel
? `Testing ${testLabel}`
: "Patcher running";

const tone = building ? "accent" : "running";

return (
<div
className={twMerge(
"fixed right-4 bottom-4 z-40 flex h-9 animate-fade-in items-center gap-2 rounded-full border px-3 text-sm font-medium shadow-lg backdrop-blur-sm",
tone === "accent"
? "border-accent-500/40 bg-accent-500/15 text-accent-300"
: "border-green-500/40 bg-green-500/15 text-green-300",
)}
>
{building ? (
<Loader2 className="h-3.5 w-3.5 shrink-0 animate-spin" />
) : (
<span className="relative flex h-2 w-2 shrink-0">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-green-500" />
</span>
)}
<span className="max-w-[240px] truncate">{label}</span>
{!building && (
<Tooltip
content={
<>
Stop patcher <Kbd shortcut="Ctrl+P" />
</>
}
>
<IconButton
icon={<Square className="h-3.5 w-3.5" />}
variant="ghost"
size="xs"
onClick={() => stopPatcher.mutate()}
loading={stopPatcher.isPending}
aria-label="Stop patcher"
className="-mr-1.5 text-green-300 hover:bg-green-500/25 hover:text-green-200"
/>
</Tooltip>
)}
</div>
);
}
15 changes: 0 additions & 15 deletions src/modules/patcher/components/StatusBar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Loader2 } from "lucide-react";
import { useEffect, useRef } from "react";

import { Progress } from "@/components";
import type { OverlayProgress } from "@/lib/tauri";
Expand All @@ -26,22 +25,8 @@ export function StatusBar() {
useHotkeyEvents();

const testingProjects = usePatcherSessionStore((s) => s.testingProjects);
const clearTestingProjects = usePatcherSessionStore((s) => s.clearTestingProjects);

const isBuilding = patcherStatus?.phase === "building";
const isRunning = patcherStatus?.running ?? false;
const isIdle = !isRunning && !isBuilding;

const wasActiveRef = useRef(false);

useEffect(() => {
if (!isIdle) {
wasActiveRef.current = true;
} else if (wasActiveRef.current && testingProjects.length > 0) {
clearTestingProjects();
wasActiveRef.current = false;
}
}, [isIdle, testingProjects, clearTestingProjects]);

if (!isBuilding) return null;

Expand Down
1 change: 1 addition & 0 deletions src/modules/patcher/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./PatcherStatusPill";
export * from "./PatcherUnsupported";
export * from "./StatusBar";
1 change: 1 addition & 0 deletions src/modules/workshop/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ export { useTestProjects } from "./useTestProject";
export { useValidateProject, validateProjectOptions } from "./useValidateProject";
export { useWorkshopProject, workshopProjectOptions } from "./useWorkshopProject";
export { useWorkshopProjects, workshopProjectsOptions } from "./useWorkshopProjects";
export { useWorkshopTestState, type WorkshopTestState } from "./useWorkshopTestState";
46 changes: 46 additions & 0 deletions src/modules/workshop/api/useWorkshopTestState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useMemo } from "react";

import type { WorkshopProject } from "@/lib/tauri";
import { usePatcherStatus } from "@/modules/patcher";
import { usePatcherSessionStore } from "@/stores";

export type WorkshopTestState =
| { kind: "idle" }
| { kind: "building-this" }
| { kind: "running-this" }
| { kind: "building-other"; otherLabel: string }
| { kind: "running-other"; otherLabel: string }
| { kind: "building-library" }
| { kind: "running-library" };

export function useWorkshopTestState(project?: WorkshopProject): WorkshopTestState {
const { data: status } = usePatcherStatus();
const testingProjects = usePatcherSessionStore((s) => s.testingProjects);

return useMemo<WorkshopTestState>(() => {
const running = status?.running ?? false;
const building = status?.phase === "building";
const pendingTest = !running && !building && testingProjects.length > 0;

if (!running && !building && !pendingTest) return { kind: "idle" };

const inBuildPhase = building || pendingTest;

const isThis = project ? testingProjects.some((p) => p.path === project.path) : false;
if (isThis) {
return inBuildPhase ? { kind: "building-this" } : { kind: "running-this" };
}

if (testingProjects.length > 0) {
const otherLabel =
testingProjects.length === 1
? testingProjects[0].displayName
: `${testingProjects.length} projects`;
return inBuildPhase
? { kind: "building-other", otherLabel }
: { kind: "running-other", otherLabel };
}

return inBuildPhase ? { kind: "building-library" } : { kind: "running-library" };
}, [status, project, testingProjects]);
}
Loading
Loading