mirror of
https://github.com/chrislicodes/obsidian-chess-study.git
synced 2026-07-22 07:50:30 +00:00
Compare commits
8 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d15b71985e | ||
|
|
edf436f198 | ||
|
|
93694d1ff9 | ||
|
|
05e17ca3b5 | ||
|
|
137821c4da | ||
|
|
e5381f1cf5 | ||
|
|
87e4b522dc | ||
|
|
0a8746532f |
16 changed files with 676 additions and 463 deletions
48
.github/workflows/release.yml
vendored
48
.github/workflows/release.yml
vendored
|
|
@ -1,34 +1,34 @@
|
|||
name: Release Obsidian plugin
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18.x'
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18.x'
|
||||
|
||||
- name: Build plugin
|
||||
run: |
|
||||
npm install
|
||||
npm run build
|
||||
- name: Build plugin
|
||||
run: |
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
- name: Create release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
tag="${GITHUB_REF#refs/tags/}"
|
||||
- name: Create release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
tag="${GITHUB_REF#refs/tags/}"
|
||||
|
||||
gh release create "$tag" \
|
||||
--title="$tag" \
|
||||
--draft \
|
||||
main.js manifest.json styles.css
|
||||
gh release create "$tag" \
|
||||
--title="$tag" \
|
||||
--draft \
|
||||
main.js manifest.json styles.css
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"tabWidth": 1,
|
||||
"proseWrap": "preserve",
|
||||
"semi": true
|
||||
}
|
||||
|
|
|
|||
19
README.md
19
README.md
|
|
@ -12,8 +12,9 @@ With this plugin, you can either import PGNs or simply start a fresh new game. I
|
|||
- [Installation](#installation)
|
||||
- [Usage](#usage)
|
||||
- [Features](#features)
|
||||
- [1.0.0](#100)
|
||||
- [1.1.0](#110)
|
||||
- [1.0.0](#100)
|
||||
- [1.1.0](#110)
|
||||
- [1.2.0](#120)
|
||||
- [Settings](#settings)
|
||||
- [Roadmap](#roadmap)
|
||||
- [Tools Used](#tools-used)
|
||||
|
|
@ -62,6 +63,14 @@ After that the PGN viewer/editor will render and you are good to go (styles are
|
|||
|
||||

|
||||
|
||||
### 1.2.0
|
||||
|
||||
- [x] Add FEN start position support
|
||||
- [x] Add undo button
|
||||
- [x] Copy current FEN string to clipboard
|
||||
|
||||
Thanks to [@latenitecoding](https://github.com/latenitecoding) for the contributions
|
||||
|
||||
## Settings
|
||||
|
||||
Here are the available settings for a `chessStudy` code block:
|
||||
|
|
@ -71,17 +80,17 @@ Here are the available settings for a `chessStudy` code block:
|
|||
| `chessStudyId` | Valid nanoid | Valid ID for a file stored in the plugin storage |
|
||||
| `boardOrientation` | `white` \| `black` | Orientation of the board |
|
||||
| `boardColor` | `green` \| `brown` | Color of the board |
|
||||
| `viewComments` | `true` \| `false` | Whether to display the comments section |
|
||||
|
||||
You can permanently set some settings in the [Obsidian](https://obsidian.md/) plugin settings for Obsidian Chess Study.
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [ ] Add option to export current FEN
|
||||
- [ ] Add option to start from a specific position (FEN)
|
||||
- [ ] Add undo button
|
||||
- [ ] Add view to manage stored games
|
||||
- [ ] Add more styles
|
||||
- [ ] Add more settings
|
||||
- [ ] Support canvas view
|
||||
- [ ] Mobile support
|
||||
|
||||
## Tools Used
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"id": "chess-study",
|
||||
"name": "Chess Study",
|
||||
"version": "1.1.0",
|
||||
"minAppVersion": "0.15.0",
|
||||
"version": "1.2.0",
|
||||
"minAppVersion": "1.4.0",
|
||||
"description": "A chess study helper and PGN viewer/editor.",
|
||||
"author": "Christoph Lindstädt",
|
||||
"authorUrl": "https://github.com/chrislicodes",
|
||||
"isDesktopOnly": false
|
||||
"isDesktopOnly": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { App, Modal, Setting } from 'obsidian';
|
||||
import { ChessString } from 'src/main';
|
||||
|
||||
export class PgnModal extends Modal {
|
||||
pgn: string;
|
||||
export class ChessStringModal extends Modal {
|
||||
chessString: ChessString;
|
||||
onSubmit: (pgn: string) => void;
|
||||
|
||||
constructor(app: App, onSubmit: (pgn: string) => void) {
|
||||
|
|
@ -13,13 +14,13 @@ export class PgnModal extends Modal {
|
|||
const { contentEl } = this;
|
||||
|
||||
contentEl.createEl('h1', {
|
||||
text: 'Paste the full PGN (leave empty for a new game):',
|
||||
text: 'Paste the full PGN/FEN (leave empty for a new game):',
|
||||
});
|
||||
|
||||
new Setting(contentEl).setName('PGN').addTextArea((text) =>
|
||||
new Setting(contentEl).setName('PGN/FEN').addTextArea((text) =>
|
||||
text
|
||||
.onChange((value) => {
|
||||
this.pgn = value;
|
||||
this.chessString = value;
|
||||
})
|
||||
.inputEl.setCssStyles({ width: '100%', height: '250px' })
|
||||
);
|
||||
|
|
@ -30,7 +31,7 @@ export class PgnModal extends Modal {
|
|||
.setCta()
|
||||
.onClick(() => {
|
||||
this.close();
|
||||
this.onSubmit(this.pgn);
|
||||
this.onSubmit(this.chessString);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
@ -4,11 +4,13 @@ import ChessStudyPlugin from 'src/main';
|
|||
export interface ChessStudyPluginSettings {
|
||||
boardOrientation: 'white' | 'black';
|
||||
boardColor: 'green' | 'brown';
|
||||
viewComments: true | false;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: ChessStudyPluginSettings = {
|
||||
boardOrientation: 'white',
|
||||
boardColor: 'green',
|
||||
viewComments: true,
|
||||
};
|
||||
|
||||
export class SettingsTab extends PluginSettingTab {
|
||||
|
|
@ -34,9 +36,7 @@ export class SettingsTab extends PluginSettingTab {
|
|||
dropdown
|
||||
.setValue(this.plugin.settings.boardOrientation)
|
||||
.onChange((orientation) => {
|
||||
this.plugin.settings.boardOrientation = orientation as
|
||||
| 'white'
|
||||
| 'black';
|
||||
this.plugin.settings.boardOrientation = orientation as 'white' | 'black';
|
||||
this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
|
@ -55,5 +55,19 @@ export class SettingsTab extends PluginSettingTab {
|
|||
this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('View Comments')
|
||||
.setDesc('Sets the default view of the comments')
|
||||
.addDropdown((dropdown) => {
|
||||
dropdown.addOption('true', 'True');
|
||||
dropdown.addOption('false', 'False');
|
||||
dropdown
|
||||
.setValue(this.plugin.settings.viewComments.toString())
|
||||
.onChange((viewComments) => {
|
||||
this.plugin.settings.viewComments = viewComments === 'true';
|
||||
this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,13 +35,14 @@ interface AppProps {
|
|||
}
|
||||
|
||||
export interface GameState {
|
||||
currentMove: ChessStudyMove | VariantMove;
|
||||
currentMove: ChessStudyMove | VariantMove | null;
|
||||
isViewOnly: boolean;
|
||||
study: ChessStudyFileData;
|
||||
}
|
||||
|
||||
export type GameActions =
|
||||
| { type: 'ADD_MOVE_TO_HISTORY'; move: Move }
|
||||
| { type: 'REMOVE_LAST_MOVE_FROM_HISTORY' }
|
||||
| { type: 'DISPLAY_NEXT_MOVE_IN_HISTORY' }
|
||||
| { type: 'DISPLAY_PREVIOUS_MOVE_IN_HISTORY' }
|
||||
| { type: 'DISPLAY_SELECTED_MOVE_IN_HISTORY'; moveId: string }
|
||||
|
|
@ -55,17 +56,18 @@ export const ChessStudy = ({
|
|||
dataAdapter,
|
||||
}: AppProps) => {
|
||||
// Parse Obsidian / Code Block Settings
|
||||
const { boardColor, boardOrientation, chessStudyId } = parseUserConfig(
|
||||
pluginSettings,
|
||||
source
|
||||
);
|
||||
const { boardColor, boardOrientation, viewComments, chessStudyId } =
|
||||
parseUserConfig(pluginSettings, source);
|
||||
|
||||
// Setup Chessground API
|
||||
const [chessView, setChessView] = useState<Api | null>(null);
|
||||
|
||||
// Setup Chess.js API
|
||||
const initialChessLogic = useMemo(() => {
|
||||
const chess = new Chess();
|
||||
const [initialChessLogic, firstPlayer, initialMoveNumber] = useMemo(() => {
|
||||
const chess = new Chess(chessStudyData.rootFEN);
|
||||
|
||||
const firstPlayer = chess.turn();
|
||||
const initialMoveNumber = chess.moveNumber();
|
||||
|
||||
chessStudyData.moves.forEach((move) => {
|
||||
chess.move({
|
||||
|
|
@ -74,16 +76,18 @@ export const ChessStudy = ({
|
|||
promotion: move.promotion,
|
||||
});
|
||||
});
|
||||
return chess;
|
||||
}, [chessStudyData.moves]);
|
||||
|
||||
return [chess, firstPlayer, initialMoveNumber];
|
||||
}, [chessStudyData.moves, chessStudyData.rootFEN]);
|
||||
|
||||
const [chessLogic, setChessLogic] = useState(initialChessLogic);
|
||||
|
||||
const [gameState, dispatch] = useImmerReducer<GameState, GameActions>(
|
||||
(draft, action) => {
|
||||
const hasNoMoves = draft.study.moves.length === 0;
|
||||
switch (action.type) {
|
||||
case 'DISPLAY_NEXT_MOVE_IN_HISTORY': {
|
||||
if (!chessView) return draft;
|
||||
if (!chessView || hasNoMoves) return draft;
|
||||
|
||||
displayMoveInHistory(draft, chessView, setChessLogic, {
|
||||
offset: 1,
|
||||
|
|
@ -93,7 +97,7 @@ export const ChessStudy = ({
|
|||
return draft;
|
||||
}
|
||||
case 'DISPLAY_PREVIOUS_MOVE_IN_HISTORY': {
|
||||
if (!chessView) return draft;
|
||||
if (!chessView || hasNoMoves) return draft;
|
||||
|
||||
displayMoveInHistory(draft, chessView, setChessLogic, {
|
||||
offset: -1,
|
||||
|
|
@ -102,8 +106,62 @@ export const ChessStudy = ({
|
|||
|
||||
return draft;
|
||||
}
|
||||
case 'REMOVE_LAST_MOVE_FROM_HISTORY': {
|
||||
if (!chessView || hasNoMoves) return draft;
|
||||
|
||||
const moves = draft.study.moves;
|
||||
|
||||
const currentMoveId = draft.currentMove?.moveId;
|
||||
|
||||
if (currentMoveId) {
|
||||
const { variant, moveIndex } = findMoveIndex(moves, currentMoveId);
|
||||
|
||||
if (variant) {
|
||||
const parent = moves[variant.parentMoveIndex];
|
||||
const variantMoves = parent.variants[variant.variantIndex].moves;
|
||||
|
||||
const isLastMove = moveIndex === variantMoves.length - 1;
|
||||
|
||||
if (isLastMove) {
|
||||
displayMoveInHistory(draft, chessView, setChessLogic, {
|
||||
offset: -1,
|
||||
selectedMoveId: currentMoveId,
|
||||
});
|
||||
}
|
||||
|
||||
variantMoves.pop();
|
||||
if (variantMoves.length === 0) {
|
||||
parent.variants.splice(variant.variantIndex, 1);
|
||||
}
|
||||
|
||||
if (isLastMove) {
|
||||
draft.currentMove =
|
||||
variantMoves.length > 0
|
||||
? variantMoves[variantMoves.length - 1]
|
||||
: moves[variant.parentMoveIndex];
|
||||
}
|
||||
} else {
|
||||
const isLastMove = moveIndex === moves.length - 1;
|
||||
|
||||
if (isLastMove) {
|
||||
displayMoveInHistory(draft, chessView, setChessLogic, {
|
||||
offset: -1,
|
||||
selectedMoveId: currentMoveId,
|
||||
});
|
||||
}
|
||||
|
||||
moves.pop();
|
||||
|
||||
if (isLastMove) {
|
||||
draft.currentMove = moves.length > 0 ? moves[moves.length - 1] : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return draft;
|
||||
}
|
||||
case 'DISPLAY_SELECTED_MOVE_IN_HISTORY': {
|
||||
if (!chessView) return draft;
|
||||
if (!chessView || hasNoMoves) return draft;
|
||||
|
||||
const selectedMoveId = action.moveId;
|
||||
|
||||
|
|
@ -115,18 +173,26 @@ export const ChessStudy = ({
|
|||
return draft;
|
||||
}
|
||||
case 'SYNC_SHAPES': {
|
||||
if (!chessView || hasNoMoves) return draft;
|
||||
|
||||
const move = getCurrentMove(draft);
|
||||
|
||||
move.shapes = action.shapes;
|
||||
draft.currentMove = move;
|
||||
if (move) {
|
||||
move.shapes = action.shapes;
|
||||
draft.currentMove = move;
|
||||
}
|
||||
|
||||
return draft;
|
||||
}
|
||||
case 'SYNC_COMMENT': {
|
||||
if (!chessView || hasNoMoves) return draft;
|
||||
|
||||
const move = getCurrentMove(draft);
|
||||
|
||||
move.comment = action.comment;
|
||||
draft.currentMove = move;
|
||||
if (move) {
|
||||
move.comment = action.comment;
|
||||
draft.currentMove = move;
|
||||
}
|
||||
|
||||
return draft;
|
||||
}
|
||||
|
|
@ -136,80 +202,94 @@ export const ChessStudy = ({
|
|||
const moves = draft.study.moves;
|
||||
const currentMoveId = draft.currentMove?.moveId;
|
||||
|
||||
const currentMoveIndex = moves.findIndex(
|
||||
(move) => move.moveId === currentMoveId
|
||||
);
|
||||
|
||||
const { variant, moveIndex } = findMoveIndex(moves, currentMoveId);
|
||||
const moveId = nanoid();
|
||||
|
||||
if (variant) {
|
||||
//handle variant
|
||||
const parent = moves[variant.parentMoveIndex];
|
||||
const variantMoves = parent.variants[variant.variantIndex].moves;
|
||||
if (currentMoveId) {
|
||||
const currentMoveIndex = moves.findIndex(
|
||||
(move) => move.moveId === currentMoveId
|
||||
);
|
||||
|
||||
const isLastMove = moveIndex === variantMoves.length - 1;
|
||||
const { variant, moveIndex } = findMoveIndex(moves, currentMoveId);
|
||||
|
||||
//Only push if its the last move in the variant because depth can only be 1
|
||||
if (isLastMove) {
|
||||
const move = {
|
||||
...newMove,
|
||||
moveId: moveId,
|
||||
shapes: [],
|
||||
comment: null,
|
||||
};
|
||||
variantMoves.push(move);
|
||||
if (variant) {
|
||||
//handle variant
|
||||
const parent = moves[variant.parentMoveIndex];
|
||||
const variantMoves = parent.variants[variant.variantIndex].moves;
|
||||
|
||||
const tempChess = new Chess(newMove.after);
|
||||
const isLastMove = moveIndex === variantMoves.length - 1;
|
||||
|
||||
draft.currentMove = move;
|
||||
//Only push if its the last move in the variant because depth can only be 1
|
||||
if (isLastMove) {
|
||||
const move = {
|
||||
...newMove,
|
||||
moveId: moveId,
|
||||
shapes: [],
|
||||
comment: null,
|
||||
};
|
||||
variantMoves.push(move);
|
||||
|
||||
chessView?.set({
|
||||
fen: newMove.after,
|
||||
check: tempChess.isCheck(),
|
||||
});
|
||||
const tempChess = new Chess(newMove.after);
|
||||
|
||||
draft.currentMove = move;
|
||||
|
||||
chessView?.set({
|
||||
fen: newMove.after,
|
||||
check: tempChess.isCheck(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
//handle main line
|
||||
const isLastMove = currentMoveIndex === moves.length - 1;
|
||||
|
||||
if (isLastMove) {
|
||||
const move = {
|
||||
...newMove,
|
||||
moveId: moveId,
|
||||
variants: [],
|
||||
shapes: [],
|
||||
comment: null,
|
||||
};
|
||||
moves.push(move);
|
||||
|
||||
draft.currentMove = move;
|
||||
} else {
|
||||
const currentMove = moves[moveIndex];
|
||||
|
||||
// check if the next move is the same move
|
||||
const nextMove = moves[moveIndex + 1];
|
||||
|
||||
if (nextMove.san === newMove.san) {
|
||||
draft.currentMove = nextMove;
|
||||
return draft;
|
||||
}
|
||||
|
||||
const move = {
|
||||
...newMove,
|
||||
moveId: moveId,
|
||||
shapes: [],
|
||||
comment: null,
|
||||
};
|
||||
|
||||
currentMove.variants.push({
|
||||
parentMoveId: currentMove.moveId,
|
||||
variantId: nanoid(),
|
||||
moves: [move],
|
||||
});
|
||||
|
||||
draft.currentMove = move;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//handle main line
|
||||
const isLastMove = currentMoveIndex === moves.length - 1;
|
||||
const move = {
|
||||
...newMove,
|
||||
moveId: moveId,
|
||||
variants: [],
|
||||
shapes: [],
|
||||
comment: null,
|
||||
};
|
||||
moves.push(move);
|
||||
|
||||
if (isLastMove) {
|
||||
const move = {
|
||||
...newMove,
|
||||
moveId: moveId,
|
||||
variants: [],
|
||||
shapes: [],
|
||||
comment: null,
|
||||
};
|
||||
moves.push(move);
|
||||
|
||||
draft.currentMove = move;
|
||||
} else {
|
||||
const currentMove = moves[moveIndex];
|
||||
|
||||
// check if the next move is the same move
|
||||
const nextMove = moves[moveIndex + 1];
|
||||
|
||||
if (nextMove.san === newMove.san) {
|
||||
draft.currentMove = nextMove;
|
||||
return draft;
|
||||
}
|
||||
|
||||
const move = {
|
||||
...newMove,
|
||||
moveId: moveId,
|
||||
shapes: [],
|
||||
comment: null,
|
||||
};
|
||||
|
||||
currentMove.variants.push({
|
||||
parentMoveId: currentMove.moveId,
|
||||
variantId: nanoid(),
|
||||
moves: [move],
|
||||
});
|
||||
|
||||
draft.currentMove = move;
|
||||
}
|
||||
draft.currentMove = move;
|
||||
}
|
||||
|
||||
return draft;
|
||||
|
|
@ -219,7 +299,7 @@ export const ChessStudy = ({
|
|||
}
|
||||
},
|
||||
{
|
||||
currentMove: chessStudyData.moves[chessStudyData.moves.length - 1],
|
||||
currentMove: chessStudyData.moves[chessStudyData.moves.length - 1] ?? null,
|
||||
isViewOnly: false,
|
||||
study: chessStudyData,
|
||||
}
|
||||
|
|
@ -253,13 +333,19 @@ export const ChessStudy = ({
|
|||
syncShapes={(shapes: DrawShape[]) =>
|
||||
dispatch({ type: 'SYNC_SHAPES', shapes })
|
||||
}
|
||||
shapes={gameState.currentMove?.shapes}
|
||||
shapes={gameState.currentMove?.shapes || []}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="pgn-container">
|
||||
<PgnViewer
|
||||
history={gameState.study.moves}
|
||||
currentMoveId={gameState.currentMove?.moveId}
|
||||
currentMoveId={gameState.currentMove?.moveId ?? null}
|
||||
firstPlayer={firstPlayer}
|
||||
initialMoveNumber={initialMoveNumber}
|
||||
onUndoButtonClick={() =>
|
||||
dispatch({ type: 'REMOVE_LAST_MOVE_FROM_HISTORY' })
|
||||
}
|
||||
onBackButtonClick={() =>
|
||||
dispatch({ type: 'DISPLAY_PREVIOUS_MOVE_IN_HISTORY' })
|
||||
}
|
||||
|
|
@ -273,17 +359,27 @@ export const ChessStudy = ({
|
|||
})
|
||||
}
|
||||
onSaveButtonClick={onSaveButtonClick}
|
||||
onCopyButtonClick={() => {
|
||||
try {
|
||||
navigator.clipboard.writeText(chessLogic.fen());
|
||||
new Notice('Copied to clipboard!');
|
||||
} catch (e) {
|
||||
new Notice('Could not copy to clipboard:', e);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="CommentSection">
|
||||
<CommentSection
|
||||
currentComment={gameState.currentMove?.comment}
|
||||
setComments={(comment: JSONContent) =>
|
||||
dispatch({ type: 'SYNC_COMMENT', comment: comment })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{viewComments && (
|
||||
<div className="CommentSection">
|
||||
<CommentSection
|
||||
currentComment={gameState.currentMove?.comment ?? null}
|
||||
setComments={(comment: JSONContent) =>
|
||||
dispatch({ type: 'SYNC_COMMENT', comment: comment })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
36
src/components/react/PgnViewer/Controls/index.tsx
Normal file
36
src/components/react/PgnViewer/Controls/index.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { ArrowLeft, ArrowRight, Copy, Save, Undo2 } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
export interface ControlActions {
|
||||
onUndoButtonClick: () => void;
|
||||
onBackButtonClick: () => void;
|
||||
onForwardButtonClick: () => void;
|
||||
onSaveButtonClick: () => void;
|
||||
onCopyButtonClick: () => void;
|
||||
}
|
||||
|
||||
export const Controls = (props: ControlActions) => {
|
||||
return (
|
||||
<div>
|
||||
<div className="button-section">
|
||||
<button title="Back" onClick={() => props.onBackButtonClick()}>
|
||||
<ArrowLeft />
|
||||
</button>
|
||||
<button title="Forward" onClick={() => props.onForwardButtonClick()}>
|
||||
<ArrowRight />
|
||||
</button>
|
||||
<button title="Save" onClick={() => props.onSaveButtonClick()}>
|
||||
<Save strokeWidth={'1px'} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="button-section">
|
||||
<button title="Copy FEN" onClick={() => props.onCopyButtonClick()}>
|
||||
<Copy strokeWidth={'1px'} />
|
||||
</button>
|
||||
<button title="Undo" onClick={() => props.onUndoButtonClick()}>
|
||||
<Undo2 />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
74
src/components/react/PgnViewer/MoveItems/index.tsx
Normal file
74
src/components/react/PgnViewer/MoveItems/index.tsx
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import * as React from 'react';
|
||||
|
||||
export const MoveItem = ({
|
||||
isCurrentMove,
|
||||
san,
|
||||
onMoveItemClick,
|
||||
}: {
|
||||
isCurrentMove: boolean;
|
||||
san: string;
|
||||
onMoveItemClick: () => void;
|
||||
}) => {
|
||||
const ref = React.useRef<HTMLParagraphElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (ref.current && isCurrentMove) {
|
||||
ref.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
inline: 'end',
|
||||
});
|
||||
}
|
||||
}, [isCurrentMove]);
|
||||
|
||||
return (
|
||||
<p
|
||||
className={`move-item ${(isCurrentMove && 'active') || ''} vertical-align`}
|
||||
ref={ref}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onMoveItemClick();
|
||||
}}
|
||||
>
|
||||
{san}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
export const VariantMoveItem = ({
|
||||
isCurrentMove,
|
||||
san,
|
||||
onMoveItemClick,
|
||||
moveIndicator = null,
|
||||
}: {
|
||||
isCurrentMove: boolean;
|
||||
san: string;
|
||||
onMoveItemClick: () => void;
|
||||
moveIndicator?: string | null;
|
||||
}) => {
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (ref.current && isCurrentMove) {
|
||||
ref.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
inline: 'end',
|
||||
});
|
||||
}
|
||||
}, [isCurrentMove]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`variant-move-item ${(isCurrentMove && 'active') || ''}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onMoveItemClick();
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<span className={'variant-move-indicator'}>{moveIndicator}</span>
|
||||
{san}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
import { ArrowLeft, ArrowRight, Save } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
import { ReactNode, useEffect, useMemo, useRef } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { ChessStudyMove } from 'src/lib/storage';
|
||||
import { Controls } from './Controls';
|
||||
import { MoveItem, VariantMoveItem } from './MoveItems';
|
||||
|
||||
const chunkArray = <T,>(array: T[], chunkSize: number) => {
|
||||
const chunkArray = <T,>(array: T[], chunkSize: number, offsetByOne = false) => {
|
||||
return array.reduce((resultArray, item, index) => {
|
||||
const chunkIndex = Math.floor(index / chunkSize);
|
||||
const chunkIndex = Math.floor((index + (offsetByOne ? 1 : 0)) / chunkSize);
|
||||
|
||||
if (!resultArray[chunkIndex]) {
|
||||
resultArray[chunkIndex] = [];
|
||||
|
|
@ -17,270 +18,187 @@ const chunkArray = <T,>(array: T[], chunkSize: number) => {
|
|||
}, [] as T[][]);
|
||||
};
|
||||
|
||||
const MoveItem = ({
|
||||
isCurrentMove,
|
||||
san,
|
||||
onMoveItemClick,
|
||||
export const VariantMoveItemContainer = ({
|
||||
children,
|
||||
}: {
|
||||
isCurrentMove: boolean;
|
||||
san: string;
|
||||
onMoveItemClick: () => void;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const ref = useRef<HTMLParagraphElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current && isCurrentMove) {
|
||||
ref.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
inline: 'end',
|
||||
});
|
||||
}
|
||||
}, [isCurrentMove]);
|
||||
|
||||
return (
|
||||
<p
|
||||
className={`move-item ${
|
||||
(isCurrentMove && 'active') || ''
|
||||
} vertical-align`}
|
||||
ref={ref}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onMoveItemClick();
|
||||
}}
|
||||
>
|
||||
{san}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
const VariantMoveItem = ({
|
||||
isCurrentMove,
|
||||
san,
|
||||
onMoveItemClick,
|
||||
moveIndicator = null,
|
||||
}: {
|
||||
isCurrentMove: boolean;
|
||||
san: string;
|
||||
onMoveItemClick: () => void;
|
||||
moveIndicator?: string | null;
|
||||
}) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current && isCurrentMove) {
|
||||
ref.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
inline: 'end',
|
||||
});
|
||||
}
|
||||
}, [isCurrentMove]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`variant-move-item ${(isCurrentMove && 'active') || ''}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onMoveItemClick();
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<span className={'variant-move-indicator'}>{moveIndicator}</span>
|
||||
{san}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const VariantMoveItemContainer = ({ children }: { children: ReactNode }) => {
|
||||
return <div className="variant-move-item-container">{children}</div>;
|
||||
};
|
||||
|
||||
const VariantContainer = ({ children }: { children: ReactNode }) => {
|
||||
export const VariantContainer = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
return <div className="variant-container">{children}</div>;
|
||||
};
|
||||
|
||||
const VariantsContainer = ({ children }: { children: ReactNode }) => {
|
||||
export const VariantsContainer = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
return <div className="variants-container">{children}</div>;
|
||||
};
|
||||
|
||||
export const PgnViewer = React.memo(
|
||||
({
|
||||
interface PgnViewerProps {
|
||||
history: ChessStudyMove[];
|
||||
currentMoveId: string | null;
|
||||
firstPlayer: string;
|
||||
initialMoveNumber: number;
|
||||
onMoveItemClick: (moveId: string) => void;
|
||||
onUndoButtonClick: () => void;
|
||||
onBackButtonClick: () => void;
|
||||
onForwardButtonClick: () => void;
|
||||
onSaveButtonClick: () => void;
|
||||
onCopyButtonClick: () => void;
|
||||
}
|
||||
|
||||
export const PgnViewer = React.memo((props: PgnViewerProps) => {
|
||||
const {
|
||||
history,
|
||||
currentMoveId,
|
||||
onBackButtonClick,
|
||||
onForwardButtonClick,
|
||||
firstPlayer,
|
||||
initialMoveNumber,
|
||||
onMoveItemClick,
|
||||
onSaveButtonClick,
|
||||
}: {
|
||||
history: ChessStudyMove[];
|
||||
currentMoveId: string;
|
||||
onBackButtonClick: () => void;
|
||||
onForwardButtonClick: () => void;
|
||||
onMoveItemClick: (moveId: string) => void;
|
||||
onSaveButtonClick: () => void;
|
||||
}) => {
|
||||
const movePairs = useMemo(() => chunkArray(history, 2), [history]);
|
||||
...controlActions
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className="height-width-100">
|
||||
<div className="move-item-section">
|
||||
<div className="move-item-container">
|
||||
{movePairs.map((pair, currentMoveIndex) => {
|
||||
const [wMove, bMove] = pair;
|
||||
const movePairs = useMemo(
|
||||
() => chunkArray(history, 2, firstPlayer === 'b'),
|
||||
[firstPlayer, history]
|
||||
);
|
||||
|
||||
return (
|
||||
<React.Fragment key={wMove.san + bMove?.san + currentMoveIndex}>
|
||||
<p className="move-indicator center">
|
||||
{currentMoveIndex + 1}
|
||||
</p>
|
||||
return (
|
||||
<div className="height-width-100">
|
||||
<div className="move-item-section">
|
||||
<div className="move-item-container">
|
||||
{movePairs.map((pair, currentMoveIndex) => {
|
||||
const [wMove, bMove] = pair;
|
||||
|
||||
return (
|
||||
<React.Fragment key={wMove.san + bMove?.san + currentMoveIndex}>
|
||||
<p className="move-indicator center">
|
||||
{currentMoveIndex + initialMoveNumber}
|
||||
</p>
|
||||
{firstPlayer === 'b' && !bMove && currentMoveIndex === 0 && (
|
||||
<MoveItem
|
||||
san={wMove.san}
|
||||
isCurrentMove={wMove.moveId === currentMoveId}
|
||||
onMoveItemClick={() => onMoveItemClick(wMove.moveId)}
|
||||
san={'...'}
|
||||
isCurrentMove={false}
|
||||
onMoveItemClick={() => {}}
|
||||
/>
|
||||
{bMove && (
|
||||
<MoveItem
|
||||
san={bMove.san}
|
||||
isCurrentMove={bMove.moveId === currentMoveId}
|
||||
onMoveItemClick={() => onMoveItemClick(bMove.moveId)}
|
||||
/>
|
||||
)}
|
||||
{!!wMove.variants.concat(bMove?.variants || []).length && (
|
||||
<VariantsContainer>
|
||||
{!!wMove.variants.length && (
|
||||
<VariantContainer>
|
||||
{wMove.variants.map((variant) => {
|
||||
return (
|
||||
<VariantMoveItemContainer key={variant.variantId}>
|
||||
{chunkArray(variant.moves, 2).map(
|
||||
(pair, wMoveVarianti) => {
|
||||
const [bMove, wMove] = pair;
|
||||
)}
|
||||
<MoveItem
|
||||
san={wMove.san}
|
||||
isCurrentMove={wMove.moveId === currentMoveId}
|
||||
onMoveItemClick={() => onMoveItemClick(wMove.moveId)}
|
||||
/>
|
||||
{bMove && (
|
||||
<MoveItem
|
||||
san={bMove.san}
|
||||
isCurrentMove={bMove.moveId === currentMoveId}
|
||||
onMoveItemClick={() => onMoveItemClick(bMove.moveId)}
|
||||
/>
|
||||
)}
|
||||
{!!wMove.variants.concat(bMove?.variants || []).length && (
|
||||
<VariantsContainer>
|
||||
{!!wMove.variants.length && (
|
||||
<VariantContainer>
|
||||
{wMove.variants.map((variant) => {
|
||||
return (
|
||||
<VariantMoveItemContainer key={variant.variantId}>
|
||||
{chunkArray(variant.moves, 2).map((pair, wMoveVarianti) => {
|
||||
const [bMove, wMove] = pair;
|
||||
|
||||
return (
|
||||
<React.Fragment
|
||||
key={
|
||||
bMove.san +
|
||||
wMove?.san +
|
||||
currentMoveIndex
|
||||
return (
|
||||
<React.Fragment
|
||||
key={bMove.san + wMove?.san + currentMoveIndex}
|
||||
>
|
||||
<VariantMoveItem
|
||||
isCurrentMove={bMove.moveId === currentMoveId}
|
||||
san={bMove.san}
|
||||
onMoveItemClick={() => onMoveItemClick(bMove.moveId)}
|
||||
moveIndicator={
|
||||
(wMoveVarianti === 0 &&
|
||||
(firstPlayer === 'w' || currentMoveIndex > 0) &&
|
||||
`${
|
||||
currentMoveIndex + initialMoveNumber + wMoveVarianti
|
||||
}... `) ||
|
||||
(firstPlayer === 'b' &&
|
||||
currentMoveIndex === 0 &&
|
||||
`${
|
||||
currentMoveIndex + initialMoveNumber + wMoveVarianti
|
||||
}. `) ||
|
||||
null
|
||||
}
|
||||
/>
|
||||
{wMove && (
|
||||
<VariantMoveItem
|
||||
isCurrentMove={wMove.moveId === currentMoveId}
|
||||
san={wMove.san}
|
||||
onMoveItemClick={() => onMoveItemClick(wMove.moveId)}
|
||||
moveIndicator={
|
||||
((firstPlayer === 'w' || currentMoveIndex > 0) &&
|
||||
`${
|
||||
currentMoveIndex + initialMoveNumber + 1 + wMoveVarianti
|
||||
}. `) ||
|
||||
null
|
||||
}
|
||||
>
|
||||
<VariantMoveItem
|
||||
isCurrentMove={
|
||||
bMove.moveId === currentMoveId
|
||||
}
|
||||
san={bMove.san}
|
||||
onMoveItemClick={() =>
|
||||
onMoveItemClick(bMove.moveId)
|
||||
}
|
||||
moveIndicator={
|
||||
(wMoveVarianti === 0 &&
|
||||
`${
|
||||
currentMoveIndex +
|
||||
1 +
|
||||
wMoveVarianti
|
||||
}... `) ||
|
||||
null
|
||||
}
|
||||
/>
|
||||
{wMove && (
|
||||
<VariantMoveItem
|
||||
isCurrentMove={
|
||||
wMove.moveId === currentMoveId
|
||||
}
|
||||
san={wMove.san}
|
||||
onMoveItemClick={() =>
|
||||
onMoveItemClick(wMove.moveId)
|
||||
}
|
||||
moveIndicator={
|
||||
(wMoveVarianti % 2 === 0 &&
|
||||
null) ||
|
||||
`${
|
||||
currentMoveIndex +
|
||||
2 +
|
||||
wMoveVarianti
|
||||
}. `
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</VariantMoveItemContainer>
|
||||
);
|
||||
})}
|
||||
</VariantContainer>
|
||||
)}
|
||||
{!!bMove?.variants.length && (
|
||||
<VariantContainer>
|
||||
{bMove.variants.map((variant) => {
|
||||
return (
|
||||
<VariantMoveItemContainer key={variant.variantId}>
|
||||
{chunkArray(variant.moves, 2).map(
|
||||
(pair, bMoveVarianti) => {
|
||||
const [wMove, bMove] = pair;
|
||||
return (
|
||||
<React.Fragment
|
||||
key={
|
||||
wMove.san +
|
||||
bMove?.san +
|
||||
currentMoveIndex
|
||||
}
|
||||
>
|
||||
<VariantMoveItem
|
||||
isCurrentMove={
|
||||
wMove.moveId === currentMoveId
|
||||
}
|
||||
san={wMove.san}
|
||||
onMoveItemClick={() =>
|
||||
onMoveItemClick(wMove.moveId)
|
||||
}
|
||||
moveIndicator={`${
|
||||
currentMoveIndex + 2 + bMoveVarianti
|
||||
}. `}
|
||||
/>
|
||||
{bMove && (
|
||||
<VariantMoveItem
|
||||
isCurrentMove={
|
||||
bMove.moveId === currentMoveId
|
||||
}
|
||||
san={bMove.san}
|
||||
onMoveItemClick={() =>
|
||||
onMoveItemClick(bMove.moveId)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</VariantMoveItemContainer>
|
||||
);
|
||||
})}
|
||||
</VariantContainer>
|
||||
)}
|
||||
</VariantsContainer>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="button-section">
|
||||
<button onClick={() => onBackButtonClick()}>
|
||||
<ArrowLeft />
|
||||
</button>
|
||||
<button onClick={() => onForwardButtonClick()}>
|
||||
<ArrowRight />
|
||||
</button>
|
||||
<button onClick={() => onSaveButtonClick()}>
|
||||
<Save strokeWidth={'1px'} />
|
||||
</button>
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</VariantMoveItemContainer>
|
||||
);
|
||||
})}
|
||||
</VariantContainer>
|
||||
)}
|
||||
{!!bMove?.variants.length && (
|
||||
<VariantContainer>
|
||||
{bMove.variants.map((variant) => {
|
||||
return (
|
||||
<VariantMoveItemContainer key={variant.variantId}>
|
||||
{chunkArray(variant.moves, 2).map((pair, bMoveVarianti) => {
|
||||
const [wMove, bMove] = pair;
|
||||
return (
|
||||
<React.Fragment
|
||||
key={wMove.san + bMove?.san + currentMoveIndex}
|
||||
>
|
||||
<VariantMoveItem
|
||||
isCurrentMove={wMove.moveId === currentMoveId}
|
||||
san={wMove.san}
|
||||
onMoveItemClick={() => onMoveItemClick(wMove.moveId)}
|
||||
moveIndicator={`${
|
||||
currentMoveIndex + initialMoveNumber + 1 + bMoveVarianti
|
||||
}. `}
|
||||
/>
|
||||
{bMove && (
|
||||
<VariantMoveItem
|
||||
isCurrentMove={bMove.moveId === currentMoveId}
|
||||
san={bMove.san}
|
||||
onMoveItemClick={() => onMoveItemClick(bMove.moveId)}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</VariantMoveItemContainer>
|
||||
);
|
||||
})}
|
||||
</VariantContainer>
|
||||
)}
|
||||
</VariantsContainer>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
<Controls {...controlActions} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
PgnViewer.displayName = 'PgnViewer';
|
||||
|
|
|
|||
|
|
@ -3,14 +3,9 @@ import { Move } from 'chess.js';
|
|||
import { DrawShape } from 'chessground/draw';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { DataAdapter, normalizePath } from 'obsidian';
|
||||
import { ROOT_FEN } from 'src/main';
|
||||
|
||||
export const CURRENT_STORAGE_VERSION = '0.0.1';
|
||||
|
||||
export interface VariantMove extends Move {
|
||||
moveId: string;
|
||||
shapes: DrawShape[];
|
||||
comment: JSONContent | null;
|
||||
}
|
||||
export const CURRENT_STORAGE_VERSION = '0.0.2';
|
||||
|
||||
export interface Variant {
|
||||
variantId: string;
|
||||
|
|
@ -18,6 +13,12 @@ export interface Variant {
|
|||
moves: VariantMove[];
|
||||
}
|
||||
|
||||
export interface VariantMove extends Move {
|
||||
moveId: string;
|
||||
shapes: DrawShape[];
|
||||
comment: JSONContent | null;
|
||||
}
|
||||
|
||||
export interface ChessStudyMove extends Move {
|
||||
moveId: string;
|
||||
variants: Variant[];
|
||||
|
|
@ -29,6 +30,7 @@ export interface ChessStudyFileData {
|
|||
version: string;
|
||||
header: { title: string | null };
|
||||
moves: ChessStudyMove[];
|
||||
rootFEN: string;
|
||||
}
|
||||
|
||||
export class ChessStudyDataAdapter {
|
||||
|
|
@ -42,6 +44,13 @@ export class ChessStudyDataAdapter {
|
|||
|
||||
async saveFile(data: ChessStudyFileData, id?: string) {
|
||||
const chessStudyId = id || nanoid();
|
||||
|
||||
console.log(
|
||||
`Writing file to ${normalizePath(
|
||||
`${this.storagePath}/${chessStudyId}.json`
|
||||
)}`
|
||||
);
|
||||
|
||||
await this.adapter.write(
|
||||
normalizePath(`${this.storagePath}/${chessStudyId}.json`),
|
||||
JSON.stringify(data, null, 2),
|
||||
|
|
@ -52,16 +61,29 @@ export class ChessStudyDataAdapter {
|
|||
}
|
||||
|
||||
async loadFile(id: string): Promise<ChessStudyFileData> {
|
||||
console.log(
|
||||
`Reading file from ${normalizePath(`${this.storagePath}/${id}.json`)}`
|
||||
);
|
||||
|
||||
const data = await this.adapter.read(
|
||||
normalizePath(`${this.storagePath}/${id}.json`)
|
||||
);
|
||||
return JSON.parse(data);
|
||||
|
||||
const jsonData = JSON.parse(data);
|
||||
|
||||
//Make sure data is compatible with storage version 0.0.1.
|
||||
if (!jsonData.rootFEN) {
|
||||
return { ...jsonData, rootFEN: ROOT_FEN };
|
||||
}
|
||||
|
||||
return jsonData;
|
||||
}
|
||||
|
||||
async createStorageFolderIfNotExists() {
|
||||
const folderExists = await this.adapter.exists(this.storagePath);
|
||||
|
||||
if (!folderExists) {
|
||||
console.log(`Creating storage folder at: ${this.storagePath}`);
|
||||
this.adapter.mkdir(this.storagePath);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,9 +18,7 @@ export const findMoveIndex = (
|
|||
if (move.moveId === moveId) return { variant: null, moveIndex: iMainLine };
|
||||
|
||||
for (const [iVariant, variant] of move.variants.entries()) {
|
||||
const moveIndex = variant.moves.findIndex(
|
||||
(move) => move.moveId === moveId
|
||||
);
|
||||
const moveIndex = variant.moves.findIndex((move) => move.moveId === moveId);
|
||||
|
||||
if (moveIndex >= 0)
|
||||
return {
|
||||
|
|
@ -48,64 +46,99 @@ export const displayMoveInHistory = (
|
|||
|
||||
//Figure out where we are
|
||||
const currentMove = draft.currentMove;
|
||||
const currentMoveId = currentMove.moveId;
|
||||
|
||||
const moves = draft.study.moves;
|
||||
if (currentMove) {
|
||||
const currentMoveId = currentMove.moveId;
|
||||
|
||||
//If we pass a moveId, find out where that is and offset from there, otherwise take current moveId
|
||||
const baseMoveId = selectedMoveId || currentMoveId;
|
||||
const moves = draft.study.moves;
|
||||
|
||||
const { variant, moveIndex } = findMoveIndex(moves, baseMoveId);
|
||||
//Are we in a variant? Are we not? Decide which move to display
|
||||
//If we pass a moveId, find out where that is and offset from there, otherwise take current moveId
|
||||
const baseMoveId = selectedMoveId || currentMoveId;
|
||||
|
||||
if (variant) {
|
||||
const variantMoves =
|
||||
moves[variant.parentMoveIndex].variants[variant.variantIndex].moves;
|
||||
const { variant, moveIndex } = findMoveIndex(moves, baseMoveId);
|
||||
//Are we in a variant? Are we not? Decide which move to display
|
||||
|
||||
if (typeof variantMoves[moveIndex + offset] !== 'undefined') {
|
||||
moveToDisplay = variantMoves[moveIndex + offset];
|
||||
}
|
||||
} else {
|
||||
if (typeof moves[moveIndex + offset] !== 'undefined') {
|
||||
moveToDisplay = moves[moveIndex + offset];
|
||||
if (variant) {
|
||||
const variantMoves =
|
||||
moves[variant.parentMoveIndex].variants[variant.variantIndex].moves;
|
||||
|
||||
if (typeof variantMoves[moveIndex + offset] !== 'undefined') {
|
||||
moveToDisplay = variantMoves[moveIndex + offset];
|
||||
}
|
||||
|
||||
if (typeof moveToDisplay === 'undefined') {
|
||||
moveToDisplay = moves[variant.parentMoveIndex + offset];
|
||||
}
|
||||
} else {
|
||||
if (typeof moves[moveIndex + offset] !== 'undefined') {
|
||||
moveToDisplay = moves[moveIndex + offset];
|
||||
}
|
||||
}
|
||||
} else if (offset < 0) {
|
||||
moveToDisplay = draft.study.moves[draft.study.moves.length - 1];
|
||||
} else if (offset > 0) {
|
||||
moveToDisplay = draft.study.moves[0];
|
||||
}
|
||||
|
||||
if (!moveToDisplay) return draft;
|
||||
if (moveToDisplay) {
|
||||
const chess = new Chess(moveToDisplay.after);
|
||||
|
||||
const chess = new Chess(moveToDisplay.after);
|
||||
chessView.set({
|
||||
fen: moveToDisplay.after,
|
||||
check: chess.isCheck(),
|
||||
movable: {
|
||||
free: false,
|
||||
color: toColor(chess),
|
||||
dests: toDests(chess),
|
||||
},
|
||||
turnColor: toColor(chess),
|
||||
});
|
||||
|
||||
chessView.set({
|
||||
fen: moveToDisplay.after,
|
||||
check: chess.isCheck(),
|
||||
movable: {
|
||||
free: false,
|
||||
color: toColor(chess),
|
||||
dests: toDests(chess),
|
||||
},
|
||||
turnColor: toColor(chess),
|
||||
});
|
||||
draft.currentMove = moveToDisplay;
|
||||
|
||||
draft.currentMove = moveToDisplay;
|
||||
setChessLogic(chess);
|
||||
} else if (offset !== 0){
|
||||
const chess = draft.study.root.fen ? new Chess(draft.study.root.fen) : new Chess();
|
||||
|
||||
setChessLogic(chess);
|
||||
chessView.set({
|
||||
fen: chess.fen(),
|
||||
check: chess.isCheck(),
|
||||
movable: {
|
||||
free: false,
|
||||
color: toColor(chess),
|
||||
dests: toDests(chess),
|
||||
},
|
||||
turnColor: toColor(chess),
|
||||
});
|
||||
|
||||
draft.currentMove = null;
|
||||
|
||||
setChessLogic(chess);
|
||||
} else {
|
||||
console.log(`No move to display found`);
|
||||
return draft;
|
||||
}
|
||||
|
||||
return draft;
|
||||
};
|
||||
|
||||
export const getCurrentMove = (
|
||||
draft: Draft<GameState>
|
||||
): Draft<ChessStudyMove> | Draft<VariantMove> => {
|
||||
): Draft<ChessStudyMove> | Draft<VariantMove> | null => {
|
||||
const currentMoveId = draft.currentMove?.moveId;
|
||||
const moves = draft.study.moves;
|
||||
|
||||
const { variant, moveIndex } = findMoveIndex(moves, currentMoveId);
|
||||
if (currentMoveId) {
|
||||
const { variant, moveIndex } = findMoveIndex(moves, currentMoveId);
|
||||
|
||||
if (variant) {
|
||||
return moves[variant.parentMoveIndex].variants[variant.variantIndex].moves[
|
||||
moveIndex
|
||||
];
|
||||
} else {
|
||||
return moves[moveIndex];
|
||||
if (variant) {
|
||||
return moves[variant.parentMoveIndex].variants[variant.variantIndex].moves[
|
||||
moveIndex
|
||||
];
|
||||
} else {
|
||||
return moves[moveIndex];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
|
|
|||
18
src/main.css
18
src/main.css
|
|
@ -78,7 +78,7 @@
|
|||
/* PGN Viewer */
|
||||
|
||||
.chess-study .move-item-section {
|
||||
height: 415px;
|
||||
height: 380px;
|
||||
}
|
||||
|
||||
.chess-study .button-section {
|
||||
|
|
@ -87,7 +87,7 @@
|
|||
align-items: center;
|
||||
width: 100%;
|
||||
gap: 4px;
|
||||
height: 35px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
/* Move Items */
|
||||
|
|
@ -95,23 +95,23 @@
|
|||
.chess-study .move-item-container {
|
||||
display: grid;
|
||||
grid-template-columns: 0.15fr 0.425fr 0.425fr;
|
||||
grid-auto-rows: minmax(30px, auto);
|
||||
grid-auto-rows: minmax(35px, auto);
|
||||
width: 100%;
|
||||
max-height: 415px;
|
||||
max-height: 380px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.chess-study .move-item-container > * {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.chess-study .move-item {
|
||||
padding-left: 8px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.chess-study .move-item:hover {
|
||||
background-color: hsl(
|
||||
var(--accent-h),
|
||||
var(--accent-s),
|
||||
calc(var(--accent-l))
|
||||
);
|
||||
background-color: hsl(var(--accent-h), var(--accent-s), calc(var(--accent-l)));
|
||||
color: var(--text-on-accent);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
|
|||
41
src/main.tsx
41
src/main.tsx
|
|
@ -6,7 +6,7 @@ import {
|
|||
ChessStudyFileData,
|
||||
} from 'src/lib/storage';
|
||||
import { ReactView } from './components/ReactView';
|
||||
import { PgnModal } from './components/obsidian/PgnModal';
|
||||
import { ChessStringModal } from './components/obsidian/ChessStringModal';
|
||||
import {
|
||||
ChessStudyPluginSettings,
|
||||
DEFAULT_SETTINGS,
|
||||
|
|
@ -22,6 +22,17 @@ import { nanoid } from 'nanoid';
|
|||
import { parseUserConfig } from './lib/obsidian';
|
||||
import './main.css';
|
||||
|
||||
type FEN = string;
|
||||
type PGN = string;
|
||||
export type ChessString = FEN | PGN;
|
||||
|
||||
export const ROOT_FEN =
|
||||
'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1';
|
||||
|
||||
// TODO:
|
||||
// 1) Allow to show the root position
|
||||
// 2) Display correct move after removing the last move
|
||||
|
||||
export default class ChessStudyPlugin extends Plugin {
|
||||
settings: ChessStudyPluginSettings;
|
||||
dataAdapter: ChessStudyDataAdapter;
|
||||
|
|
@ -39,7 +50,7 @@ export default class ChessStudyPlugin extends Plugin {
|
|||
this.storagePath
|
||||
);
|
||||
|
||||
this.dataAdapter.createStorageFolderIfNotExists();
|
||||
await this.dataAdapter.createStorageFolderIfNotExists();
|
||||
|
||||
// Add settings tab
|
||||
this.addSettingTab(new SettingsTab(this.app, this));
|
||||
|
|
@ -47,17 +58,21 @@ export default class ChessStudyPlugin extends Plugin {
|
|||
// Add command
|
||||
this.addCommand({
|
||||
id: 'insert-chess-study',
|
||||
name: 'Insert PGN-Editor at cursor position',
|
||||
name: 'Insert FEN/PGN-Editor at cursor position',
|
||||
editorCallback: (editor: Editor) => {
|
||||
const cursorPosition = editor.getCursor();
|
||||
|
||||
const onSubmit = async (pgn: string) => {
|
||||
const onSubmit = async (chessString: ChessString | undefined) => {
|
||||
try {
|
||||
const chess = new Chess();
|
||||
const chessStringTrimmed = chessString?.trim() ?? '';
|
||||
|
||||
if (pgn) {
|
||||
const isFen = chessStringTrimmed.includes('/');
|
||||
|
||||
const chess = isFen ? new Chess(chessStringTrimmed) : new Chess();
|
||||
|
||||
if (!isFen) {
|
||||
//Try to parse the PGN
|
||||
chess.loadPgn(pgn, {
|
||||
chess.loadPgn(chessStringTrimmed, {
|
||||
strict: false,
|
||||
});
|
||||
}
|
||||
|
|
@ -74,6 +89,7 @@ export default class ChessStudyPlugin extends Plugin {
|
|||
shapes: [],
|
||||
comment: null,
|
||||
})),
|
||||
rootFEN: isFen ? chessStringTrimmed : ROOT_FEN,
|
||||
};
|
||||
|
||||
this.dataAdapter.createStorageFolderIfNotExists();
|
||||
|
|
@ -90,7 +106,7 @@ export default class ChessStudyPlugin extends Plugin {
|
|||
}
|
||||
};
|
||||
|
||||
new PgnModal(this.app, onSubmit).open();
|
||||
new ChessStringModal(this.app, onSubmit).open();
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -110,14 +126,7 @@ export default class ChessStudyPlugin extends Plugin {
|
|||
const data = await this.dataAdapter.loadFile(chessStudyId);
|
||||
|
||||
ctx.addChild(
|
||||
new ReactView(
|
||||
el,
|
||||
source,
|
||||
this.app,
|
||||
this.settings,
|
||||
data,
|
||||
this.dataAdapter
|
||||
)
|
||||
new ReactView(el, source, this.app, this.settings, data, this.dataAdapter)
|
||||
);
|
||||
} catch (e) {
|
||||
new Notice(
|
||||
|
|
|
|||
18
styles.css
18
styles.css
File diff suppressed because one or more lines are too long
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"1.0.0": "0.15.0",
|
||||
"1.1.0": "0.15.0"
|
||||
"1.1.0": "0.15.0",
|
||||
"1.2.0": "1.4.0"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue