import { ActionIcon, Box, Menu, Portal, Text, Tooltip } from "@mantine/core"; import { useClickOutside } from "@mantine/hooks"; import { IconArrowsJoin, IconChevronsUp, IconChevronUp, IconCopy, IconFlag, IconX, } from "@tabler/icons-react"; import equal from "fast-deep-equal"; import { useAtomValue } from "jotai"; import { memo, useContext, useState } from "react"; import { useTranslation } from "react-i18next"; import { useStore } from "zustand"; import { useStoreWithEqualityFn } from "zustand/traditional"; import Comment from "@/components/common/Comment"; import { currentTabAtom } from "@/state/atoms"; import type { Annotation } from "@/utils/annotation"; import { hasMorePriority, stripClock } from "@/utils/chess"; import { getTabFile } from "@/utils/tabs"; import { type TreeNode, treeIterator } from "@/utils/treeReducer"; import MoveCell from "./MoveCell"; import { TreeStateContext } from "./TreeStateContext"; const transpositionCache = new WeakMap>(); function getTranspositionMap(root: TreeNode) { if (transpositionCache.has(root)) { return transpositionCache.get(root)!; } const map = new Map(); const iterator = treeIterator(root); for (const item of iterator) { const strippedFen = stripClock(item.node.fen); if (!map.has(strippedFen)) { map.set(strippedFen, []); } map.get(strippedFen)!.push(item.position); } transpositionCache.set(root, map); return map; } function getTranspositions(fen: string, position: number[], root: TreeNode) { if (position.length === 0 || position.every((v) => v === 0)) return []; const map = getTranspositionMap(root); const strippedFen = stripClock(fen); const matchingPositions = map.get(strippedFen) || []; return matchingPositions.filter((targetPosition) => !hasMorePriority(position, targetPosition)); } function CompleteMoveCell({ movePath, halfMoves, move, fen, comment, annotations, showComments, first, targetRef, tableLayout, scoreText, }: { halfMoves: number; comment: string; annotations: Annotation[]; showComments: boolean; move?: string | null; fen?: string; first?: boolean; movePath: number[]; targetRef: React.RefObject; tableLayout?: boolean; scoreText?: string; }) { const store = useContext(TreeStateContext)!; const isStart = useStore(store, (s) => equal(movePath, s.headers.start)); const isCurrentVariation = useStore(store, (s) => equal(s.position, movePath)); const transpositions = useStoreWithEqualityFn( store, (s) => (fen ? getTranspositions(fen, movePath, s.root) : []), (a, b) => equal(a, b), ); const goToMove = useStore(store, (s) => s.goToMove); const deleteMove = useStore(store, (s) => s.deleteMove); const promoteVariation = useStore(store, (s) => s.promoteVariation); const promoteToMainline = useStore(store, (s) => s.promoteToMainline); const copyVariationPgn = useStore(store, (s) => s.copyVariationPgn); const setStart = useStore(store, (s) => s.setStart); const moveNumber = Math.ceil(halfMoves / 2); const isWhite = halfMoves % 2 === 1; const hasNumber = !tableLayout && halfMoves > 0 && (first || isWhite); const ref = useClickOutside(() => { setOpen(false); }); const [open, setOpen] = useState(false); const currentTab = useAtomValue(currentTabAtom); const tabFile = getTabFile(currentTab); const { t } = useTranslation(); return ( <> {hasNumber && `${moveNumber.toString()}${isWhite ? "." : "..."}`} {move && ( goToMove(movePath)} onContextMenu={(e: React.MouseEvent) => { setOpen((v) => !v); e.preventDefault(); }} fullWidth={tableLayout} rightAccessory={ tableLayout && scoreText ? ( {scoreText} ) : undefined } /> {tabFile?.metadata.type === "repertoire" && ( } onClick={() => setStart(movePath)} > {t("Menu.MarkAsStart")} )} } onClick={() => promoteToMainline(movePath)} > {t("Menu.PromoteToMainLine")} } onClick={() => promoteVariation(movePath)} > {t("Menu.PromoteVariation")} } onClick={() => copyVariationPgn(movePath)} > {t("Menu.CopyVariationPGN")} } onClick={() => deleteMove(movePath)} > {t("Menu.DeleteMove")} )} {transpositions.length > 0 && ( goToMove(transpositions[0])}> )} {showComments && !tableLayout && comment && } ); } export default memo(CompleteMoveCell, (prev, next) => { return ( prev.move === next.move && prev.fen === next.fen && prev.comment === next.comment && equal(prev.annotations, next.annotations) && prev.showComments === next.showComments && prev.first === next.first && equal(prev.movePath, next.movePath) && prev.halfMoves === next.halfMoves && prev.tableLayout === next.tableLayout && prev.scoreText === next.scoreText ); });