219 lines
6.9 KiB
TypeScript
219 lines
6.9 KiB
TypeScript
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<TreeNode, Map<string, number[][]>>();
|
|
|
|
function getTranspositionMap(root: TreeNode) {
|
|
if (transpositionCache.has(root)) {
|
|
return transpositionCache.get(root)!;
|
|
}
|
|
|
|
const map = new Map<string, number[][]>();
|
|
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<HTMLSpanElement | null>;
|
|
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 (
|
|
<>
|
|
<Box
|
|
ref={isCurrentVariation ? targetRef : undefined}
|
|
component="span"
|
|
style={{
|
|
display: tableLayout ? "block" : "inline-block",
|
|
marginLeft: hasNumber ? 6 : 0,
|
|
fontSize: "80%",
|
|
width: tableLayout ? "100%" : undefined,
|
|
}}
|
|
>
|
|
{hasNumber && `${moveNumber.toString()}${isWhite ? "." : "..."}`}
|
|
{move && (
|
|
<Menu opened={open} width={200}>
|
|
<Menu.Target>
|
|
<MoveCell
|
|
ref={ref}
|
|
move={move}
|
|
annotations={annotations}
|
|
isStart={isStart}
|
|
isCurrentVariation={isCurrentVariation}
|
|
onClick={() => goToMove(movePath)}
|
|
onContextMenu={(e: React.MouseEvent) => {
|
|
setOpen((v) => !v);
|
|
e.preventDefault();
|
|
}}
|
|
fullWidth={tableLayout}
|
|
rightAccessory={
|
|
tableLayout && scoreText ? (
|
|
<Text component="span" size="xs" c="dimmed">
|
|
{scoreText}
|
|
</Text>
|
|
) : undefined
|
|
}
|
|
/>
|
|
</Menu.Target>
|
|
|
|
<Portal>
|
|
<Menu.Dropdown>
|
|
{tabFile?.metadata.type === "repertoire" && (
|
|
<Menu.Item
|
|
leftSection={<IconFlag size="0.875rem" />}
|
|
onClick={() => setStart(movePath)}
|
|
>
|
|
{t("Menu.MarkAsStart")}
|
|
</Menu.Item>
|
|
)}
|
|
<Menu.Item
|
|
leftSection={<IconChevronsUp size="0.875rem" />}
|
|
onClick={() => promoteToMainline(movePath)}
|
|
>
|
|
{t("Menu.PromoteToMainLine")}
|
|
</Menu.Item>
|
|
|
|
<Menu.Item
|
|
leftSection={<IconChevronUp size="0.875rem" />}
|
|
onClick={() => promoteVariation(movePath)}
|
|
>
|
|
{t("Menu.PromoteVariation")}
|
|
</Menu.Item>
|
|
|
|
<Menu.Item
|
|
leftSection={<IconCopy size="0.875rem" />}
|
|
onClick={() => copyVariationPgn(movePath)}
|
|
>
|
|
{t("Menu.CopyVariationPGN")}
|
|
</Menu.Item>
|
|
|
|
<Menu.Item
|
|
color="red"
|
|
leftSection={<IconX size="0.875rem" />}
|
|
onClick={() => deleteMove(movePath)}
|
|
>
|
|
{t("Menu.DeleteMove")}
|
|
</Menu.Item>
|
|
</Menu.Dropdown>
|
|
</Portal>
|
|
</Menu>
|
|
)}
|
|
{transpositions.length > 0 && (
|
|
<Tooltip label="Transposition">
|
|
<ActionIcon size="xs" onClick={() => goToMove(transpositions[0])}>
|
|
<IconArrowsJoin size="0.875rem" />
|
|
</ActionIcon>
|
|
</Tooltip>
|
|
)}
|
|
</Box>
|
|
{showComments && !tableLayout && comment && <Comment comment={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
|
|
);
|
|
});
|