Skip to content
Open
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
8 changes: 6 additions & 2 deletions src/lib/engine/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
5 changes: 5 additions & 0 deletions src/lib/engine/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
12 changes: 9 additions & 3 deletions src/sections/analysis/panelToolbar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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();
}
};

Expand Down Expand Up @@ -62,6 +66,8 @@ export default function PanelToolBar() {
</Grid>
</Tooltip>

<PlayButton />

<NextMoveButton />

<GoToLastPositionButton />
Expand Down
119 changes: 119 additions & 0 deletions src/sections/analysis/panelToolbar/playButton.tsx
Original file line number Diff line number Diff line change
@@ -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<NodeJS.Timeout | null>(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 (
<Tooltip title={isPlaying ? "Pause auto-play" : "Auto-play moves"}>
<Grid>
<IconButton
onClick={togglePlay}
disabled={!isButtonEnabled && !isPlaying}
sx={{
paddingX: 1.2,
paddingY: 0.5,
color: isPlaying ? "primary.main" : "inherit",
}}
>
<Icon
icon={isPlaying ? "ri:pause-line" : "ri:play-line"}
height={24}
/>
</IconButton>
</Grid>
</Tooltip>
);
}