diff --git a/src/lib/engine/shared.ts b/src/lib/engine/shared.ts
index 328366a6..8b2ddc31 100644
--- a/src/lib/engine/shared.ts
+++ b/src/lib/engine/shared.ts
@@ -18,10 +18,14 @@ export const isMultiThreadSupported = () => {
}
};
-export const isIosDevice = () => /iPhone|iPad|iPod/i.test(navigator.userAgent);
+export const isIosDevice = () =>
+ typeof navigator !== "undefined" &&
+ /iPhone|iPad|iPod/i.test(navigator.userAgent);
export const isMobileDevice = () =>
- isIosDevice() || /Android|Opera Mini/i.test(navigator.userAgent);
+ isIosDevice() ||
+ (typeof navigator !== "undefined" &&
+ /Android|Opera Mini/i.test(navigator.userAgent));
export const isEngineSupported = (name: EngineName): boolean => {
switch (name) {
diff --git a/src/lib/engine/worker.ts b/src/lib/engine/worker.ts
index 224e0d9c..4b98c61a 100644
--- a/src/lib/engine/worker.ts
+++ b/src/lib/engine/worker.ts
@@ -45,6 +45,11 @@ export const sendCommandsToWorker = (
};
export const getRecommendedWorkersNb = (): number => {
+ // Return default value during SSR
+ if (typeof navigator === "undefined") {
+ return 4;
+ }
+
const maxWorkersNbFromThreads = Math.max(
1,
Math.round(navigator.hardwareConcurrency - 4),
diff --git a/src/sections/analysis/panelToolbar/index.tsx b/src/sections/analysis/panelToolbar/index.tsx
index 827d64ce..69156ac7 100644
--- a/src/sections/analysis/panelToolbar/index.tsx
+++ b/src/sections/analysis/panelToolbar/index.tsx
@@ -5,6 +5,7 @@ import { boardAtom, gameAtom } from "../states";
import { useChessActions } from "@/hooks/useChessActions";
import FlipBoardButton from "./flipBoardButton";
import NextMoveButton from "./nextMoveButton";
+import PlayButton from "./playButton";
import GoToLastPositionButton from "./goToLastPositionButton";
import SaveButton from "./saveButton";
import { useEffect } from "react";
@@ -19,11 +20,14 @@ export default function PanelToolBar() {
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
- if (boardHistory.length === 0) return;
- if (e.key === "ArrowLeft") {
+ if (e.key === "ArrowLeft" && boardHistory.length > 0) {
undoBoardMove();
- } else if (e.key === "ArrowDown") {
+ } else if (e.key === "ArrowDown" && boardHistory.length > 0) {
resetBoard();
+ } else if (e.key === " " || e.key === "Spacebar") {
+ // Space bar will be handled by PlayButton component
+ // We prevent default here to avoid page scrolling
+ e.preventDefault();
}
};
@@ -62,6 +66,8 @@ export default function PanelToolBar() {
+
+
diff --git a/src/sections/analysis/panelToolbar/playButton.tsx b/src/sections/analysis/panelToolbar/playButton.tsx
new file mode 100644
index 00000000..5f20c4af
--- /dev/null
+++ b/src/sections/analysis/panelToolbar/playButton.tsx
@@ -0,0 +1,119 @@
+import { Icon } from "@iconify/react";
+import { Grid2 as Grid, IconButton, Tooltip } from "@mui/material";
+import { useAtomValue } from "jotai";
+import { boardAtom, gameAtom } from "../states";
+import { useChessActions } from "@/hooks/useChessActions";
+import { useCallback, useEffect, useRef, useState } from "react";
+
+const PLAY_SPEED = 1000; // 1 second between moves
+
+export default function PlayButton() {
+ const { playMove: playBoardMove } = useChessActions(boardAtom);
+ const game = useAtomValue(gameAtom);
+ const board = useAtomValue(boardAtom);
+
+ const [isPlaying, setIsPlaying] = useState(false);
+ const intervalRef = useRef(null);
+
+ const gameHistory = game.history();
+ const boardHistory = board.history();
+
+ const isButtonEnabled =
+ boardHistory.length < gameHistory.length &&
+ gameHistory.slice(0, boardHistory.length).join() === boardHistory.join();
+
+ const playNextMove = useCallback(() => {
+ if (!isButtonEnabled) {
+ setIsPlaying(false);
+ return;
+ }
+
+ const nextMoveIndex = boardHistory.length;
+ const nextMove = game.history({ verbose: true })[nextMoveIndex];
+
+ if (nextMove) {
+ const comment = game
+ .getComments()
+ .find((c) => c.fen === nextMove.after)?.comment;
+
+ playBoardMove({
+ from: nextMove.from,
+ to: nextMove.to,
+ promotion: nextMove.promotion,
+ comment,
+ });
+ } else {
+ setIsPlaying(false);
+ }
+ }, [isButtonEnabled, boardHistory.length, gameHistory, game, playBoardMove]);
+
+ const togglePlay = useCallback(() => {
+ if (isPlaying) {
+ setIsPlaying(false);
+ } else {
+ setIsPlaying(true);
+ }
+ }, [isPlaying]);
+
+ // Handle interval management
+ useEffect(() => {
+ if (isPlaying) {
+ intervalRef.current = setInterval(playNextMove, PLAY_SPEED);
+ } else {
+ if (intervalRef.current) {
+ clearInterval(intervalRef.current);
+ intervalRef.current = null;
+ }
+ }
+
+ return () => {
+ if (intervalRef.current) {
+ clearInterval(intervalRef.current);
+ intervalRef.current = null;
+ }
+ };
+ }, [isPlaying, playNextMove]);
+
+ // Stop playing when no more moves available
+ useEffect(() => {
+ if (isPlaying && !isButtonEnabled) {
+ setIsPlaying(false);
+ }
+ }, [isPlaying, isButtonEnabled]);
+
+ // Spacebar shortcut
+ useEffect(() => {
+ const onKeyDown = (e: KeyboardEvent) => {
+ if (e.key === " " || e.key === "Spacebar") {
+ e.preventDefault();
+ if (isButtonEnabled || isPlaying) {
+ togglePlay();
+ }
+ }
+ };
+
+ window.addEventListener("keydown", onKeyDown);
+ return () => window.removeEventListener("keydown", onKeyDown);
+ }, [togglePlay, isButtonEnabled, isPlaying]);
+
+ return (
+
+
+
+
+
+
+
+ );
+}