From 137821c4daa7c7f0f94c43dafd971327e2d1a51b Mon Sep 17 00:00:00 2001 From: chrislicodes Date: Sun, 4 Feb 2024 19:02:36 +0100 Subject: [PATCH] chore: minor improvements --- .github/workflows/release.yml | 48 +- .prettierrc | 2 +- README.md | 19 +- .../{PgnModal.ts => ChessStringModal.ts} | 13 +- src/components/obsidian/SettingsTab.ts | 24 +- src/components/react/ChessStudy.tsx | 257 +++++----- .../react/PgnViewer/Controls/index.tsx | 36 ++ .../react/PgnViewer/MoveItems/index.tsx | 74 +++ src/components/react/PgnViewer/index.tsx | 445 +++++++----------- src/lib/storage/index.ts | 38 +- src/lib/ui-state/index.ts | 96 ++-- src/main.css | 14 +- src/main.tsx | 52 +- styles.css | 18 +- 14 files changed, 583 insertions(+), 553 deletions(-) rename src/components/obsidian/{PgnModal.ts => ChessStringModal.ts} (65%) create mode 100644 src/components/react/PgnViewer/Controls/index.tsx create mode 100644 src/components/react/PgnViewer/MoveItems/index.tsx diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1715568..880c9bb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 diff --git a/.prettierrc b/.prettierrc index ac5c94e..c2a4773 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,6 +1,6 @@ { "singleQuote": true, - "tabWidth": 2, + "tabWidth": 1, "proseWrap": "preserve", "semi": true } diff --git a/README.md b/README.md index 81f0ca0..6ba0d60 100644 --- a/README.md +++ b/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 ![chess-study-variants](imgs/chess-study-variants.png) +### 1.2.0 + +- [x] Add FEN start position support +- [x] Add undo button +- [x] Copy current FEN string to clipboard + +Thanks to @latenitecoding for the contributions + ## Settings Here are the available settings for a `chessStudy` code block: @@ -71,19 +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 | -| `fen` | Valid FEN string | FEN string | | `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 diff --git a/src/components/obsidian/PgnModal.ts b/src/components/obsidian/ChessStringModal.ts similarity index 65% rename from src/components/obsidian/PgnModal.ts rename to src/components/obsidian/ChessStringModal.ts index 3bf882c..a346ce3 100644 --- a/src/components/obsidian/PgnModal.ts +++ b/src/components/obsidian/ChessStringModal.ts @@ -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); }) ); } diff --git a/src/components/obsidian/SettingsTab.ts b/src/components/obsidian/SettingsTab.ts index 9dbdf9b..99916f2 100644 --- a/src/components/obsidian/SettingsTab.ts +++ b/src/components/obsidian/SettingsTab.ts @@ -4,14 +4,12 @@ import ChessStudyPlugin from 'src/main'; export interface ChessStudyPluginSettings { boardOrientation: 'white' | 'black'; boardColor: 'green' | 'brown'; - fen: string; viewComments: true | false; } export const DEFAULT_SETTINGS: ChessStudyPluginSettings = { boardOrientation: 'white', boardColor: 'green', - fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1', viewComments: true, }; @@ -38,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(); }); }); @@ -60,32 +56,18 @@ export class SettingsTab extends PluginSettingTab { }); }); - new Setting(containerEl) - .setName('FEN') - .setDesc('Sets the default board position') - .addText((text) => { - text - .setValue(this.plugin.settings.fen) - .onChange((fen) => { - this.plugin.settings.fen = fen as string; - 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) + .setValue(this.plugin.settings.viewComments.toString()) .onChange((viewComments) => { - this.plugin.settings.viewComments = (viewComments == true || viewComments == "true" || viewComments == "True") as true | false; + this.plugin.settings.viewComments = viewComments === 'true'; this.plugin.saveSettings(); }); }); - } } diff --git a/src/components/react/ChessStudy.tsx b/src/components/react/ChessStudy.tsx index 06471e9..a06e28b 100644 --- a/src/components/react/ChessStudy.tsx +++ b/src/components/react/ChessStudy.tsx @@ -35,7 +35,7 @@ interface AppProps { } export interface GameState { - currentMove: ChessStudyMove | VariantMove; + currentMove: ChessStudyMove | VariantMove | null; isViewOnly: boolean; study: ChessStudyFileData; } @@ -56,16 +56,15 @@ export const ChessStudy = ({ dataAdapter, }: AppProps) => { // Parse Obsidian / Code Block Settings - const { boardColor, boardOrientation, fen, viewComments, chessStudyId } = - + const { boardColor, boardOrientation, viewComments, chessStudyId } = parseUserConfig(pluginSettings, source); // Setup Chessground API const [chessView, setChessView] = useState(null); // Setup Chess.js API - const [firstPlayer, initialMoveNumber, initialChessLogic] = useMemo(() => { - const chess = (fen) ? new Chess(fen) : new Chess(); + const [initialChessLogic, firstPlayer, initialMoveNumber] = useMemo(() => { + const chess = new Chess(chessStudyData.rootFEN); const firstPlayer = chess.turn(); const initialMoveNumber = chess.moveNumber(); @@ -78,16 +77,17 @@ export const ChessStudy = ({ }); }); - return [firstPlayer, initialMoveNumber, chess]; - }, [chessStudyData.moves]); + return [chess, firstPlayer, initialMoveNumber]; + }, [chessStudyData.moves, chessStudyData.rootFEN]); const [chessLogic, setChessLogic] = useState(initialChessLogic); const [gameState, dispatch] = useImmerReducer( (draft, action) => { + const hasNoMoves = draft.study.moves.length === 0; switch (action.type) { case 'DISPLAY_NEXT_MOVE_IN_HISTORY': { - if (!chessView || !draft || draft.study.moves.length == 0) return draft; + if (!chessView || hasNoMoves) return draft; displayMoveInHistory(draft, chessView, setChessLogic, { offset: 1, @@ -97,7 +97,7 @@ export const ChessStudy = ({ return draft; } case 'DISPLAY_PREVIOUS_MOVE_IN_HISTORY': { - if (!chessView || !draft || draft.study.moves.length == 0) return draft; + if (!chessView || hasNoMoves) return draft; displayMoveInHistory(draft, chessView, setChessLogic, { offset: -1, @@ -107,56 +107,61 @@ export const ChessStudy = ({ return draft; } case 'REMOVE_LAST_MOVE_FROM_HISTORY': { - if (!chessView || !draft || draft.study.moves.length == 0) return draft; + if (!chessView || hasNoMoves) return draft; - let moves = draft.study.moves; + const moves = draft.study.moves; const currentMoveId = draft.currentMove?.moveId; - const { variant, moveIndex } = findMoveIndex(moves, currentMoveId); + if (currentMoveId) { + const { variant, moveIndex } = findMoveIndex(moves, currentMoveId); - if (variant) { - const parent = moves[variant.parentMoveIndex]; - const variantMoves = parent.variants[variant.variantIndex].moves; + if (variant) { + const parent = moves[variant.parentMoveIndex]; + const variantMoves = parent.variants[variant.variantIndex].moves; - const isLastMove = moveIndex === variantMoves.length - 1; + const isLastMove = moveIndex === variantMoves.length - 1; - if (isLastMove) { - displayMoveInHistory(draft, chessView, setChessLogic, { - offset: -1, - selectedMoveId: currentMoveId, - }); - } + if (isLastMove) { + displayMoveInHistory(draft, chessView, setChessLogic, { + offset: -1, + selectedMoveId: currentMoveId, + }); + } - variantMoves.pop(); - if (variantMoves.length == 0) { - parent.variants.splice(variant.variantIndex, 1); - } + 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) { + 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, - }); - } + if (isLastMove) { + displayMoveInHistory(draft, chessView, setChessLogic, { + offset: -1, + selectedMoveId: currentMoveId, + }); + } - moves.pop(); + moves.pop(); - if (isLastMove) { - draft.currentMove = (moves.length > 0) ? moves[moves.length - 1] : null; + if (isLastMove) { + draft.currentMove = moves.length > 0 ? moves[moves.length - 1] : null; + } } } return draft; } case 'DISPLAY_SELECTED_MOVE_IN_HISTORY': { - if (!chessView || !draft || draft.study.moves.length == 0) return draft; + if (!chessView || hasNoMoves) return draft; const selectedMoveId = action.moveId; @@ -168,22 +173,26 @@ export const ChessStudy = ({ return draft; } case 'SYNC_SHAPES': { - if (!chessView || !draft || draft.study.moves.length == 0) return draft; + 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 || !draft || draft.study.moves.length == 0) return draft; + 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; } @@ -193,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; @@ -276,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, } @@ -310,14 +333,14 @@ export const ChessStudy = ({ syncShapes={(shapes: DrawShape[]) => dispatch({ type: 'SYNC_SHAPES', shapes }) } - shapes={gameState.currentMove?.shapes} + shapes={gameState.currentMove?.shapes || []} /> +
@@ -338,7 +361,7 @@ export const ChessStudy = ({ onSaveButtonClick={onSaveButtonClick} onCopyButtonClick={() => { try { - navigator.clipboard.writeText(chessLogic.fen()) + navigator.clipboard.writeText(chessLogic.fen()); new Notice('Copied to clipboard!'); } catch (e) { new Notice('Could not copy to clipboard:', e); @@ -350,7 +373,7 @@ export const ChessStudy = ({ {viewComments && (
dispatch({ type: 'SYNC_COMMENT', comment: comment }) } diff --git a/src/components/react/PgnViewer/Controls/index.tsx b/src/components/react/PgnViewer/Controls/index.tsx new file mode 100644 index 0000000..0a09a85 --- /dev/null +++ b/src/components/react/PgnViewer/Controls/index.tsx @@ -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 ( +
+
+ + + +
+
+ + +
+
+ ); +}; diff --git a/src/components/react/PgnViewer/MoveItems/index.tsx b/src/components/react/PgnViewer/MoveItems/index.tsx new file mode 100644 index 0000000..2fae032 --- /dev/null +++ b/src/components/react/PgnViewer/MoveItems/index.tsx @@ -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(null); + + React.useEffect(() => { + if (ref.current && isCurrentMove) { + ref.current?.scrollIntoView({ + behavior: 'smooth', + block: 'nearest', + inline: 'end', + }); + } + }, [isCurrentMove]); + + return ( +

{ + e.stopPropagation(); + onMoveItemClick(); + }} + > + {san} +

+ ); +}; + +export const VariantMoveItem = ({ + isCurrentMove, + san, + onMoveItemClick, + moveIndicator = null, +}: { + isCurrentMove: boolean; + san: string; + onMoveItemClick: () => void; + moveIndicator?: string | null; +}) => { + const ref = React.useRef(null); + + React.useEffect(() => { + if (ref.current && isCurrentMove) { + ref.current?.scrollIntoView({ + behavior: 'smooth', + block: 'nearest', + inline: 'end', + }); + } + }, [isCurrentMove]); + + return ( +
{ + e.stopPropagation(); + onMoveItemClick(); + }} + ref={ref} + > + {moveIndicator} + {san} +
+ ); +}; diff --git a/src/components/react/PgnViewer/index.tsx b/src/components/react/PgnViewer/index.tsx index 054efd6..ecafe63 100644 --- a/src/components/react/PgnViewer/index.tsx +++ b/src/components/react/PgnViewer/index.tsx @@ -1,11 +1,12 @@ -import { ArrowLeft, ArrowRight, Save, Undo2 } 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 = (array: T[], chunkSize: number, offsetByOne: boolean = false) => { +const chunkArray = (array: T[], chunkSize: number, offsetByOne = false) => { return array.reduce((resultArray, item, index) => { - const chunkIndex = Math.floor((index + ((offsetByOne) ? 1 : 0)) / chunkSize); + const chunkIndex = Math.floor((index + (offsetByOne ? 1 : 0)) / chunkSize); if (!resultArray[chunkIndex]) { resultArray[chunkIndex] = []; @@ -17,307 +18,187 @@ const chunkArray = (array: T[], chunkSize: number, offsetByOne: boolean = fa }, [] as T[][]); }; -const MoveItem = ({ - isCurrentMove, - san, - onMoveItemClick, +export const VariantMoveItemContainer = ({ + children, }: { - isCurrentMove: boolean; - san: string; - onMoveItemClick: () => void; + children: React.ReactNode; }) => { - const ref = useRef(null); - - useEffect(() => { - if (ref.current && isCurrentMove) { - ref.current?.scrollIntoView({ - behavior: 'smooth', - block: 'nearest', - inline: 'end', - }); - } - }, [isCurrentMove]); - - return ( -

{ - e.stopPropagation(); - onMoveItemClick(); - }} - > - {san} -

- ); -}; - -const VariantMoveItem = ({ - isCurrentMove, - san, - onMoveItemClick, - moveIndicator = null, -}: { - isCurrentMove: boolean; - san: string; - onMoveItemClick: () => void; - moveIndicator?: string | null; -}) => { - const ref = useRef(null); - - useEffect(() => { - if (ref.current && isCurrentMove) { - ref.current?.scrollIntoView({ - behavior: 'smooth', - block: 'nearest', - inline: 'end', - }); - } - }, [isCurrentMove]); - - return ( -
{ - e.stopPropagation(); - onMoveItemClick(); - }} - ref={ref} - > - {moveIndicator} - {san} -
- ); -}; - -const VariantMoveItemContainer = ({ children }: { children: ReactNode }) => { return
{children}
; }; -const VariantContainer = ({ children }: { children: ReactNode }) => { +export const VariantContainer = ({ + children, +}: { + children: React.ReactNode; +}) => { return
{children}
; }; -const VariantsContainer = ({ children }: { children: ReactNode }) => { +export const VariantsContainer = ({ + children, +}: { + children: React.ReactNode; +}) => { return
{children}
; }; -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, firstPlayer, initialMoveNumber, - onUndoButtonClick, - onBackButtonClick, - onForwardButtonClick, onMoveItemClick, - onSaveButtonClick, - onCopyButtonClick, - }: { - history: ChessStudyMove[]; - currentMoveId: string; - firstPlayer: string; - initialMoveNumber: number; - onUndoButtonClick: () => void; - onBackButtonClick: () => void; - onForwardButtonClick: () => void; - onMoveItemClick: (moveId: string) => void; - onSaveButtonClick: () => void; - onCopyButtonClick: () => void; - }) => { - const movePairs = useMemo(() => chunkArray(history, 2, firstPlayer === 'b'), [history]); + ...controlActions + } = props; - return ( -
-
-
- {movePairs.map((pair, currentMoveIndex) => { - const [wMove, bMove] = pair; + const movePairs = useMemo( + () => chunkArray(history, 2, firstPlayer === 'b'), + [firstPlayer, history] + ); - return ( - -

- {currentMoveIndex + initialMoveNumber} -

- {(firstPlayer === 'b' && bMove === undefined && currentMoveIndex === 0) && ( - {}} - /> - )} + return ( +
+
+
+ {movePairs.map((pair, currentMoveIndex) => { + const [wMove, bMove] = pair; + + return ( + +

+ {currentMoveIndex + initialMoveNumber} +

+ {firstPlayer === 'b' && !bMove && currentMoveIndex === 0 && ( onMoveItemClick(wMove.moveId)} + san={'...'} + isCurrentMove={false} + onMoveItemClick={() => {}} /> - {bMove && ( - onMoveItemClick(bMove.moveId)} - /> - )} - {!!wMove.variants.concat(bMove?.variants || []).length && ( - - {!!wMove.variants.length && ( - - {wMove.variants.map((variant) => { - return ( - - {chunkArray(variant.moves, 2).map( - (pair, wMoveVarianti) => { - const [bMove, wMove] = pair; + )} + onMoveItemClick(wMove.moveId)} + /> + {bMove && ( + onMoveItemClick(bMove.moveId)} + /> + )} + {!!wMove.variants.concat(bMove?.variants || []).length && ( + + {!!wMove.variants.length && ( + + {wMove.variants.map((variant) => { + return ( + + {chunkArray(variant.moves, 2).map((pair, wMoveVarianti) => { + const [bMove, wMove] = pair; - return ( - - - onMoveItemClick(bMove.moveId) - } - moveIndicator={ - (wMoveVarianti === 0 && - (firstPlayer === 'w' - || currentMoveIndex > 0) && - `${ - currentMoveIndex + - initialMoveNumber + - wMoveVarianti - }... `) || - (firstPlayer === 'b' && - currentMoveIndex === 0 && - `${ - currentMoveIndex + - initialMoveNumber + - wMoveVarianti - }. `) || - null - } - /> - {wMove && ( - - onMoveItemClick(wMove.moveId) - } - moveIndicator={ - ((firstPlayer === 'w' || - currentMoveIndex > 0) && - `${ - currentMoveIndex + - initialMoveNumber + 1 + - wMoveVarianti - }. `) || - null - } - /> - )} - - ); - } - )} - - ); - })} - - )} - {!!bMove?.variants.length && ( - - {bMove.variants.map((variant) => { - return ( - - {chunkArray(variant.moves, 2).map( - (pair, bMoveVarianti) => { - const [wMove, bMove] = pair; - return ( - - - onMoveItemClick(wMove.moveId) - } - moveIndicator={ + return ( + + onMoveItemClick(bMove.moveId)} + moveIndicator={ + (wMoveVarianti === 0 && + (firstPlayer === 'w' || currentMoveIndex > 0) && + `${ + currentMoveIndex + initialMoveNumber + wMoveVarianti + }... `) || + (firstPlayer === 'b' && + currentMoveIndex === 0 && + `${ + currentMoveIndex + initialMoveNumber + wMoveVarianti + }. `) || + null + } + /> + {wMove && ( + onMoveItemClick(wMove.moveId)} + moveIndicator={ + ((firstPlayer === 'w' || currentMoveIndex > 0) && `${ - currentMoveIndex - + initialMoveNumber + 1 - + bMoveVarianti - }. ` - } - /> - {bMove && ( - - onMoveItemClick(bMove.moveId) - } - /> - )} - - ); - } - )} - - ); - })} - - )} - - )} -
- ); - })} -
-
-
- - - -
-
- - + currentMoveIndex + initialMoveNumber + 1 + wMoveVarianti + }. `) || + null + } + /> + )} + + ); + })} + + ); + })} + + )} + {!!bMove?.variants.length && ( + + {bMove.variants.map((variant) => { + return ( + + {chunkArray(variant.moves, 2).map((pair, bMoveVarianti) => { + const [wMove, bMove] = pair; + return ( + + onMoveItemClick(wMove.moveId)} + moveIndicator={`${ + currentMoveIndex + initialMoveNumber + 1 + bMoveVarianti + }. `} + /> + {bMove && ( + onMoveItemClick(bMove.moveId)} + /> + )} + + ); + })} + + ); + })} + + )} + + )} + + ); + })}
- ); - } -); + +
+ ); +}); PgnViewer.displayName = 'PgnViewer'; diff --git a/src/lib/storage/index.ts b/src/lib/storage/index.ts index 1abe140..5bd7651 100644 --- a/src/lib/storage/index.ts +++ b/src/lib/storage/index.ts @@ -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 { + 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); } } diff --git a/src/lib/ui-state/index.ts b/src/lib/ui-state/index.ts index 0a9667a..5cb622e 100644 --- a/src/lib/ui-state/index.ts +++ b/src/lib/ui-state/index.ts @@ -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,74 @@ 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]; + if (variant) { + const variantMoves = + moves[variant.parentMoveIndex].variants[variant.variantIndex].moves; + + if (typeof variantMoves[moveIndex + offset] !== 'undefined') { + moveToDisplay = variantMoves[moveIndex + offset]; + } + } else { + if (typeof moves[moveIndex + offset] !== 'undefined') { + moveToDisplay = moves[moveIndex + offset]; + } } - } else { - if (typeof moves[moveIndex + offset] !== 'undefined') { - moveToDisplay = moves[moveIndex + offset]; + + if (!moveToDisplay) { + console.log(`No move to display found`); + return draft; } + + 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), + }); + + draft.currentMove = moveToDisplay; + + setChessLogic(chess); } - if (!moveToDisplay) return draft; - - 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), - }); - - draft.currentMove = moveToDisplay; - - setChessLogic(chess); - return draft; }; export const getCurrentMove = ( draft: Draft -): Draft | Draft => { +): Draft | Draft | 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; }; diff --git a/src/main.css b/src/main.css index 7d186f9..690f112 100644 --- a/src/main.css +++ b/src/main.css @@ -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: 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; } diff --git a/src/main.tsx b/src/main.tsx index 3f24f38..0a295ca 100644 --- a/src/main.tsx +++ b/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)); @@ -51,22 +62,17 @@ export default class ChessStudyPlugin extends Plugin { editorCallback: (editor: Editor) => { const cursorPosition = editor.getCursor(); - const onSubmit = async (pgn_or_fen: string) => { + const onSubmit = async (chessString: ChessString | undefined) => { try { - let pgn = '', fen = ''; - if (pgn_or_fen) { - if (pgn_or_fen.includes('/')) { - fen = pgn_or_fen.trim(); - } else { - pgn = pgn_or_fen.trim(); - } - } + const chessStringTrimmed = chessString?.trim() ?? ''; - const chess = (fen) ? new Chess(fen) : new Chess(); + const isFen = chessStringTrimmed.includes('/'); - if (pgn) { + const chess = isFen ? new Chess(chessStringTrimmed) : new Chess(); + + if (!isFen) { //Try to parse the PGN - chess.loadPgn(pgn, { + chess.loadPgn(chessStringTrimmed, { strict: false, }); } @@ -83,18 +89,15 @@ export default class ChessStudyPlugin extends Plugin { shapes: [], comment: null, })), + rootFEN: isFen ? chessStringTrimmed : ROOT_FEN, }; this.dataAdapter.createStorageFolderIfNotExists(); const id = await this.dataAdapter.saveFile(chessStudyFileData); - const blockStr = (fen) - ? `\`\`\`chessStudy\nchessStudyId: ${id}\nfen: ${fen}\n\`\`\`` - : `\`\`\`chessStudy\nchessStudyId: ${id}\n\`\`\``; - editor.replaceRange( - blockStr, + `\`\`\`chessStudy\nchessStudyId: ${id}\n\`\`\``, cursorPosition ); } catch (e) { @@ -103,7 +106,7 @@ export default class ChessStudyPlugin extends Plugin { } }; - new PgnModal(this.app, onSubmit).open(); + new ChessStringModal(this.app, onSubmit).open(); }, }); @@ -123,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( diff --git a/styles.css b/styles.css index 0656493..1a1f6f3 100644 --- a/styles.css +++ b/styles.css @@ -326,7 +326,7 @@ cg-board square.current-premove { height: 100%; } .chess-study .move-item-section { - height: 415px; + height: 380px; } .chess-study .button-section { display: flex; @@ -334,26 +334,25 @@ cg-board square.current-premove { align-items: center; width: 100%; gap: 4px; - height: 35px; + height: 32px; } .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; } @@ -424,3 +423,4 @@ cg-board square.current-premove { width: 100%; height: 100%; } +/*# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiYXNzZXRzL2JvYXJkL2dyZWVuLmNzcyIsICJub2RlX21vZHVsZXMvY2hlc3Nncm91bmQvYXNzZXRzL2NoZXNzZ3JvdW5kLmJhc2UuY3NzIiwgIm5vZGVfbW9kdWxlcy9jaGVzc2dyb3VuZC9hc3NldHMvY2hlc3Nncm91bmQuYnJvd24uY3NzIiwgIm5vZGVfbW9kdWxlcy9jaGVzc2dyb3VuZC9hc3NldHMvY2hlc3Nncm91bmQuY2J1cm5ldHQuY3NzIiwgInNyYy9tYWluLmNzcyJdLAogICJzb3VyY2VzQ29udGVudCI6IFsiLmdyZWVuLWJvYXJkIGNnLWJvYXJkIHtcblx0YmFja2dyb3VuZC1pbWFnZTogdXJsKCdkYXRhOmltYWdlL3N2Zyt4bWw7YmFzZTY0LFBEOTRiV3dnZG1WeWMybHZiajBpTVM0d0lpQmxibU52WkdsdVp6MGlWVlJHTFRnaUlITjBZVzVrWVd4dmJtVTlJbTV2SWo4K0NqeHpkbWNnZUcxc2JuTTlJbWgwZEhBNkx5OTNkM2N1ZHpNdWIzSm5Mekl3TURBdmMzWm5JaUI0Yld4dWN6cDRQU0pvZEhSd09pOHZkM2QzTG5jekxtOXlaeTh4T1RrNUwzaHNhVzVySWdvZ0lDQWdJSFpwWlhkQ2IzZzlJakFnTUNBNElEZ2lJSE5vWVhCbExYSmxibVJsY21sdVp6MGlZM0pwYzNCRlpHZGxjeUkrQ2p4bklHbGtQU0poSWo0S0lDQThaeUJwWkQwaVlpSStDaUFnSUNBOFp5QnBaRDBpWXlJK0NpQWdJQ0FnSUR4bklHbGtQU0prSWo0S0lDQWdJQ0FnSUNBOGNtVmpkQ0IzYVdSMGFEMGlNU0lnYUdWcFoyaDBQU0l4SWlCbWFXeHNQU0lqWm1abVptUmtJaUJwWkQwaVpTSXZQZ29nSUNBZ0lDQWdJRHgxYzJVZ2VEMGlNU0lnZVQwaU1TSWdhSEpsWmowaUkyVWlJSGc2YUhKbFpqMGlJMlVpTHo0S0lDQWdJQ0FnSUNBOGNtVmpkQ0I1UFNJeElpQjNhV1IwYUQwaU1TSWdhR1ZwWjJoMFBTSXhJaUJtYVd4c1BTSWpPRFpoTmpZMklpQnBaRDBpWmlJdlBnb2dJQ0FnSUNBZ0lEeDFjMlVnZUQwaU1TSWdlVDBpTFRFaUlHaHlaV1k5SWlObUlpQjRPbWh5WldZOUlpTm1JaTgrQ2lBZ0lDQWdJRHd2Wno0S0lDQWdJQ0FnUEhWelpTQjRQU0l5SWlCb2NtVm1QU0lqWkNJZ2VEcG9jbVZtUFNJalpDSXZQZ29nSUNBZ1BDOW5QZ29nSUNBZ1BIVnpaU0I0UFNJMElpQm9jbVZtUFNJall5SWdlRHBvY21WbVBTSWpZeUl2UGdvZ0lEd3ZaejRLSUNBOGRYTmxJSGs5SWpJaUlHaHlaV1k5SWlOaUlpQjRPbWh5WldZOUlpTmlJaTgrQ2p3dlp6NEtQSFZ6WlNCNVBTSTBJaUJvY21WbVBTSWpZU0lnZURwb2NtVm1QU0lqWVNJdlBnbzhMM04yWno0SycpO1xufVxuIiwgIi5jZy13cmFwIHtcbiAgYm94LXNpemluZzogY29udGVudC1ib3g7XG4gIHBvc2l0aW9uOiByZWxhdGl2ZTtcbiAgZGlzcGxheTogYmxvY2s7XG59XG5cbmNnLWNvbnRhaW5lciB7XG4gIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgd2lkdGg6IDEwMCU7XG4gIGhlaWdodDogMTAwJTtcbiAgZGlzcGxheTogYmxvY2s7XG4gIHRvcDogMDtcbn1cblxuY2ctYm9hcmQge1xuICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gIHRvcDogMDtcbiAgbGVmdDogMDtcbiAgd2lkdGg6IDEwMCU7XG4gIGhlaWdodDogMTAwJTtcbiAgLXdlYmtpdC11c2VyLXNlbGVjdDogbm9uZTtcbiAgLW1vei11c2VyLXNlbGVjdDogbm9uZTtcbiAgLW1zLXVzZXItc2VsZWN0OiBub25lO1xuICB1c2VyLXNlbGVjdDogbm9uZTtcbiAgbGluZS1oZWlnaHQ6IDA7XG4gIGJhY2tncm91bmQtc2l6ZTogY292ZXI7XG59XG5cbi5jZy13cmFwLm1hbmlwdWxhYmxlIGNnLWJvYXJkIHtcbiAgY3Vyc29yOiBwb2ludGVyO1xufVxuXG5jZy1ib2FyZCBzcXVhcmUge1xuICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gIHRvcDogMDtcbiAgbGVmdDogMDtcbiAgd2lkdGg6IDEyLjUlO1xuICBoZWlnaHQ6IDEyLjUlO1xuICBwb2ludGVyLWV2ZW50czogbm9uZTtcbn1cblxuY2ctYm9hcmQgc3F1YXJlLm1vdmUtZGVzdCB7XG4gIHBvaW50ZXItZXZlbnRzOiBhdXRvO1xufVxuXG5jZy1ib2FyZCBzcXVhcmUubGFzdC1tb3ZlIHtcbiAgd2lsbC1jaGFuZ2U6IHRyYW5zZm9ybTtcbn1cblxuLmNnLXdyYXAgcGllY2Uge1xuICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gIHRvcDogMDtcbiAgbGVmdDogMDtcbiAgd2lkdGg6IDEyLjUlO1xuICBoZWlnaHQ6IDEyLjUlO1xuICBiYWNrZ3JvdW5kLXNpemU6IGNvdmVyO1xuICB6LWluZGV4OiAyO1xuICB3aWxsLWNoYW5nZTogdHJhbnNmb3JtO1xuICBwb2ludGVyLWV2ZW50czogbm9uZTtcbn1cblxuY2ctYm9hcmQgcGllY2UuZHJhZ2dpbmcge1xuICBjdXJzb3I6IG1vdmU7XG4gIC8qICFpbXBvcnRhbnQgdG8gb3ZlcnJpZGUgei1pbmRleCBmcm9tIDNEIHBpZWNlIGlubGluZSBzdHlsZSAqL1xuICB6LWluZGV4OiAxMSAhaW1wb3J0YW50O1xufVxuXG5waWVjZS5hbmltIHtcbiAgei1pbmRleDogODtcbn1cblxucGllY2UuZmFkaW5nIHtcbiAgei1pbmRleDogMTtcbiAgb3BhY2l0eTogMC41O1xufVxuXG4uY2ctd3JhcCBwaWVjZS5naG9zdCB7XG4gIG9wYWNpdHk6IDAuMztcbn1cblxuLmNnLXdyYXAgcGllY2Ugc3ZnIHtcbiAgb3ZlcmZsb3c6IGhpZGRlbjtcbiAgcG9zaXRpb246IHJlbGF0aXZlO1xuICB0b3A6IDBweDtcbiAgbGVmdDogMHB4O1xuICB3aWR0aDogMTAwJTtcbiAgaGVpZ2h0OiAxMDAlO1xuICBwb2ludGVyLWV2ZW50czogbm9uZTtcbiAgei1pbmRleDogMjtcbiAgb3BhY2l0eTogMC42O1xufVxuXG4uY2ctd3JhcCBjZy1hdXRvLXBpZWNlcyxcbi5jZy13cmFwIC5jZy1zaGFwZXMsXG4uY2ctd3JhcCAuY2ctY3VzdG9tLXN2Z3Mge1xuICBvdmVyZmxvdzogdmlzaWJsZTtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICB0b3A6IDBweDtcbiAgbGVmdDogMHB4O1xuICB3aWR0aDogMTAwJTtcbiAgaGVpZ2h0OiAxMDAlO1xuICBwb2ludGVyLWV2ZW50czogbm9uZTtcbn1cblxuLmNnLXdyYXAgY2ctYXV0by1waWVjZXMge1xuICB6LWluZGV4OiAyO1xufVxuXG4uY2ctd3JhcCBjZy1hdXRvLXBpZWNlcyBwaWVjZSB7XG4gIG9wYWNpdHk6IDAuMztcbn1cblxuLmNnLXdyYXAgLmNnLXNoYXBlcyB7XG4gIG92ZXJmbG93OiBoaWRkZW47XG4gIG9wYWNpdHk6IDAuNjtcbiAgei1pbmRleDogMjtcbn1cblxuLmNnLXdyYXAgLmNnLWN1c3RvbS1zdmdzIHtcbiAgLyogb3ZlciBwaWVjZS5hbmltID0gOCwgYnV0IHVuZGVyIHBpZWNlLmRyYWdnaW5nID0gMTEgKi9cbiAgei1pbmRleDogOTtcbn1cblxuLmNnLXdyYXAgLmNnLWN1c3RvbS1zdmdzIHN2ZyB7XG4gIG92ZXJmbG93OiB2aXNpYmxlO1xufVxuXG4uY2ctd3JhcCBjb29yZHMge1xuICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gIGRpc3BsYXk6IGZsZXg7XG4gIHBvaW50ZXItZXZlbnRzOiBub25lO1xuICBvcGFjaXR5OiAwLjg7XG4gIGZvbnQtZmFtaWx5OiBzYW5zLXNlcmlmO1xuICBmb250LXNpemU6IDlweDtcbn1cblxuLmNnLXdyYXAgY29vcmRzLnJhbmtzIHtcbiAgbGVmdDogNHB4O1xuICB0b3A6IC0yMHB4O1xuICBmbGV4LWZsb3c6IGNvbHVtbi1yZXZlcnNlO1xuICBoZWlnaHQ6IDEwMCU7XG4gIHdpZHRoOiAxMnB4O1xufVxuXG4uY2ctd3JhcCBjb29yZHMucmFua3MuYmxhY2sge1xuICBmbGV4LWZsb3c6IGNvbHVtbjtcbn1cblxuLmNnLXdyYXAgY29vcmRzLnJhbmtzLmxlZnQge1xuICBsZWZ0OiAtMTVweDtcbiAgYWxpZ24taXRlbXM6IGZsZXgtZW5kO1xufVxuXG4uY2ctd3JhcCBjb29yZHMuZmlsZXMge1xuICBib3R0b206IC00cHg7XG4gIGxlZnQ6IDI0cHg7XG4gIGZsZXgtZmxvdzogcm93O1xuICB3aWR0aDogMTAwJTtcbiAgaGVpZ2h0OiAxNnB4O1xuICB0ZXh0LXRyYW5zZm9ybTogdXBwZXJjYXNlO1xuICB0ZXh0LWFsaWduOiBjZW50ZXI7XG59XG5cbi5jZy13cmFwIGNvb3Jkcy5maWxlcy5ibGFjayB7XG4gIGZsZXgtZmxvdzogcm93LXJldmVyc2U7XG59XG5cbi5jZy13cmFwIGNvb3JkcyBjb29yZCB7XG4gIGZsZXg6IDEgMSBhdXRvO1xufVxuXG4uY2ctd3JhcCBjb29yZHMucmFua3MgY29vcmQge1xuICB0cmFuc2Zvcm06IHRyYW5zbGF0ZVkoMzklKTtcbn1cbiIsICIvKiogQ29sb3JlZCBib2FyZCBzcXVhcmVzIGFzIGFuIGVtYmVkZGVkIFNWRyAqL1xuY2ctYm9hcmQge1xuICBiYWNrZ3JvdW5kLWNvbG9yOiAjZjBkOWI1O1xuICBiYWNrZ3JvdW5kLWltYWdlOiB1cmwoJ2RhdGE6aW1hZ2Uvc3ZnK3htbDtiYXNlNjQsUEQ5NGJXd2dkbVZ5YzJsdmJqMGlNUzR3SWlCbGJtTnZaR2x1WnowaVZWUkdMVGdpSUhOMFlXNWtZV3h2Ym1VOUltNXZJajgrQ2p4emRtY2dlRzFzYm5NOUltaDBkSEE2THk5M2QzY3Vkek11YjNKbkx6SXdNREF2YzNabklpQjRiV3h1Y3pwNFBTSm9kSFJ3T2k4dmQzZDNMbmN6TG05eVp5OHhPVGs1TDNoc2FXNXJJZ29nSUNBZ0lIWnBaWGRDYjNnOUlqQWdNQ0E0SURnaUlITm9ZWEJsTFhKbGJtUmxjbWx1WnowaVkzSnBjM0JGWkdkbGN5SStDanhuSUdsa1BTSmhJajRLSUNBOFp5QnBaRDBpWWlJK0NpQWdJQ0E4WnlCcFpEMGlZeUkrQ2lBZ0lDQWdJRHhuSUdsa1BTSmtJajRLSUNBZ0lDQWdJQ0E4Y21WamRDQjNhV1IwYUQwaU1TSWdhR1ZwWjJoMFBTSXhJaUJwWkQwaVpTSWdiM0JoWTJsMGVUMGlNQ0l2UGdvZ0lDQWdJQ0FnSUR4MWMyVWdlRDBpTVNJZ2VUMGlNU0lnYUhKbFpqMGlJMlVpSUhnNmFISmxaajBpSTJVaUx6NEtJQ0FnSUNBZ0lDQThjbVZqZENCNVBTSXhJaUIzYVdSMGFEMGlNU0lnYUdWcFoyaDBQU0l4SWlCcFpEMGlaaUlnYjNCaFkybDBlVDBpTUM0eUlpOCtDaUFnSUNBZ0lDQWdQSFZ6WlNCNFBTSXhJaUI1UFNJdE1TSWdhSEpsWmowaUkyWWlJSGc2YUhKbFpqMGlJMllpTHo0S0lDQWdJQ0FnUEM5blBnb2dJQ0FnSUNBOGRYTmxJSGc5SWpJaUlHaHlaV1k5SWlOa0lpQjRPbWh5WldZOUlpTmtJaTgrQ2lBZ0lDQThMMmMrQ2lBZ0lDQThkWE5sSUhnOUlqUWlJR2h5WldZOUlpTmpJaUI0T21oeVpXWTlJaU5qSWk4K0NpQWdQQzluUGdvZ0lEeDFjMlVnZVQwaU1pSWdhSEpsWmowaUkySWlJSGc2YUhKbFpqMGlJMklpTHo0S1BDOW5QZ284ZFhObElIazlJalFpSUdoeVpXWTlJaU5oSWlCNE9taHlaV1k5SWlOaElpOCtDand2YzNablBnPT0nKTtcbn1cblxuLyoqIEludGVyYWN0aXZlIGJvYXJkIHNxdWFyZSBjb2xvcnMgKi9cbmNnLWJvYXJkIHNxdWFyZS5tb3ZlLWRlc3Qge1xuICBiYWNrZ3JvdW5kOiByYWRpYWwtZ3JhZGllbnQocmdiYSgyMCwgODUsIDMwLCAwLjUpIDIyJSwgIzIwODUzMCAwLCByZ2JhKDAsIDAsIDAsIDAuMykgMCwgcmdiYSgwLCAwLCAwLCAwKSAwKTtcbn1cbmNnLWJvYXJkIHNxdWFyZS5wcmVtb3ZlLWRlc3Qge1xuICBiYWNrZ3JvdW5kOiByYWRpYWwtZ3JhZGllbnQocmdiYSgyMCwgMzAsIDg1LCAwLjUpIDIyJSwgIzIwMzA4NSAwLCByZ2JhKDAsIDAsIDAsIDAuMykgMCwgcmdiYSgwLCAwLCAwLCAwKSAwKTtcbn1cbmNnLWJvYXJkIHNxdWFyZS5vYy5tb3ZlLWRlc3Qge1xuICBiYWNrZ3JvdW5kOiByYWRpYWwtZ3JhZGllbnQodHJhbnNwYXJlbnQgMCUsIHRyYW5zcGFyZW50IDgwJSwgcmdiYSgyMCwgODUsIDAsIDAuMykgODAlKTtcbn1cbmNnLWJvYXJkIHNxdWFyZS5vYy5wcmVtb3ZlLWRlc3Qge1xuICBiYWNrZ3JvdW5kOiByYWRpYWwtZ3JhZGllbnQodHJhbnNwYXJlbnQgMCUsIHRyYW5zcGFyZW50IDgwJSwgcmdiYSgyMCwgMzAsIDg1LCAwLjIpIDgwJSk7XG59XG5jZy1ib2FyZCBzcXVhcmUubW92ZS1kZXN0OmhvdmVyIHtcbiAgYmFja2dyb3VuZDogcmdiYSgyMCwgODUsIDMwLCAwLjMpO1xufVxuY2ctYm9hcmQgc3F1YXJlLnByZW1vdmUtZGVzdDpob3ZlciB7XG4gIGJhY2tncm91bmQ6IHJnYmEoMjAsIDMwLCA4NSwgMC4yKTtcbn1cbmNnLWJvYXJkIHNxdWFyZS5sYXN0LW1vdmUge1xuICBiYWNrZ3JvdW5kLWNvbG9yOiByZ2JhKDE1NSwgMTk5LCAwLCAwLjQxKTtcbn1cbmNnLWJvYXJkIHNxdWFyZS5zZWxlY3RlZCB7XG4gIGJhY2tncm91bmQtY29sb3I6IHJnYmEoMjAsIDg1LCAzMCwgMC41KTtcbn1cbmNnLWJvYXJkIHNxdWFyZS5jaGVjayB7XG4gIGJhY2tncm91bmQ6IHJhZGlhbC1ncmFkaWVudChcbiAgICBlbGxpcHNlIGF0IGNlbnRlcixcbiAgICByZ2JhKDI1NSwgMCwgMCwgMSkgMCUsXG4gICAgcmdiYSgyMzEsIDAsIDAsIDEpIDI1JSxcbiAgICByZ2JhKDE2OSwgMCwgMCwgMCkgODklLFxuICAgIHJnYmEoMTU4LCAwLCAwLCAwKSAxMDAlXG4gICk7XG59XG5jZy1ib2FyZCBzcXVhcmUuY3VycmVudC1wcmVtb3ZlIHtcbiAgYmFja2dyb3VuZC1jb2xvcjogcmdiYSgyMCwgMzAsIDg1LCAwLjUpO1xufVxuXG4vKiogQWx0ZXJuYXRpbmcgY29sb3JzIGluIHJhbmsvZmlsZSBsYWJlbHMgKi9cbi5jZy13cmFwLm9yaWVudGF0aW9uLXdoaXRlIGNvb3Jkcy5yYW5rcyBjb29yZDpudGgtY2hpbGQoMm4pLFxuLmNnLXdyYXAub3JpZW50YXRpb24td2hpdGUgY29vcmRzLmZpbGVzIGNvb3JkOm50aC1jaGlsZCgybiksXG4uY2ctd3JhcC5vcmllbnRhdGlvbi1ibGFjayBjb29yZHMucmFua3MgY29vcmQ6bnRoLWNoaWxkKDJuICsgMSksXG4uY2ctd3JhcC5vcmllbnRhdGlvbi1ibGFjayBjb29yZHMuZmlsZXMgY29vcmQ6bnRoLWNoaWxkKDJuICsgMSkge1xuICBjb2xvcjogcmdiYSg3MiwgNzIsIDcyLCAwLjgpO1xufVxuXG4uY2ctd3JhcC5vcmllbnRhdGlvbi1ibGFjayBjb29yZHMucmFua3MgY29vcmQ6bnRoLWNoaWxkKDJuKSxcbi5jZy13cmFwLm9yaWVudGF0aW9uLWJsYWNrIGNvb3Jkcy5maWxlcyBjb29yZDpudGgtY2hpbGQoMm4pLFxuLmNnLXdyYXAub3JpZW50YXRpb24td2hpdGUgY29vcmRzLnJhbmtzIGNvb3JkOm50aC1jaGlsZCgybiArIDEpLFxuLmNnLXdyYXAub3JpZW50YXRpb24td2hpdGUgY29vcmRzLmZpbGVzIGNvb3JkOm50aC1jaGlsZCgybiArIDEpIHtcbiAgY29sb3I6IHJnYmEoMjU1LCAyNTUsIDI1NSwgMC44KTtcbn1cbiIsICIvKiogRW1iZWRkZWQgU1ZHcyBmb3IgYWxsIGNoZXNzIHBpZWNlcyAqL1xuLmNnLXdyYXAgcGllY2UucGF3bi53aGl0ZSB7XG4gIGJhY2tncm91bmQtaW1hZ2U6IHVybCgnZGF0YTppbWFnZS9zdmcreG1sO2Jhc2U2NCxQSE4yWnlCNGJXeHVjejBpYUhSMGNEb3ZMM2QzZHk1M015NXZjbWN2TWpBd01DOXpkbWNpSUhkcFpIUm9QU0kwTlNJZ2FHVnBaMmgwUFNJME5TSStQSEJoZEdnZ1pEMGlUVEl5TGpVZ09XTXRNaTR5TVNBd0xUUWdNUzQzT1MwMElEUWdNQ0F1T0RrdU1qa2dNUzQzTVM0M09DQXlMak00UXpFM0xqTXpJREUyTGpVZ01UWWdNVGd1TlRrZ01UWWdNakZqTUNBeUxqQXpMamswSURNdU9EUWdNaTQwTVNBMUxqQXpMVE1nTVM0d05pMDNMalF4SURVdU5UVXROeTQwTVNBeE15NDBOMmd5TTJNd0xUY3VPVEl0TkM0ME1TMHhNaTQwTVMwM0xqUXhMVEV6TGpRM0lERXVORGN0TVM0eE9TQXlMalF4TFRNZ01pNDBNUzAxTGpBeklEQXRNaTQwTVMweExqTXpMVFF1TlMwekxqSTRMVFV1TmpJdU5Ea3RMalkzTGpjNExURXVORGt1TnpndE1pNHpPQ0F3TFRJdU1qRXRNUzQzT1MwMExUUXROSG9pSUdacGJHdzlJaU5tWm1ZaUlITjBjbTlyWlQwaUl6QXdNQ0lnYzNSeWIydGxMWGRwWkhSb1BTSXhMalVpSUhOMGNtOXJaUzFzYVc1bFkyRndQU0p5YjNWdVpDSXZQand2YzNablBnPT0nKTtcbn1cbi5jZy13cmFwIHBpZWNlLmJpc2hvcC53aGl0ZSB7XG4gIGJhY2tncm91bmQtaW1hZ2U6IHVybCgnZGF0YTppbWFnZS9zdmcreG1sO2Jhc2U2NCxQSE4yWnlCNGJXeHVjejBpYUhSMGNEb3ZMM2QzZHk1M015NXZjbWN2TWpBd01DOXpkbWNpSUhkcFpIUm9QU0kwTlNJZ2FHVnBaMmgwUFNJME5TSStQR2NnWm1sc2JEMGlibTl1WlNJZ1ptbHNiQzF5ZFd4bFBTSmxkbVZ1YjJSa0lpQnpkSEp2YTJVOUlpTXdNREFpSUhOMGNtOXJaUzEzYVdSMGFEMGlNUzQxSWlCemRISnZhMlV0YkdsdVpXTmhjRDBpY205MWJtUWlJSE4wY205clpTMXNhVzVsYW05cGJqMGljbTkxYm1RaVBqeG5JR1pwYkd3OUlpTm1abVlpSUhOMGNtOXJaUzFzYVc1bFkyRndQU0ppZFhSMElqNDhjR0YwYUNCa1BTSk5PU0F6Tm1NekxqTTVMUzQ1TnlBeE1DNHhNUzQwTXlBeE15NDFMVElnTXk0ek9TQXlMalF6SURFd0xqRXhJREV1TURNZ01UTXVOU0F5SURBZ01DQXhMalkxTGpVMElETWdNaTB1TmpndU9UY3RNUzQyTlM0NU9TMHpJQzQxTFRNdU16a3RMamszTFRFd0xqRXhMalEyTFRFekxqVXRNUzB6TGpNNUlERXVORFl0TVRBdU1URXVNRE10TVRNdU5TQXhMVEV1TXpVMExqUTVMVEl1TXpJekxqUTNMVE10TGpVZ01TNHpOVFF0TVM0NU5DQXpMVElnTXkweWVpSXZQanh3WVhSb0lHUTlJazB4TlNBek1tTXlMalVnTWk0MUlERXlMalVnTWk0MUlERTFJREFnTGpVdE1TNDFJREF0TWlBd0xUSWdNQzB5TGpVdE1pNDFMVFF0TWk0MUxUUWdOUzQxTFRFdU5TQTJMVEV4TGpVdE5TMHhOUzQxTFRFeElEUXRNVEF1TlNBeE5DMDFJREUxTGpVZ01DQXdMVEl1TlNBeExqVXRNaTQxSURRZ01DQXdMUzQxTGpVZ01DQXllaUl2UGp4d1lYUm9JR1E5SWsweU5TQTRZVEl1TlNBeUxqVWdNQ0F4SURFdE5TQXdJREl1TlNBeUxqVWdNQ0F4SURFZ05TQXdlaUl2UGp3dlp6NDhjR0YwYUNCa1BTSk5NVGN1TlNBeU5tZ3hNRTB4TlNBek1HZ3hOVzB0Tnk0MUxURTBMalYyTlUweU1DQXhPR2cxSWlCemRISnZhMlV0YkdsdVpXcHZhVzQ5SW0xcGRHVnlJaTgrUEM5blBqd3ZjM1puUGc9PScpO1xufVxuLmNnLXdyYXAgcGllY2Uua25pZ2h0LndoaXRlIHtcbiAgYmFja2dyb3VuZC1pbWFnZTogdXJsKCdkYXRhOmltYWdlL3N2Zyt4bWw7YmFzZTY0LFBITjJaeUI0Yld4dWN6MGlhSFIwY0RvdkwzZDNkeTUzTXk1dmNtY3ZNakF3TUM5emRtY2lJSGRwWkhSb1BTSTBOU0lnYUdWcFoyaDBQU0kwTlNJK1BHY2dabWxzYkQwaWJtOXVaU0lnWm1sc2JDMXlkV3hsUFNKbGRtVnViMlJrSWlCemRISnZhMlU5SWlNd01EQWlJSE4wY205clpTMTNhV1IwYUQwaU1TNDFJaUJ6ZEhKdmEyVXRiR2x1WldOaGNEMGljbTkxYm1RaUlITjBjbTlyWlMxc2FXNWxhbTlwYmowaWNtOTFibVFpUGp4d1lYUm9JR1E5SWsweU1pQXhNR014TUM0MUlERWdNVFl1TlNBNElERTJJREk1U0RFMVl6QXRPU0F4TUMwMkxqVWdPQzB5TVNJZ1ptbHNiRDBpSTJabVppSXZQanh3WVhSb0lHUTlJazB5TkNBeE9HTXVNemdnTWk0NU1TMDFMalUxSURjdU16Y3RPQ0E1TFRNZ01pMHlMamd5SURRdU16UXROU0EwTFRFdU1EUXlMUzQ1TkNBeExqUXhMVE11TURRZ01DMHpMVEVnTUNBdU1Ua2dNUzR5TXkweElESXRNU0F3TFRRdU1EQXpJREV0TkMwMElEQXRNaUEyTFRFeUlEWXRNVEp6TVM0NE9TMHhMamtnTWkwekxqVmpMUzQzTXkwdU9UazBMUzQxTFRJdExqVXRNeUF4TFRFZ015QXlMalVnTXlBeUxqVm9Nbk11TnpndE1TNDVPVElnTWk0MUxUTmpNU0F3SURFZ015QXhJRE1pSUdacGJHdzlJaU5tWm1ZaUx6NDhjR0YwYUNCa1BTSk5PUzQxSURJMUxqVmhMalV1TlNBd0lERWdNUzB4SURBZ0xqVXVOU0F3SURFZ01TQXhJREI2YlRVdU5ETXpMVGt1TnpWaExqVWdNUzQxSURNd0lERWdNUzB1T0RZMkxTNDFMalVnTVM0MUlETXdJREVnTVNBdU9EWTJMalY2SWlCbWFXeHNQU0lqTURBd0lpOCtQQzluUGp3dmMzWm5QZz09Jyk7XG59XG4uY2ctd3JhcCBwaWVjZS5yb29rLndoaXRlIHtcbiAgYmFja2dyb3VuZC1pbWFnZTogdXJsKCdkYXRhOmltYWdlL3N2Zyt4bWw7YmFzZTY0LFBITjJaeUI0Yld4dWN6MGlhSFIwY0RvdkwzZDNkeTUzTXk1dmNtY3ZNakF3TUM5emRtY2lJSGRwWkhSb1BTSTBOU0lnYUdWcFoyaDBQU0kwTlNJK1BHY2dabWxzYkQwaUkyWm1aaUlnWm1sc2JDMXlkV3hsUFNKbGRtVnViMlJrSWlCemRISnZhMlU5SWlNd01EQWlJSE4wY205clpTMTNhV1IwYUQwaU1TNDFJaUJ6ZEhKdmEyVXRiR2x1WldOaGNEMGljbTkxYm1RaUlITjBjbTlyWlMxc2FXNWxhbTlwYmowaWNtOTFibVFpUGp4d1lYUm9JR1E5SWswNUlETTVhREkzZGkwelNEbDJNM3B0TXkwemRpMDBhREl4ZGpSSU1USjZiUzB4TFRJeVZqbG9OSFl5YURWV09XZzFkakpvTlZZNWFEUjJOU0lnYzNSeWIydGxMV3hwYm1WallYQTlJbUoxZEhRaUx6NDhjR0YwYUNCa1BTSk5NelFnTVRSc0xUTWdNMGd4Tkd3dE15MHpJaTgrUEhCaGRHZ2daRDBpVFRNeElERTNkakV5TGpWSU1UUldNVGNpSUhOMGNtOXJaUzFzYVc1bFkyRndQU0ppZFhSMElpQnpkSEp2YTJVdGJHbHVaV3B2YVc0OUltMXBkR1Z5SWk4K1BIQmhkR2dnWkQwaVRUTXhJREk1TGpWc01TNDFJREl1TldndE1qQnNNUzQxTFRJdU5TSXZQanh3WVhSb0lHUTlJazB4TVNBeE5HZ3lNeUlnWm1sc2JEMGlibTl1WlNJZ2MzUnliMnRsTFd4cGJtVnFiMmx1UFNKdGFYUmxjaUl2UGp3dlp6NDhMM04yWno0PScpO1xufVxuLmNnLXdyYXAgcGllY2UucXVlZW4ud2hpdGUge1xuICBiYWNrZ3JvdW5kLWltYWdlOiB1cmwoJ2RhdGE6aW1hZ2Uvc3ZnK3htbDtiYXNlNjQsUEhOMlp5QjRiV3h1Y3owaWFIUjBjRG92TDNkM2R5NTNNeTV2Y21jdk1qQXdNQzl6ZG1jaUlIZHBaSFJvUFNJME5TSWdhR1ZwWjJoMFBTSTBOU0krUEdjZ1ptbHNiRDBpSTJabVppSWdabWxzYkMxeWRXeGxQU0psZG1WdWIyUmtJaUJ6ZEhKdmEyVTlJaU13TURBaUlITjBjbTlyWlMxM2FXUjBhRDBpTVM0MUlpQnpkSEp2YTJVdGJHbHVaV05oY0QwaWNtOTFibVFpSUhOMGNtOXJaUzFzYVc1bGFtOXBiajBpY205MWJtUWlQanh3WVhSb0lHUTlJazA0SURFeVlUSWdNaUF3SURFZ01TMDBJREFnTWlBeUlEQWdNU0F4SURRZ01IcHRNVFl1TlMwMExqVmhNaUF5SURBZ01TQXhMVFFnTUNBeUlESWdNQ0F4SURFZ05DQXdlazAwTVNBeE1tRXlJRElnTUNBeElERXROQ0F3SURJZ01pQXdJREVnTVNBMElEQjZUVEUySURndU5XRXlJRElnTUNBeElERXROQ0F3SURJZ01pQXdJREVnTVNBMElEQjZUVE16SURsaE1pQXlJREFnTVNBeExUUWdNQ0F5SURJZ01DQXhJREVnTkNBd2VpSXZQanh3WVhSb0lHUTlJazA1SURJMll6Z3VOUzB4TGpVZ01qRXRNUzQxSURJM0lEQnNNaTB4TWkwM0lERXhWakV4YkMwMUxqVWdNVE11TlMwekxURTFMVE1nTVRVdE5TNDFMVEUwVmpJMVREY2dNVFJzTWlBeE1ub2lJSE4wY205clpTMXNhVzVsWTJGd1BTSmlkWFIwSWk4K1BIQmhkR2dnWkQwaVRUa2dNalpqTUNBeUlERXVOU0F5SURJdU5TQTBJREVnTVM0MUlERWdNU0F1TlNBekxqVXRNUzQxSURFdE1TNDFJREl1TlMweExqVWdNaTQxTFRFdU5TQXhMalV1TlNBeUxqVXVOU0F5TGpVZ05pNDFJREVnTVRZdU5TQXhJREl6SURBZ01DQXdJREV1TlMweElEQXRNaTQxSURBZ01DQXVOUzB4TGpVdE1TMHlMalV0TGpVdE1pNDFMUzQxTFRJZ0xqVXRNeTQxSURFdE1pQXlMalV0TWlBeUxqVXROQzA0TGpVdE1TNDFMVEU0TGpVdE1TNDFMVEkzSURCNklpQnpkSEp2YTJVdGJHbHVaV05oY0QwaVluVjBkQ0l2UGp4d1lYUm9JR1E5SWsweE1TNDFJRE13WXpNdU5TMHhJREU0TGpVdE1TQXlNaUF3VFRFeUlETXpMalZqTmkweElERTFMVEVnTWpFZ01DSWdabWxzYkQwaWJtOXVaU0l2UGp3dlp6NDhMM04yWno0PScpO1xufVxuLmNnLXdyYXAgcGllY2Uua2luZy53aGl0ZSB7XG4gIGJhY2tncm91bmQtaW1hZ2U6IHVybCgnZGF0YTppbWFnZS9zdmcreG1sO2Jhc2U2NCxQSE4yWnlCNGJXeHVjejBpYUhSMGNEb3ZMM2QzZHk1M015NXZjbWN2TWpBd01DOXpkbWNpSUhkcFpIUm9QU0kwTlNJZ2FHVnBaMmgwUFNJME5TSStQR2NnWm1sc2JEMGlibTl1WlNJZ1ptbHNiQzF5ZFd4bFBTSmxkbVZ1YjJSa0lpQnpkSEp2YTJVOUlpTXdNREFpSUhOMGNtOXJaUzEzYVdSMGFEMGlNUzQxSWlCemRISnZhMlV0YkdsdVpXTmhjRDBpY205MWJtUWlJSE4wY205clpTMXNhVzVsYW05cGJqMGljbTkxYm1RaVBqeHdZWFJvSUdROUlrMHlNaTQxSURFeExqWXpWalpOTWpBZ09HZzFJaUJ6ZEhKdmEyVXRiR2x1WldwdmFXNDlJbTFwZEdWeUlpOCtQSEJoZEdnZ1pEMGlUVEl5TGpVZ01qVnpOQzQxTFRjdU5TQXpMVEV3TGpWak1DQXdMVEV0TWk0MUxUTXRNaTQxY3kweklESXVOUzB6SURJdU5XTXRNUzQxSURNZ015QXhNQzQxSURNZ01UQXVOU0lnWm1sc2JEMGlJMlptWmlJZ2MzUnliMnRsTFd4cGJtVmpZWEE5SW1KMWRIUWlJSE4wY205clpTMXNhVzVsYW05cGJqMGliV2wwWlhJaUx6NDhjR0YwYUNCa1BTSk5NVEV1TlNBek4yTTFMalVnTXk0MUlERTFMalVnTXk0MUlESXhJREIyTFRkek9TMDBMalVnTmkweE1DNDFZeTAwTFRZdU5TMHhNeTQxTFRNdU5TMHhOaUEwVmpJM2RpMHpMalZqTFRNdU5TMDNMalV0TVRNdE1UQXVOUzB4TmkwMExUTWdOaUExSURFd0lEVWdNVEJXTXpkNklpQm1hV3hzUFNJalptWm1JaTgrUEhCaGRHZ2daRDBpVFRFeExqVWdNekJqTlM0MUxUTWdNVFV1TlMweklESXhJREJ0TFRJeElETXVOV00xTGpVdE15QXhOUzQxTFRNZ01qRWdNRzB0TWpFZ015NDFZelV1TlMweklERTFMalV0TXlBeU1TQXdJaTgrUEM5blBqd3ZjM1puUGc9PScpO1xufVxuLmNnLXdyYXAgcGllY2UucGF3bi5ibGFjayB7XG4gIGJhY2tncm91bmQtaW1hZ2U6IHVybCgnZGF0YTppbWFnZS9zdmcreG1sO2Jhc2U2NCxQSE4yWnlCNGJXeHVjejBpYUhSMGNEb3ZMM2QzZHk1M015NXZjbWN2TWpBd01DOXpkbWNpSUhkcFpIUm9QU0kwTlNJZ2FHVnBaMmgwUFNJME5TSStQSEJoZEdnZ1pEMGlUVEl5TGpVZ09XTXRNaTR5TVNBd0xUUWdNUzQzT1MwMElEUWdNQ0F1T0RrdU1qa2dNUzQzTVM0M09DQXlMak00UXpFM0xqTXpJREUyTGpVZ01UWWdNVGd1TlRrZ01UWWdNakZqTUNBeUxqQXpMamswSURNdU9EUWdNaTQwTVNBMUxqQXpMVE1nTVM0d05pMDNMalF4SURVdU5UVXROeTQwTVNBeE15NDBOMmd5TTJNd0xUY3VPVEl0TkM0ME1TMHhNaTQwTVMwM0xqUXhMVEV6TGpRM0lERXVORGN0TVM0eE9TQXlMalF4TFRNZ01pNDBNUzAxTGpBeklEQXRNaTQwTVMweExqTXpMVFF1TlMwekxqSTRMVFV1TmpJdU5Ea3RMalkzTGpjNExURXVORGt1TnpndE1pNHpPQ0F3TFRJdU1qRXRNUzQzT1MwMExUUXROSG9pSUhOMGNtOXJaVDBpSXpBd01DSWdjM1J5YjJ0bExYZHBaSFJvUFNJeExqVWlJSE4wY205clpTMXNhVzVsWTJGd1BTSnliM1Z1WkNJdlBqd3ZjM1puUGc9PScpO1xufVxuLmNnLXdyYXAgcGllY2UuYmlzaG9wLmJsYWNrIHtcbiAgYmFja2dyb3VuZC1pbWFnZTogdXJsKCdkYXRhOmltYWdlL3N2Zyt4bWw7YmFzZTY0LFBITjJaeUI0Yld4dWN6MGlhSFIwY0RvdkwzZDNkeTUzTXk1dmNtY3ZNakF3TUM5emRtY2lJSGRwWkhSb1BTSTBOU0lnYUdWcFoyaDBQU0kwTlNJK1BHY2dabWxzYkQwaWJtOXVaU0lnWm1sc2JDMXlkV3hsUFNKbGRtVnViMlJrSWlCemRISnZhMlU5SWlNd01EQWlJSE4wY205clpTMTNhV1IwYUQwaU1TNDFJaUJ6ZEhKdmEyVXRiR2x1WldOaGNEMGljbTkxYm1RaUlITjBjbTlyWlMxc2FXNWxhbTlwYmowaWNtOTFibVFpUGp4bklHWnBiR3c5SWlNd01EQWlJSE4wY205clpTMXNhVzVsWTJGd1BTSmlkWFIwSWo0OGNHRjBhQ0JrUFNKTk9TQXpObU16TGpNNUxTNDVOeUF4TUM0eE1TNDBNeUF4TXk0MUxUSWdNeTR6T1NBeUxqUXpJREV3TGpFeElERXVNRE1nTVRNdU5TQXlJREFnTUNBeExqWTFMalUwSURNZ01pMHVOamd1T1RjdE1TNDJOUzQ1T1MweklDNDFMVE11TXprdExqazNMVEV3TGpFeExqUTJMVEV6TGpVdE1TMHpMak01SURFdU5EWXRNVEF1TVRFdU1ETXRNVE11TlNBeExURXVNelUwTGpRNUxUSXVNekl6TGpRM0xUTXRMalVnTVM0ek5UUXRNUzQ1TkNBekxUSWdNeTB5ZWlJdlBqeHdZWFJvSUdROUlrMHhOU0F6TW1NeUxqVWdNaTQxSURFeUxqVWdNaTQxSURFMUlEQWdMalV0TVM0MUlEQXRNaUF3TFRJZ01DMHlMalV0TWk0MUxUUXRNaTQxTFRRZ05TNDFMVEV1TlNBMkxURXhMalV0TlMweE5TNDFMVEV4SURRdE1UQXVOU0F4TkMwMUlERTFMalVnTUNBd0xUSXVOU0F4TGpVdE1pNDFJRFFnTUNBd0xTNDFMalVnTUNBeWVpSXZQanh3WVhSb0lHUTlJazB5TlNBNFlUSXVOU0F5TGpVZ01DQXhJREV0TlNBd0lESXVOU0F5TGpVZ01DQXhJREVnTlNBd2VpSXZQand2Wno0OGNHRjBhQ0JrUFNKTk1UY3VOU0F5Tm1neE1FMHhOU0F6TUdneE5XMHROeTQxTFRFMExqVjJOVTB5TUNBeE9HZzFJaUJ6ZEhKdmEyVTlJaU5sWTJWalpXTWlJSE4wY205clpTMXNhVzVsYW05cGJqMGliV2wwWlhJaUx6NDhMMmMrUEM5emRtYysnKTtcbn1cbi5jZy13cmFwIHBpZWNlLmtuaWdodC5ibGFjayB7XG4gIGJhY2tncm91bmQtaW1hZ2U6IHVybCgnZGF0YTppbWFnZS9zdmcreG1sO2Jhc2U2NCxQSE4yWnlCNGJXeHVjejBpYUhSMGNEb3ZMM2QzZHk1M015NXZjbWN2TWpBd01DOXpkbWNpSUhkcFpIUm9QU0kwTlNJZ2FHVnBaMmgwUFNJME5TSStQR2NnWm1sc2JEMGlibTl1WlNJZ1ptbHNiQzF5ZFd4bFBTSmxkbVZ1YjJSa0lpQnpkSEp2YTJVOUlpTXdNREFpSUhOMGNtOXJaUzEzYVdSMGFEMGlNUzQxSWlCemRISnZhMlV0YkdsdVpXTmhjRDBpY205MWJtUWlJSE4wY205clpTMXNhVzVsYW05cGJqMGljbTkxYm1RaVBqeHdZWFJvSUdROUlrMHlNaUF4TUdNeE1DNDFJREVnTVRZdU5TQTRJREUySURJNVNERTFZekF0T1NBeE1DMDJMalVnT0MweU1TSWdabWxzYkQwaUl6QXdNQ0l2UGp4d1lYUm9JR1E5SWsweU5DQXhPR011TXpnZ01pNDVNUzAxTGpVMUlEY3VNemN0T0NBNUxUTWdNaTB5TGpneUlEUXVNelF0TlNBMExURXVNRFF5TFM0NU5DQXhMalF4TFRNdU1EUWdNQzB6TFRFZ01DQXVNVGtnTVM0eU15MHhJREl0TVNBd0xUUXVNREF6SURFdE5DMDBJREF0TWlBMkxURXlJRFl0TVRKek1TNDRPUzB4TGprZ01pMHpMalZqTFM0M015MHVPVGswTFM0MUxUSXRMalV0TXlBeExURWdNeUF5TGpVZ015QXlMalZvTW5NdU56Z3RNUzQ1T1RJZ01pNDFMVE5qTVNBd0lERWdNeUF4SURNaUlHWnBiR3c5SWlNd01EQWlMejQ4Y0dGMGFDQmtQU0pOT1M0MUlESTFMalZoTGpVdU5TQXdJREVnTVMweElEQWdMalV1TlNBd0lERWdNU0F4SURCNmJUVXVORE16TFRrdU56VmhMalVnTVM0MUlETXdJREVnTVMwdU9EWTJMUzQxTGpVZ01TNDFJRE13SURFZ01TQXVPRFkyTGpWNklpQm1hV3hzUFNJalpXTmxZMlZqSWlCemRISnZhMlU5SWlObFkyVmpaV01pTHo0OGNHRjBhQ0JrUFNKTk1qUXVOVFVnTVRBdU5Hd3RMalExSURFdU5EVXVOUzR4TldNekxqRTFJREVnTlM0Mk5TQXlMalE1SURjdU9TQTJMamMxVXpNMUxqYzFJREk1TGpBMklETTFMakkxSURNNWJDMHVNRFV1TldneUxqSTFiQzR3TlMwdU5XTXVOUzB4TUM0d05pMHVPRGd0TVRZdU9EVXRNeTR5TlMweU1TNHpOQzB5TGpNM0xUUXVORGt0TlM0M09TMDJMalkwTFRrdU1Ua3ROeTR4Tm13dExqVXhMUzR4ZWlJZ1ptbHNiRDBpSTJWalpXTmxZeUlnYzNSeWIydGxQU0p1YjI1bElpOCtQQzluUGp3dmMzWm5QZz09Jyk7XG59XG4uY2ctd3JhcCBwaWVjZS5yb29rLmJsYWNrIHtcbiAgYmFja2dyb3VuZC1pbWFnZTogdXJsKCdkYXRhOmltYWdlL3N2Zyt4bWw7YmFzZTY0LFBITjJaeUI0Yld4dWN6MGlhSFIwY0RvdkwzZDNkeTUzTXk1dmNtY3ZNakF3TUM5emRtY2lJSGRwWkhSb1BTSTBOU0lnYUdWcFoyaDBQU0kwTlNJK1BHY2dabWxzYkMxeWRXeGxQU0psZG1WdWIyUmtJaUJ6ZEhKdmEyVTlJaU13TURBaUlITjBjbTlyWlMxM2FXUjBhRDBpTVM0MUlpQnpkSEp2YTJVdGJHbHVaV05oY0QwaWNtOTFibVFpSUhOMGNtOXJaUzFzYVc1bGFtOXBiajBpY205MWJtUWlQanh3WVhSb0lHUTlJazA1SURNNWFESTNkaTB6U0RsMk0zcHRNeTQxTFRkc01TNDFMVEl1TldneE4yd3hMalVnTWk0MWFDMHlNSHB0TFM0MUlEUjJMVFJvTWpGMk5FZ3hNbm9pSUhOMGNtOXJaUzFzYVc1bFkyRndQU0ppZFhSMElpOCtQSEJoZEdnZ1pEMGlUVEUwSURJNUxqVjJMVEV6YURFM2RqRXpTREUwZWlJZ2MzUnliMnRsTFd4cGJtVmpZWEE5SW1KMWRIUWlJSE4wY205clpTMXNhVzVsYW05cGJqMGliV2wwWlhJaUx6NDhjR0YwYUNCa1BTSk5NVFFnTVRZdU5Vd3hNU0F4TkdneU0yd3RNeUF5TGpWSU1UUjZUVEV4SURFMFZqbG9OSFl5YURWV09XZzFkakpvTlZZNWFEUjJOVWd4TVhvaUlITjBjbTlyWlMxc2FXNWxZMkZ3UFNKaWRYUjBJaTgrUEhCaGRHZ2daRDBpVFRFeUlETTFMalZvTWpGdExUSXdMVFJvTVRsdExURTRMVEpvTVRkdExURTNMVEV6YURFM1RURXhJREUwYURJeklpQm1hV3hzUFNKdWIyNWxJaUJ6ZEhKdmEyVTlJaU5sWTJWalpXTWlJSE4wY205clpTMTNhV1IwYUQwaU1TSWdjM1J5YjJ0bExXeHBibVZxYjJsdVBTSnRhWFJsY2lJdlBqd3ZaejQ4TDNOMlp6ND0nKTtcbn1cbi5jZy13cmFwIHBpZWNlLnF1ZWVuLmJsYWNrIHtcbiAgYmFja2dyb3VuZC1pbWFnZTogdXJsKCdkYXRhOmltYWdlL3N2Zyt4bWw7YmFzZTY0LFBITjJaeUI0Yld4dWN6MGlhSFIwY0RvdkwzZDNkeTUzTXk1dmNtY3ZNakF3TUM5emRtY2lJSGRwWkhSb1BTSTBOU0lnYUdWcFoyaDBQU0kwTlNJK1BHY2dabWxzYkMxeWRXeGxQU0psZG1WdWIyUmtJaUJ6ZEhKdmEyVTlJaU13TURBaUlITjBjbTlyWlMxM2FXUjBhRDBpTVM0MUlpQnpkSEp2YTJVdGJHbHVaV05oY0QwaWNtOTFibVFpSUhOMGNtOXJaUzFzYVc1bGFtOXBiajBpY205MWJtUWlQanhuSUhOMGNtOXJaVDBpYm05dVpTSStQR05wY21Oc1pTQmplRDBpTmlJZ1kzazlJakV5SWlCeVBTSXlMamMxSWk4K1BHTnBjbU5zWlNCamVEMGlNVFFpSUdONVBTSTVJaUJ5UFNJeUxqYzFJaTgrUEdOcGNtTnNaU0JqZUQwaU1qSXVOU0lnWTNrOUlqZ2lJSEk5SWpJdU56VWlMejQ4WTJseVkyeGxJR040UFNJek1TSWdZM2s5SWpraUlISTlJakl1TnpVaUx6NDhZMmx5WTJ4bElHTjRQU0l6T1NJZ1kzazlJakV5SWlCeVBTSXlMamMxSWk4K1BDOW5Qanh3WVhSb0lHUTlJazA1SURJMll6Z3VOUzB4TGpVZ01qRXRNUzQxSURJM0lEQnNNaTQxTFRFeUxqVk1NekVnTWpWc0xTNHpMVEUwTGpFdE5TNHlJREV6TGpZdE15MHhOQzQxTFRNZ01UUXVOUzAxTGpJdE1UTXVOa3d4TkNBeU5TQTJMalVnTVRNdU5TQTVJREkyZWlJZ2MzUnliMnRsTFd4cGJtVmpZWEE5SW1KMWRIUWlMejQ4Y0dGMGFDQmtQU0pOT1NBeU5tTXdJRElnTVM0MUlESWdNaTQxSURRZ01TQXhMalVnTVNBeElDNDFJRE11TlMweExqVWdNUzB4TGpVZ01pNDFMVEV1TlNBeUxqVXRNUzQxSURFdU5TNDFJREl1TlM0MUlESXVOU0EyTGpVZ01TQXhOaTQxSURFZ01qTWdNQ0F3SURBZ01TNDFMVEVnTUMweUxqVWdNQ0F3SUM0MUxURXVOUzB4TFRJdU5TMHVOUzB5TGpVdExqVXRNaUF1TlMwekxqVWdNUzB5SURJdU5TMHlJREl1TlMwMExUZ3VOUzB4TGpVdE1UZ3VOUzB4TGpVdE1qY2dNSG9pSUhOMGNtOXJaUzFzYVc1bFkyRndQU0ppZFhSMElpOCtQSEJoZEdnZ1pEMGlUVEV4SURNNExqVmhNelVnTXpVZ01TQXdJREFnTWpNZ01DSWdabWxzYkQwaWJtOXVaU0lnYzNSeWIydGxMV3hwYm1WallYQTlJbUoxZEhRaUx6NDhjR0YwYUNCa1BTSk5NVEVnTWpsaE16VWdNelVnTVNBd0lERWdNak1nTUcwdE1qRXVOU0F5TGpWb01qQnRMVEl4SUROaE16VWdNelVnTVNBd0lEQWdNaklnTUcwdE1qTWdNMkV6TlNBek5TQXhJREFnTUNBeU5DQXdJaUJtYVd4c1BTSnViMjVsSWlCemRISnZhMlU5SWlObFkyVmpaV01pTHo0OEwyYytQQzl6ZG1jKycpO1xufVxuLmNnLXdyYXAgcGllY2Uua2luZy5ibGFjayB7XG4gIGJhY2tncm91bmQtaW1hZ2U6IHVybCgnZGF0YTppbWFnZS9zdmcreG1sO2Jhc2U2NCxQSE4yWnlCNGJXeHVjejBpYUhSMGNEb3ZMM2QzZHk1M015NXZjbWN2TWpBd01DOXpkbWNpSUhkcFpIUm9QU0kwTlNJZ2FHVnBaMmgwUFNJME5TSStQR2NnWm1sc2JEMGlibTl1WlNJZ1ptbHNiQzF5ZFd4bFBTSmxkbVZ1YjJSa0lpQnpkSEp2YTJVOUlpTXdNREFpSUhOMGNtOXJaUzEzYVdSMGFEMGlNUzQxSWlCemRISnZhMlV0YkdsdVpXTmhjRDBpY205MWJtUWlJSE4wY205clpTMXNhVzVsYW05cGJqMGljbTkxYm1RaVBqeHdZWFJvSUdROUlrMHlNaTQxSURFeExqWXpWallpSUhOMGNtOXJaUzFzYVc1bGFtOXBiajBpYldsMFpYSWlMejQ4Y0dGMGFDQmtQU0pOTWpJdU5TQXlOWE0wTGpVdE55NDFJRE10TVRBdU5XTXdJREF0TVMweUxqVXRNeTB5TGpWekxUTWdNaTQxTFRNZ01pNDFZeTB4TGpVZ015QXpJREV3TGpVZ015QXhNQzQxSWlCbWFXeHNQU0lqTURBd0lpQnpkSEp2YTJVdGJHbHVaV05oY0QwaVluVjBkQ0lnYzNSeWIydGxMV3hwYm1WcWIybHVQU0p0YVhSbGNpSXZQanh3WVhSb0lHUTlJazB4TVM0MUlETTNZelV1TlNBekxqVWdNVFV1TlNBekxqVWdNakVnTUhZdE4zTTVMVFF1TlNBMkxURXdMalZqTFRRdE5pNDFMVEV6TGpVdE15NDFMVEUySURSV01qZDJMVE11TldNdE15NDFMVGN1TlMweE15MHhNQzQxTFRFMkxUUXRNeUEySURVZ01UQWdOU0F4TUZZek4zb2lJR1pwYkd3OUlpTXdNREFpTHo0OGNHRjBhQ0JrUFNKTk1qQWdPR2cxSWlCemRISnZhMlV0YkdsdVpXcHZhVzQ5SW0xcGRHVnlJaTgrUEhCaGRHZ2daRDBpVFRNeUlESTVMalZ6T0M0MUxUUWdOaTR3TXkwNUxqWTFRek0wTGpFMUlERTBJREkxSURFNElESXlMalVnTWpRdU5Xd3VNREVnTWk0eExTNHdNUzB5TGpGRE1qQWdNVGdnT1M0NU1EWWdNVFFnTmk0NU9UY2dNVGt1T0RWakxUSXVORGszSURVdU5qVWdOQzQ0TlRNZ09TQTBMamcxTXlBNUlpQnpkSEp2YTJVOUlpTmxZMlZqWldNaUx6NDhjR0YwYUNCa1BTSk5NVEV1TlNBek1HTTFMalV0TXlBeE5TNDFMVE1nTWpFZ01HMHRNakVnTXk0MVl6VXVOUzB6SURFMUxqVXRNeUF5TVNBd2JTMHlNU0F6TGpWak5TNDFMVE1nTVRVdU5TMHpJREl4SURBaUlITjBjbTlyWlQwaUkyVmpaV05sWXlJdlBqd3ZaejQ4TDNOMlp6ND0nKTtcbn1cbiIsICIvKiBHZW5lcmFsICovXG4uY2hlc3Mtc3R1ZHkgYnV0dG9uOmFjdGl2ZSB7XG5cdHRyYW5zZm9ybTogdHJhbnNsYXRlWSgxcHgpO1xufVxuXG4uY2hlc3Mtc3R1ZHkgLnZlcnRpY2FsLWFsaWduIHtcblx0ZGlzcGxheTogZmxleDtcblx0YWxpZ24taXRlbXM6IGNlbnRlcjtcbn1cblxuLmNoZXNzLXN0dWR5IC5jZW50ZXIge1xuXHRkaXNwbGF5OiBmbGV4O1xuXHRhbGlnbi1pdGVtczogY2VudGVyO1xuXHRqdXN0aWZ5LWNvbnRlbnQ6IGNlbnRlcjtcbn1cblxuLmNoZXNzLXN0dWR5IC5oZWlnaHQtd2lkdGgtMTAwIHtcblx0d2lkdGg6IDEwMCU7XG5cdGhlaWdodDogMTAwJTtcbn1cblxuLmNoZXNzLXN0dWR5IC50YWJsZSB7XG5cdGRpc3BsYXk6IHRhYmxlO1xufVxuXG4uY2hlc3Mtc3R1ZHkge1xuXHRoZWlnaHQ6IDEwMCU7XG5cdG1heC13aWR0aDogNzUwcHg7XG5cdGxpbmUtaGVpZ2h0OiAxLjU7XG5cdC13ZWJraXQtZm9udC1zbW9vdGhpbmc6IGFudGlhbGlhc2VkO1xuXHRiYWNrZ3JvdW5kLWNvbG9yOiB2YXIoLS1iYWNrZ3JvdW5kLXNlY29uZGFyeSk7XG5cdGJvcmRlcjogMXB4IHNvbGlkIHZhcigtLWJhY2tncm91bmQtc2Vjb25kYXJ5LWFsdCk7XG5cdGJvcmRlci1yYWRpdXM6IDVweDtcbn1cblxuLmNoZXNzLXN0dWR5ICosXG4uY2hlc3Mtc3R1ZHkgKjo6YmVmb3JlLFxuLmNoZXNzLXN0dWR5ICo6OmFmdGVyIHtcblx0Ym94LXNpemluZzogYm9yZGVyLWJveDtcbn1cbi5jaGVzcy1zdHVkeSAqIHtcblx0bWFyZ2luOiAwO1xufVxuXG4uY2hlc3Mtc3R1ZHkgaW5wdXQsXG4uY2hlc3Mtc3R1ZHkgYnV0dG9uLFxuLmNoZXNzLXN0dWR5IHRleHRhcmVhLFxuLmNoZXNzLXN0dWR5IHNlbGVjdCB7XG5cdGZvbnQ6IGluaGVyaXQ7XG59XG5cbi5jaGVzcy1zdHVkeSBwLFxuLmNoZXNzLXN0dWR5IGgxLFxuLmNoZXNzLXN0dWR5IGgyLFxuLmNoZXNzLXN0dWR5IGgzLFxuLmNoZXNzLXN0dWR5IGg0LFxuLmNoZXNzLXN0dWR5IGg1LFxuLmNoZXNzLXN0dWR5IGg2IHtcblx0b3ZlcmZsb3ctd3JhcDogYnJlYWstd29yZDtcbn1cblxuLyogQ29udGFpbmVyICovXG4uY2hlc3Mtc3R1ZHkgLmNoZXNzZ3JvdW5kLXBnbi1jb250YWluZXIge1xuXHRkaXNwbGF5OiBmbGV4O1xuXHRoZWlnaHQ6IDQ1MHB4O1xufVxuXG4uY2hlc3Mtc3R1ZHkgLmNoZXNzZ3JvdW5kLXBnbi1jb250YWluZXIgLmNoZXNzZ3JvdW5kLWNvbnRhaW5lciB7XG5cdGZsZXg6IDAgMCA0NTBweDtcblx0aGVpZ2h0OiAxMDAlO1xufVxuXG4uY2hlc3Mtc3R1ZHkgLmNoZXNzZ3JvdW5kLXBnbi1jb250YWluZXIgLnBnbi1jb250YWluZXIge1xuXHRmbGV4OiAxO1xuXHRoZWlnaHQ6IDEwMCU7XG59XG5cbi8qIFBHTiBWaWV3ZXIgKi9cblxuLmNoZXNzLXN0dWR5IC5tb3ZlLWl0ZW0tc2VjdGlvbiB7XG5cdGhlaWdodDogMzgwcHg7XG59XG5cbi5jaGVzcy1zdHVkeSAuYnV0dG9uLXNlY3Rpb24ge1xuXHRkaXNwbGF5OiBmbGV4O1xuXHRqdXN0aWZ5LWNvbnRlbnQ6IGNlbnRlcjtcblx0YWxpZ24taXRlbXM6IGNlbnRlcjtcblx0d2lkdGg6IDEwMCU7XG5cdGdhcDogNHB4O1xuXHRoZWlnaHQ6IDMycHg7XG59XG5cbi8qIE1vdmUgSXRlbXMgKi9cblxuLmNoZXNzLXN0dWR5IC5tb3ZlLWl0ZW0tY29udGFpbmVyIHtcblx0ZGlzcGxheTogZ3JpZDtcblx0Z3JpZC10ZW1wbGF0ZS1jb2x1bW5zOiAwLjE1ZnIgMC40MjVmciAwLjQyNWZyO1xuXHRncmlkLWF1dG8tcm93czogbWlubWF4KDM1cHgsIGF1dG8pO1xuXHR3aWR0aDogMTAwJTtcblx0bWF4LWhlaWdodDogMzgwcHg7XG5cdG92ZXJmbG93LXk6IHNjcm9sbDtcbn1cblxuLmNoZXNzLXN0dWR5IC5tb3ZlLWl0ZW0tY29udGFpbmVyID4gKiB7XG5cdG1hcmdpbjogMDtcbn1cblxuLmNoZXNzLXN0dWR5IC5tb3ZlLWl0ZW0ge1xuXHRwYWRkaW5nLWxlZnQ6IDhweDtcblx0dXNlci1zZWxlY3Q6IG5vbmU7XG59XG5cbi5jaGVzcy1zdHVkeSAubW92ZS1pdGVtOmhvdmVyIHtcblx0YmFja2dyb3VuZC1jb2xvcjogaHNsKHZhcigtLWFjY2VudC1oKSwgdmFyKC0tYWNjZW50LXMpLCBjYWxjKHZhcigtLWFjY2VudC1sKSkpO1xuXHRjb2xvcjogdmFyKC0tdGV4dC1vbi1hY2NlbnQpO1xuXHRjdXJzb3I6IHBvaW50ZXI7XG59XG5cbi5jaGVzcy1zdHVkeSAubW92ZS1pdGVtLmFjdGl2ZSB7XG5cdGJhY2tncm91bmQtY29sb3I6IGhzbChcblx0XHR2YXIoLS1hY2NlbnQtaCksXG5cdFx0dmFyKC0tYWNjZW50LXMpLFxuXHRcdGNhbGModmFyKC0tYWNjZW50LWwpIC0gMTAlKVxuXHQpO1xuXHRjb2xvcjogdmFyKC0tdGV4dC1vbi1hY2NlbnQpO1xuXHRmb250LXdlaWdodDogYm9sZDtcbn1cblxuLyogVmFyaWFudCBNb3ZlIEl0ZW1zICovXG5cbi5jaGVzcy1zdHVkeSAudmFyaWFudHMtY29udGFpbmVyIHtcblx0Z3JpZC1jb2x1bW46IHNwYW4gMyAvIGF1dG87XG5cdGRpc3BsYXk6IGZsZXg7XG5cdGZsZXgtZGlyZWN0aW9uOiBjb2x1bW47XG5cdGJvcmRlci1ib3R0b206IDFweCBzb2xpZCB2YXIoLS1jb2xvci1iYXNlLTUwKTtcbn1cblxuLmNoZXNzLXN0dWR5IC52YXJpYW50LWNvbnRhaW5lciB7XG5cdGdyaWQtY29sdW1uOiBzcGFuIDMgLyBhdXRvO1xuXHRkaXNwbGF5OiBmbGV4O1xuXHRmbGV4LWRpcmVjdGlvbjogY29sdW1uO1xufVxuXG4uY2hlc3Mtc3R1ZHkgLm1vdmUtaW5kaWNhdG9yIHtcblx0Y29sb3I6IHZhcigtLWNvbG9yLWJhc2UtNjApO1xuXHR0ZXh0LWFsaWduOiBjZW50ZXI7XG5cdGJhY2tncm91bmQtY29sb3I6IHZhcigtLWNvbG9yLWJhc2UtMTApO1xuXHR1c2VyLXNlbGVjdDogbm9uZTtcblx0Y3Vyc29yOiBkZWZhdWx0O1xufVxuXG4uY2hlc3Mtc3R1ZHkgLnZhcmlhbnQtbW92ZS1pdGVtIHtcblx0cGFkZGluZy1sZWZ0OiA2cHg7XG5cdHVzZXItc2VsZWN0OiBub25lO1xuXHRjdXJzb3I6IHBvaW50ZXI7XG59XG5cbi5jaGVzcy1zdHVkeSAudmFyaWFudC1tb3ZlLWl0ZW0tY29udGFpbmVyIHtcblx0ZGlzcGxheTogZmxleDtcblx0ZmxleC13cmFwOiB3cmFwO1xuXHRwYWRkaW5nOiA0cHggMDtcblx0Ym9yZGVyLXRvcDogMXB4IHNvbGlkIHZhcigtLWNvbG9yLWJhc2UtNTApO1xufVxuXG4uY2hlc3Mtc3R1ZHkgLnZhcmlhbnQtbW92ZS1pdGVtLmFjdGl2ZSxcbi5jaGVzcy1zdHVkeSAudmFyaWFudC1tb3ZlLWl0ZW06aG92ZXIge1xuXHRjb2xvcjogaHNsKHZhcigtLWFjY2VudC1oKSwgdmFyKC0tYWNjZW50LXMpLCBjYWxjKHZhcigtLWFjY2VudC1sKSAtIDEwJSkpO1xufVxuXG4uY2hlc3Mtc3R1ZHkgLnZhcmlhbnQtbW92ZS1pbmRpY2F0b3Ige1xuXHRmb250LXdlaWdodDogYm9sZDtcbn1cblxuLyogQ29tbWVudCBTZWN0aW9uICovXG5cbi5jaGVzcy1zdHVkeSAuZWRpdG9yLWlucHV0IHtcblx0bWluLWhlaWdodDogMTUwcHg7XG5cdHJlc2l6ZTogbm9uZTtcblx0Zm9udC1zaXplOiAxNXB4O1xuXHRjYXJldC1jb2xvcjogcmdiKDUsIDUsIDUpO1xuXHRwb3NpdGlvbjogcmVsYXRpdmU7XG5cdHRhYi1zaXplOiAxO1xuXHRvdXRsaW5lOiAwO1xuXHRwYWRkaW5nOiAxNXB4IDEwcHg7XG5cdGNhcmV0LWNvbG9yOiAjNDQ0O1xufVxuXG4uY2hlc3Mtc3R1ZHkgLkNvbW1lbnRTZWN0aW9uIHtcblx0aGVpZ2h0OiAyNTBweDtcblx0cGFkZGluZy10b3A6IDVweDtcblx0b3ZlcmZsb3c6IHNjcm9sbDtcblx0Ym9yZGVyLXRvcDogMXB4IHNvbGlkIHZhcigtLWJhY2tncm91bmQtc2Vjb25kYXJ5LWFsdCk7XG59XG5cbi5jaGVzcy1zdHVkeSAuUHJvc2VNaXJyb3IsXG4uY2hlc3Mtc3R1ZHkgLkNvbW1lbnRTZWN0aW9uID4gZGl2IHtcblx0d2lkdGg6IDEwMCU7XG5cdGhlaWdodDogMTAwJTtcbn1cbiJdLAogICJtYXBwaW5ncyI6ICI7QUFBQTtBQUNDO0FBQUE7OztBQ0REO0FBQ0U7QUFDQTtBQUNBO0FBQUE7QUFHRjtBQUNFO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFBQTtBQUdGO0FBQ0U7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUFBO0FBR0Y7QUFDRTtBQUFBO0FBR0Y7QUFDRTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFBQTtBQUdGO0FBQ0U7QUFBQTtBQUdGO0FBQ0U7QUFBQTtBQUdGO0FBQ0U7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQUE7QUFHRjtBQUNFO0FBRUE7QUFBQTtBQUdGO0FBQ0U7QUFBQTtBQUdGO0FBQ0U7QUFDQTtBQUFBO0FBR0Y7QUFDRTtBQUFBO0FBR0Y7QUFDRTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFBQTtBQUdGO0FBQUE7QUFBQTtBQUdFO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQUE7QUFHRjtBQUNFO0FBQUE7QUFHRjtBQUNFO0FBQUE7QUFHRjtBQUNFO0FBQ0E7QUFDQTtBQUFBO0FBR0Y7QUFFRTtBQUFBO0FBR0Y7QUFDRTtBQUFBO0FBR0Y7QUFDRTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFBQTtBQUdGO0FBQ0U7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUFBO0FBR0Y7QUFDRTtBQUFBO0FBR0Y7QUFDRTtBQUNBO0FBQUE7QUFHRjtBQUNFO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQUE7QUFHRjtBQUNFO0FBQUE7QUFHRjtBQUNFO0FBQUE7QUFHRjtBQUNFO0FBQUE7OztBQzNLRjtBQUNFO0FBQ0E7QUFBQTtBQUlGO0FBQ0U7QUFBQTtBQUVGO0FBQ0U7QUFBQTtBQUVGO0FBQ0U7QUFBQTtBQUVGO0FBQ0U7QUFBQTtBQUVGO0FBQ0U7QUFBQTtBQUVGO0FBQ0U7QUFBQTtBQUVGO0FBQ0U7QUFBQTtBQUVGO0FBQ0U7QUFBQTtBQUVGO0FBQ0U7QUFBQTtBQVFGO0FBQ0U7QUFBQTtBQUlGO0FBQUE7QUFBQTtBQUFBO0FBSUU7QUFBQTtBQUdGO0FBQUE7QUFBQTtBQUFBO0FBSUU7QUFBQTs7O0FDdkRGO0FBQ0U7QUFBQTtBQUVGO0FBQ0U7QUFBQTtBQUVGO0FBQ0U7QUFBQTtBQUVGO0FBQ0U7QUFBQTtBQUVGO0FBQ0U7QUFBQTtBQUVGO0FBQ0U7QUFBQTtBQUVGO0FBQ0U7QUFBQTtBQUVGO0FBQ0U7QUFBQTtBQUVGO0FBQ0U7QUFBQTtBQUVGO0FBQ0U7QUFBQTtBQUVGO0FBQ0U7QUFBQTtBQUVGO0FBQ0U7QUFBQTs7O0FDbENGO0FBQ0M7QUFBQTtBQUdEO0FBQ0M7QUFDQTtBQUFBO0FBR0Q7QUFDQztBQUNBO0FBQ0E7QUFBQTtBQUdEO0FBQ0M7QUFDQTtBQUFBO0FBR0Q7QUFDQztBQUFBO0FBR0Q7QUFDQztBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUFBO0FBR0Q7QUFBQTtBQUFBO0FBR0M7QUFBQTtBQUVEO0FBQ0M7QUFBQTtBQUdEO0FBQUE7QUFBQTtBQUFBO0FBSUM7QUFBQTtBQUdEO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBT0M7QUFBQTtBQUlEO0FBQ0M7QUFDQTtBQUFBO0FBR0Q7QUFDQztBQUNBO0FBQUE7QUFHRDtBQUNDO0FBQ0E7QUFBQTtBQUtEO0FBQ0M7QUFBQTtBQUdEO0FBQ0M7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQUE7QUFLRDtBQUNDO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUFBO0FBR0Q7QUFDQztBQUFBO0FBR0Q7QUFDQztBQUNBO0FBQUE7QUFHRDtBQUNDO0FBQ0E7QUFDQTtBQUFBO0FBR0Q7QUFDQztBQUtBO0FBQ0E7QUFBQTtBQUtEO0FBQ0M7QUFDQTtBQUNBO0FBQ0E7QUFBQTtBQUdEO0FBQ0M7QUFDQTtBQUNBO0FBQUE7QUFHRDtBQUNDO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFBQTtBQUdEO0FBQ0M7QUFDQTtBQUNBO0FBQUE7QUFHRDtBQUNDO0FBQ0E7QUFDQTtBQUNBO0FBQUE7QUFHRDtBQUFBO0FBRUM7QUFBQTtBQUdEO0FBQ0M7QUFBQTtBQUtEO0FBQ0M7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQUE7QUFHRDtBQUNDO0FBQ0E7QUFDQTtBQUNBO0FBQUE7QUFHRDtBQUFBO0FBRUM7QUFDQTtBQUFBOyIsCiAgIm5hbWVzIjogW10KfQo= */