|
@@ -483,9 +330,7 @@ export default function SplitEditor({ splitFilePayload, speedRunAPIBase }: Split
|
{!hasChildren && (
handleTimeChange(id, ts, false)}
+ time={segment.average ? segment.average + totalAvg : null}
/>
)}
|
@@ -493,9 +338,7 @@ export default function SplitEditor({ splitFilePayload, speedRunAPIBase }: Split
{!hasChildren && (
handleTimeChange(id, ts, true)}
+ time={segment.pb ? segment.pb + totalBest : null}
/>
)}
|
@@ -517,6 +360,12 @@ export default function SplitEditor({ splitFilePayload, speedRunAPIBase }: Split
renderRows(segment.children ?? [], depth + 1, nextInheritedGroup, nextIsDirectChild)}
);
+
+ if (!hasChildren) {
+ totalAvg += segment.average
+ totalBest += segment.pb;
+ }
+ return frag;
});
}
@@ -529,10 +378,6 @@ export default function SplitEditor({ splitFilePayload, speedRunAPIBase }: Split
setGameName(e.target.value)}
- onBlur={() => {
- clearTimeout(timeoutID.current);
- }}
- onKeyUp={searchSpeedrun}
id="game_name"
name="game_name"
type="text"
@@ -540,33 +385,6 @@ export default function SplitEditor({ splitFilePayload, speedRunAPIBase }: Split
/>
- {gameResults.length > 0 && (
-
-
- {gameResults.map((gameResult) => (
- - {
- setGameName(gameResult.names.international);
- setGameResults([]);
- }}
- key={gameResult.id}
- >
-
-
![{gameResult.assets["cover-tiny"].uri}]({gameResult.assets["cover-tiny"].uri})
-
- {gameResult.names.international}
- {gameResult.released}
-
-
-
- ))}
-
-
- )}
-
void;
+ time: number | null;
};
-type timeRowElements = {
- hours: HTMLInputElement | null;
- minutes: HTMLInputElement | null;
- seconds: HTMLInputElement | null;
- centis: HTMLInputElement | null;
+type Handle = {
+ getMillis(): number;
};
-export default function TimeRow({ id, time, onChangeCallback }: timeRowParams) {
- // Get refs to all the parts so we can update all at once
- const timeRef = useRef
({
- hours: null,
- minutes: null,
- seconds: null,
- centis: null,
- });
-
- // Set initial values from the passed in timeParts
- useEffect(() => {
- let el = timeRef.current.hours;
- if (el) {
- el.value = time?.hours.toString() ?? "";
- }
-
- el = timeRef.current.minutes;
- if (el) {
- el.value = time?.minutes.toString() ?? "";
- }
-
- el = timeRef.current.seconds;
- if (el) {
- el.value = time?.seconds.toString() ?? "";
- }
-
- el = timeRef.current.centis;
- if (el) {
- el.value = time?.centis.toString() ?? "";
- }
- }, []);
-
- const handleChange = () => {
- let hours = timeRef.current.hours?.value ?? "0";
- let minutes = timeRef.current.minutes?.value ?? "0";
- let seconds = timeRef.current.seconds?.value ?? "0";
- let centis = timeRef.current.centis?.value ?? "0";
-
- hours = numeric(hours) ? hours : "";
- minutes = numeric(minutes) ? minutes : "";
- seconds = numeric(seconds) ? seconds : "";
- centis = numeric(centis) ? centis : "";
-
- const hoursNum = numeric(hours.trim()) ? Number(hours) : 0;
- const minutesNum = Math.min(Math.max(numeric(minutes.trim()) ? Number(minutes) : 0, 0), 59);
- const secondsNum = Math.min(Math.max(numeric(seconds.trim()) ? Number(seconds) : 0, 0), 59);
- const centisNum = Math.min(Math.max(numeric(centis.trim()) ? Number(centis) : 0, 0), 99);
-
- let el = timeRef.current.hours;
- if (el) {
- el.value = hoursNum.toString() ?? "";
- }
-
- el = timeRef.current.minutes;
- if (el) {
- el.value = minutesNum.toString() ?? "";
- }
-
- el = timeRef.current.seconds;
- if (el) {
- el.value = secondsNum.toString() ?? "";
- }
-
- el = timeRef.current.centis;
- if (el) {
- el.value = centisNum.toString() ?? "";
- }
-
- onChangeCallback(id, {
- negative: false,
- hours: hoursNum,
- minutes: minutesNum,
- seconds: secondsNum,
- centis: centisNum,
- });
- };
+export const TimeRow = forwardRef(
+ (props, ref) => {
+ const hourRef: RefObject = useRef(null)
+ const minuteRef: RefObject = useRef(null)
+ const secondRef: RefObject = useRef(null)
+ const centiRef: RefObject = useRef(null)
+
+ useImperativeHandle(ref, () => ({
+ getMillis: () => {
+ return partsToMS({
+ negative: false,
+ hours: parseInt(hourRef.current?.value ?? "0", 10),
+ minutes: parseInt(minuteRef.current?.value ?? "0", 10),
+ seconds: parseInt(secondRef.current?.value ?? "0", 10),
+ centis: parseInt(centiRef.current?.value ?? "0", 10),
+ })
+ }
+ }))
return (
{
- timeRef.current.hours = el;
- }}
+ ref={hourRef}
placeholder="H"
- onChange={handleChange}
+ defaultValue={props.time != null ? msToParts(props.time).hours : ""}
/>
:
{
- timeRef.current.minutes = el;
- }}
+ ref={minuteRef}
placeholder="MM"
- onChange={handleChange}
+ defaultValue={props.time != null ? msToParts(props.time).minutes : ""}
/>
:
{
- timeRef.current.seconds = el;
- }}
+ ref={secondRef}
placeholder="SS"
- onChange={handleChange}
+ defaultValue={props.time != null ? msToParts(props.time).seconds : ""}
/>
.
{
- timeRef.current.centis = el;
- }}
+ ref={centiRef}
placeholder={"cc"}
- onChange={handleChange}
+ defaultValue={props.time != null ? msToParts(props.time).centis : ""}
/>
);
-}
+});
diff --git a/frontend/src/components/editor/hashColor.ts b/frontend/src/components/editor/hashColor.ts
new file mode 100644
index 0000000..f99825a
--- /dev/null
+++ b/frontend/src/components/editor/hashColor.ts
@@ -0,0 +1,24 @@
+export type GroupCtx = { bg: string };
+
+export function hashStringToInt(s: string): number {
+ let h = 2166136261; // FNV-1a-ish
+ for (let i = 0; i < s.length; i++) {
+ h ^= s.charCodeAt(i);
+ h = Math.imul(h, 16777619);
+ }
+ return h >>> 0;
+}
+
+export function colorFromId(id: string): string {
+ const n = hashStringToInt(id);
+
+ const hue = n % 360;
+
+ // Keep saturation strong but not neon
+ const sat = 45 + (n % 15); // 45–59%
+
+ // Dark background range
+ const light = 18 + (n % 10); // 18–27%
+
+ return `hsl(${hue} ${sat}% ${light}%)`;
+}
diff --git a/frontend/src/components/splitter/SegmentList.tsx b/frontend/src/components/splitter/SegmentList.tsx
index f44dc4a..7c4c34d 100644
--- a/frontend/src/components/splitter/SegmentList.tsx
+++ b/frontend/src/components/splitter/SegmentList.tsx
@@ -227,13 +227,6 @@ export default function SegmentList({ sessionPayload, comparison }: SplitListPar
return m;
}, [sessionPayload.leaf_segments]);
- // Determine the final leaf segment id (so we can render it separately)
- const finalLeafId = useMemo(() => {
- const leaves = sessionPayload.leaf_segments;
- if (!leaves || leaves.length === 0) return null;
- return leaves[leaves.length - 1].id;
- }, [sessionPayload.leaf_segments]);
-
const parentById = useMemo(() => {
const m = new Map();
for (const fs of flatSegments) {
@@ -243,6 +236,36 @@ export default function SegmentList({ sessionPayload, comparison }: SplitListPar
}, [flatSegments]);
const [expandedParents, setExpandedParents] = useState>(() => new Set());
+ const segmentById = useMemo(() => {
+ const m = new Map();
+ for (const fs of flatSegments) m.set(fs.Segment.id, fs.Segment);
+ return m;
+ }, [flatSegments]);
+
+ // For each parent segment id, find the "last" leaf (by leaf_segments order) in its subtree.
+ const lastLeafByParentId = useMemo(() => {
+ const result = new Map(); // parentId -> leafId
+
+ if (!sessionPayload.leaf_segments || sessionPayload.leaf_segments.length === 0) return result;
+
+ // For each leaf, walk ancestors and assign/overwrite (later leaves overwrite earlier => "last" wins)
+ for (const leaf of sessionPayload.leaf_segments) {
+ const ancestors = getAncestorIds(leaf.id, parentById);
+ for (const anc of ancestors) {
+ result.set(anc, leaf.id);
+ }
+ }
+
+ return result;
+ }, [sessionPayload.leaf_segments, parentById]);
+
+ // Determine the final leaf segment id (so we can render it separately)
+ const finalLeafId = useMemo(() => {
+ const leaves = sessionPayload.leaf_segments;
+ if (!leaves || leaves.length === 0) return null;
+ return leaves[leaves.length - 1].id;
+ }, [sessionPayload.leaf_segments]);
+
useEffect(() => {
const leaves = sessionPayload.leaf_segments;
if (!leaves || leaves.length === 0) {
@@ -321,14 +344,38 @@ export default function SegmentList({ sessionPayload, comparison }: SplitListPar
) : null;
+ const lastLeafId = lastLeafByParentId.get(segmentData.Segment.id) ?? null;
+ const lastLeafSplit = lastLeafId ? sessionPayload.current_run?.splits[lastLeafId] ?? null : null;
+
+ // Comparison time (cumulative display) pulled from last leaf
+ let parentComparison: JSX.Element | null = null;
+
+ // Delta pulled from last leaf vs its cumulative target
+ let parentDelta: JSX.Element | null = null;
+
+ if (lastLeafId && lastLeafSplit) {
+ const leafSeg = segmentById.get(lastLeafId);
+ const cTarget = targets.cumulative[lastLeafId] ?? null;
+ const iTarget = targets.individual[lastLeafId] ?? null;
+
+ if (leafSeg) {
+ parentComparison = getSegmentDisplayTime(leafSeg, lastLeafSplit, cTarget, iTarget);
+ }
+
+ if (cTarget != null) {
+ const delta = lastLeafSplit.current_cumulative - cTarget;
+ parentDelta = getDeltaDisplayTime(delta);
+ }
+ }
+
main.push(
|
{toggle}
{segmentData.Segment.name}
|
- |
- |
+ {parentDelta} |
+ {parentComparison} |
,
);
continue;
diff --git a/frontend/src/styles/forms.css b/frontend/src/styles/forms.css
index 008e747..7e35f2e 100644
--- a/frontend/src/styles/forms.css
+++ b/frontend/src/styles/forms.css
@@ -94,4 +94,21 @@
::placeholder {
color: #808080; /* Example: a light gray color */
}
+
+ tr.seg-group > td {
+ background: var(--group-bg);
+ }
+
+ tr.seg-group-parent > td {
+ filter: brightness(1.15);
+ font-weight: 600;
+ }
+
+ tr.seg-group-child > td {
+ filter: brightness(0.95);
+ }
+
+ tr.seg-group:hover > td {
+ filter: brightness(1.25);
+ }
}
diff --git a/frontend/src/test/setupTests.ts b/frontend/src/test/setupTests.ts
new file mode 100644
index 0000000..aeb44fe
--- /dev/null
+++ b/frontend/src/test/setupTests.ts
@@ -0,0 +1,11 @@
+import "@testing-library/jest-dom"
+
+import { vi } from "vitest"
+
+vi.mock("../../wailsjs/runtime/runtime", () => ({
+ EventsOn: vi.fn(),
+ EventsOff: vi.fn(),
+ EventsEmit: vi.fn(),
+ WindowSetTitle: vi.fn(),
+ WindowSetSize: vi.fn(),
+}))
diff --git a/frontend/vite.config.js b/frontend/vite.config.js
index d89c4f4..d7ac4d5 100644
--- a/frontend/vite.config.js
+++ b/frontend/vite.config.js
@@ -4,4 +4,9 @@ import { defineConfig } from "vite";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
+ test: {
+ environment: "jsdom",
+ setupFiles: "./src/test/setupTests.ts",
+ globals: true
+ }
});
diff --git a/hotkeys/darwin/provider_darwin.m b/hotkeys/darwin/provider_darwin.m
index efad784..ca7f865 100644
--- a/hotkeys/darwin/provider_darwin.m
+++ b/hotkeys/darwin/provider_darwin.m
@@ -123,7 +123,7 @@ static CGEventRef tapCallback(CGEventTapProxy proxy, CGEventType type, CGEventRe
CGEventMask mask = (CGEventMaskBit(kCGEventKeyDown));
gEventTap = CGEventTapCreate(kCGSessionEventTap, kCGHeadInsertEventTap,
- kCGEventTapOptionDefault, mask, tapCallback, NULL);
+ kCGEventTapOptionListenOnly, mask, tapCallback, NULL);
if (!gEventTap) {
pthread_mutex_lock(&gMu);
gRunning = false;
diff --git a/skin/default-skin.zip b/skin/default-skin.zip
index 5b10854..e61bcf9 100644
Binary files a/skin/default-skin.zip and b/skin/default-skin.zip differ
diff --git a/skin/service.go b/skin/service.go
index 9efae3a..434cd07 100644
--- a/skin/service.go
+++ b/skin/service.go
@@ -91,7 +91,7 @@ func (s *Service) Startup() error {
return err
}
- err := os.MkdirAll(target, 0o755)
+ err := os.MkdirAll(filepath.Join(target, "default"), 0o755)
if err != nil {
return err
}
@@ -102,7 +102,7 @@ func (s *Service) Startup() error {
}
for _, f := range r.File {
- p := filepath.Join(target, f.Name)
+ p := filepath.Join(target, "default", f.Name)
// Prevent ZipSlip
if !strings.HasPrefix(filepath.Clean(p)+string(os.PathSeparator), filepath.Clean(target)+string(os.PathSeparator)) {