├── .env.local
├── .gitignore
├── LICENSE
├── README.md
├── doc
├── build.png
└── study.png
├── jest.config.js
├── mongoscripts
├── README.md
└── largestrepertoire.js
├── package.json
├── src
├── client
│ ├── about.html
│ ├── css
│ │ ├── about.css
│ │ ├── board.css
│ │ ├── chessground.css
│ │ ├── constants.css
│ │ ├── main.css
│ │ ├── pieces.css
│ │ └── toastr.min.css
│ ├── favicon.ico
│ ├── img
│ │ ├── board
│ │ │ ├── blue.svg
│ │ │ ├── brown.svg
│ │ │ ├── gray.jpg
│ │ │ ├── green.svg
│ │ │ ├── leather.jpg
│ │ │ ├── marble.jpg
│ │ │ ├── purple.svg
│ │ │ ├── wood1.jpg
│ │ │ ├── wood2.jpg
│ │ │ └── wood3.jpg
│ │ ├── export.png
│ │ ├── faviconbright.ico
│ │ ├── faviconbright2.ico
│ │ ├── favicondark.ico
│ │ ├── import.png
│ │ ├── left.png
│ │ ├── logobright.png
│ │ ├── logobright150.png
│ │ ├── logodark.png
│ │ ├── logodark150.png
│ │ ├── pencil.png
│ │ ├── pieces
│ │ │ └── merida
│ │ │ │ ├── bB.svg
│ │ │ │ ├── bK.svg
│ │ │ │ ├── bN.svg
│ │ │ │ ├── bP.svg
│ │ │ │ ├── bQ.svg
│ │ │ │ ├── bR.svg
│ │ │ │ ├── wB.svg
│ │ │ │ ├── wK.svg
│ │ │ │ ├── wN.svg
│ │ │ │ ├── wP.svg
│ │ │ │ ├── wQ.svg
│ │ │ │ └── wR.svg
│ │ ├── plus.png
│ │ ├── soundoff.png
│ │ ├── soundon.png
│ │ ├── theme.png
│ │ ├── trash.png
│ │ └── x.png
│ ├── js
│ │ ├── annotation
│ │ │ ├── annotationrenderer.ts
│ │ │ ├── annotator.ts
│ │ │ ├── noopannotationrenderer.ts
│ │ │ └── nullannotator.ts
│ │ ├── auth
│ │ │ └── authmanager.ts
│ │ ├── board
│ │ │ ├── board.ts
│ │ │ ├── boardhandler.ts
│ │ │ ├── chessgroundboard.ts
│ │ │ ├── chessgroundboardfactory.ts
│ │ │ ├── delegatingboard.ts
│ │ │ └── noopboard.ts
│ │ ├── build
│ │ │ ├── annotation
│ │ │ │ ├── buildannotation.ts
│ │ │ │ ├── defaultannotationrenderer.ts
│ │ │ │ ├── defaultannotator.ts
│ │ │ │ └── displaytype.ts
│ │ │ ├── buildboardhandler.ts
│ │ │ ├── buildmode.ts
│ │ │ ├── colorchooser.ts
│ │ │ ├── currentrepertoireexporter.ts
│ │ │ ├── currentrepertoireupdater.ts
│ │ │ ├── examplerepertoirehandler.ts
│ │ │ ├── examplerepertoires.ts
│ │ │ ├── import
│ │ │ │ ├── converterstatus.ts
│ │ │ │ ├── currentrepertoireimporter.ts
│ │ │ │ ├── importdialog.ts
│ │ │ │ ├── pgnimporter.ts
│ │ │ │ ├── pgnimportprogress.ts
│ │ │ │ ├── pgnparser.test.ts
│ │ │ │ ├── pgnparser.ts
│ │ │ │ ├── repertoireincrementalconverter.ts
│ │ │ │ └── treemodelpopulator.ts
│ │ │ ├── renameinput.ts
│ │ │ └── treecontroller.ts
│ │ ├── common
│ │ │ ├── config.ts
│ │ │ ├── debouncer.test.ts
│ │ │ ├── debouncer.ts
│ │ │ ├── listrefreshableview.ts
│ │ │ ├── refreshableview.ts
│ │ │ ├── toasts.ts
│ │ │ ├── tooltips.ts
│ │ │ └── viewinfo.ts
│ │ ├── evaluate
│ │ │ ├── annotation
│ │ │ │ ├── statisticannotation.ts
│ │ │ │ ├── statisticannotationrenderer.ts
│ │ │ │ └── statisticannotator.ts
│ │ │ ├── childmovedrawer.ts
│ │ │ ├── evaluateboardhandler.ts
│ │ │ ├── evaluatemode.ts
│ │ │ ├── insights
│ │ │ │ ├── insight.ts
│ │ │ │ ├── insightcalculator.ts
│ │ │ │ └── insightspanel.ts
│ │ │ └── repertoirenamelabel.ts
│ │ ├── footer
│ │ │ └── footerlinks.ts
│ │ ├── impressions
│ │ │ ├── debouncingimpressionsender.ts
│ │ │ └── impressionsender.ts
│ │ ├── lib
│ │ │ ├── auth0.min.js
│ │ │ ├── chess.min.js
│ │ │ ├── chessground.min.js
│ │ │ ├── jquery.min.js
│ │ │ ├── micromodal.min.js
│ │ │ ├── pgnparser.js
│ │ │ ├── tippy.all.min.js
│ │ │ └── toastr.min.js
│ │ ├── main.ts
│ │ ├── mode
│ │ │ ├── mode.ts
│ │ │ ├── modemanager.test.ts
│ │ │ ├── modemanager.ts
│ │ │ ├── modetype.ts
│ │ │ └── noopmode.ts
│ │ ├── picker
│ │ │ ├── confirmdeletedialog.ts
│ │ │ ├── pickercontroller.ts
│ │ │ ├── pickerfeature.ts
│ │ │ ├── pickermodel.test.ts
│ │ │ ├── pickermodel.ts
│ │ │ └── pickerview.ts
│ │ ├── preferences
│ │ │ ├── preferenceloader.ts
│ │ │ └── preferencesaver.ts
│ │ ├── priveleged
│ │ │ ├── privelegedcopydialog.ts
│ │ │ └── privelegedfeature.ts
│ │ ├── server
│ │ │ ├── accesstokenserverwrapper.ts
│ │ │ ├── delegatingserverwrapper.ts
│ │ │ ├── evaluatedflagfetcher.ts
│ │ │ ├── localstorageserverwrapper.test.ts
│ │ │ ├── localstorageserverwrapper.ts
│ │ │ └── serverwrapper.ts
│ │ ├── sound
│ │ │ ├── soundplayer.ts
│ │ │ └── soundtoggler.ts
│ │ ├── statistics
│ │ │ ├── debouncingstatisticsrecorder.ts
│ │ │ ├── delegatingstatisticsmodel.ts
│ │ │ ├── serverstatisticsmodel.ts
│ │ │ ├── statisticrecorder.ts
│ │ │ ├── statisticsmodel.ts
│ │ │ └── zerostatisticsmodel.ts
│ │ ├── study
│ │ │ ├── line.ts
│ │ │ ├── lineemitter.ts
│ │ │ ├── lineliststudier.ts
│ │ │ ├── lineshuffler.ts
│ │ │ ├── linestudier.ts
│ │ │ ├── studyboardhandler.ts
│ │ │ └── studymode.ts
│ │ ├── theme
│ │ │ ├── boardthemeinfo.ts
│ │ │ ├── boardthemesetter.ts
│ │ │ └── themepalette.ts
│ │ └── tree
│ │ │ ├── addmoveresult.ts
│ │ │ ├── emptymessage.ts
│ │ │ ├── fennormalizer.ts
│ │ │ ├── fentopgnmap.ts
│ │ │ ├── pgntonodemap.ts
│ │ │ ├── treebutton.ts
│ │ │ ├── treebuttons.ts
│ │ │ ├── treemodel.ts
│ │ │ ├── treenavigator.ts
│ │ │ ├── treenode.ts
│ │ │ ├── treenodehandler.ts
│ │ │ └── treeview.ts
│ ├── main.html
│ └── ogg
│ │ ├── capture.ogg
│ │ ├── finishline.ogg
│ │ ├── move.ogg
│ │ └── wrongmove.ogg
├── flag
│ ├── flags.ts
│ └── rolloutstate.ts
├── protocol
│ ├── actions.ts
│ ├── boardtheme.ts
│ ├── color.ts
│ ├── evaluatedflags.ts
│ ├── impression
│ │ ├── extradata.test.ts
│ │ ├── extradata.ts
│ │ ├── impression.ts
│ │ └── impressioncode.ts
│ ├── move.ts
│ ├── preference
│ │ ├── preference.test.ts
│ │ └── preference.ts
│ ├── soundvalue.ts
│ ├── statistic
│ │ ├── cumulatedstatistic.ts
│ │ ├── statistic.ts
│ │ └── statistictype.ts
│ └── storage.ts
├── server
│ ├── action.ts
│ ├── actions
│ │ ├── createrepertoireaction.ts
│ │ ├── deleterepertoireaction.ts
│ │ ├── evaluateflagsaction.ts
│ │ ├── getpreferenceaction.ts
│ │ ├── loadcumulatedstatisticsaction.ts
│ │ ├── loadrepertoireaction.ts
│ │ ├── logimpressionsaction.ts
│ │ ├── privelegedcopyaction.ts
│ │ ├── recordstatisticsaction.ts
│ │ ├── repertoiremetadataaction.ts
│ │ ├── setpreferenceaction.ts
│ │ └── updaterepertoireaction.ts
│ ├── checkrequestresult.ts
│ ├── databasewrapper.ts
│ ├── endpointregistry.ts
│ ├── flagevaluator.ts
│ ├── main.ts
│ ├── privelegedusers.ts
│ └── server.ts
├── tools
│ ├── generate-pgn-parser.sh
│ └── grammar.peg
└── util
│ ├── assert.ts
│ ├── datetime.ts
│ └── random.ts
├── tsconfig.json
├── tslint.json
├── tstypes
└── express-jwt-authz
│ └── index.d.ts
└── webpack.config.js
/.env.local:
--------------------------------------------------------------------------------
1 | DATABASE_PATH=mongodb://127.0.0.1:27017
2 | ENABLE_LOCAL_FLAGS=1
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/*
3 | package-lock.json
4 | .env
5 | build/*
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Agustin O Venezuela III
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # studyopenings
2 |
3 | > **UPDATE (Dec 31 2024):** After 5 years of serving studyopenings in production, I've decided to take down the site. I've financed the server costs entirely out-of-pocket and maintained the site entirely in my free time alongside a full-time job and, though I've enjoyed it until now, I no longer wish to pursue this. I wish studyopenings users the best of luck in their chess improvement journey.
4 |
5 | 
6 |
7 | ## What is this?
8 | A tool to help chess players memorize opening repertoires.
9 |
10 | The tool lets you:
11 |
12 | - _build_ repertoires by playing opening moves on a board and
13 |
14 | - _study_ repertoires by repeatedly recalling one side of the opening lines.
15 |
16 | ## Credits
17 | - Chessboard UI uses lichess' [Chessground](https://github.com/ornicar/chessground).
18 | - Chess game logic uses [chess.js](https://github.com/jhlywa/chess.js/).
19 | - PGN parser uses [PEG.js](https://pegjs.org/) and the grammar from [kevinludwig/pgn-parser](https://github.com/kevinludwig/pgn-parser).
20 | - Authentication uses [Auth0](https://auth0.com/).
21 | - Icons from [the Noun Project](https://thenounproject.com/).
22 | - Tooltips use [tippy.js](https://atomiks.github.io/tippyjs/).
23 | - Toasts use [toastr](https://github.com/CodeSeven/toastr).
24 | - Sounds from [lichess](https://github.com/ornicar/lila/tree/master/public/sound) and use [howler.js](https://howlerjs.com).
25 | - Feedback mechanism uses [Doorbell.io](https://doorbell.io).
26 | - Database uses [MongoDB](https://www.mongodb.com) and is hosted on [MongoDB Atlas](https://www.mongodb.com/cloud/atlas).
27 | - Server written in [Node.js](http://nodejs.org).
28 | - Client bundling uses [Webpack](https://webpack.js.org).
29 | - Unit tests use [Jest](https://jestjs.io) and [ts-jest](https://github.com/kulshekhar/ts-jest).
30 |
31 | ## Running locally
32 | 1. Clone the repository.
33 | 2. [Install MongoDB](https://docs.mongodb.com) if necessary. Then start a local MongoDB database instance:
34 | ```shell
35 | $ mongod --dbpath ~/data/db --port 27017
36 | [...]
37 | waiting for connections on port 27017
38 | ```
39 | 3. Copy the `.env` file which points the application to your local database:
40 | ```shell
41 | studyopenings/ $ cp .env.local .env
42 | ```
43 | 4. Run the server:
44 | ```shell
45 | studyopenings/ $ npm install
46 | studyopenings/ $ npm run start-dev
47 | [...]
48 | studyopenings is running!
49 | Listening on 5000.
50 | Using database path: mongodb://127.0.0.1:27017
51 | ```
52 |
53 | 5. Go to http://localhost:5000.
54 |
55 | ## Running tests
56 |
57 | To run all the tests once:
58 | ```shell
59 | studyopenings/ $ npm install
60 | studyopenings/ $ npm run test
61 | [...]
62 | Ran all test suites.
63 | ```
64 |
65 | To run the tests continuously as changes are made:
66 |
67 | ```shell
68 | studyopenings/ $ npm install
69 | studyopenings/ $ jest --watch
70 | ```
71 |
72 | ## Author
73 | Justin Venezuela • jven@jvenezue.la • http://jvenezue.la
74 |
--------------------------------------------------------------------------------
/doc/build.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jven/studyopenings/55ac1b6ad98589744440c4677b9664e56c402db6/doc/build.png
--------------------------------------------------------------------------------
/doc/study.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jven/studyopenings/55ac1b6ad98589744440c4677b9664e56c402db6/doc/study.png
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | testEnvironment: 'jsdom',
4 | };
--------------------------------------------------------------------------------
/mongoscripts/README.md:
--------------------------------------------------------------------------------
1 | # mongoscripts
2 |
3 | ## Running scripts
4 |
5 | For example, to run the `largestrepertoire.js` script locally:
6 |
7 | ```shell
8 | $ mongo mongoscripts/largestrepertoire.js
9 | [...]
10 | ObjectId("largestRepertoireId")
11 | ```
12 |
13 | To run in production:
14 |
15 | ```shell
16 | $ mongo "mongodb+srv://path-to-database.mongodb.net/test" --username db-user mongoscripts/largestrepertoire.js
17 | [...]
18 | ObjectId("largestRepertoireId")
19 | ```
20 |
--------------------------------------------------------------------------------
/mongoscripts/largestrepertoire.js:
--------------------------------------------------------------------------------
1 | var db = db.getSiblingDB('studyopenings');
2 | var maxObj = null;
3 | var maxSize = 0;
4 | db.repertoires.find().forEach(function(obj) { var size = Object.bsonsize(obj); if (size > maxSize) { maxSize = size; maxObj = obj; } } );
5 |
6 | printjson(maxObj._id);
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "studyopenings",
3 | "version": "1.0.0",
4 | "description": "An opening a day takes the blunders away!",
5 | "scripts": {
6 | "clean": "rm -rf build/*",
7 | "compile": "tsc",
8 | "copystatic": "mkdir -p build/client && cp -r src/client/css src/client/img src/client/ogg src/client/*.html src/client/*.ico build/client",
9 | "copyclientlib": "mkdir -p build/client/js && cp -r src/client/js/lib build/client/js",
10 | "fixlint": "tslint --fix 'src/**/*.ts'",
11 | "lint": "tslint 'src/**/*.ts'",
12 | "start": "npm run compile && (npm run webpack & (npm run copystatic && npm run copyclientlib && node build/server/main.js))",
13 | "start-dev": "npm run lint && npm run compile && npm run copystatic && npm run copyclientlib && npm run webpack-dev && node build/server/main.js",
14 | "test": "jest",
15 | "webpack": "npx webpack --mode=production",
16 | "webpack-dev": "npx webpack --mode=development"
17 | },
18 | "repository": {
19 | "type": "git",
20 | "url": "git+https://github.com/jven/studyopenings.git"
21 | },
22 | "author": "",
23 | "license": "ISC",
24 | "bugs": {
25 | "url": "https://github.com/jven/studyopenings/issues"
26 | },
27 | "homepage": "https://github.com/jven/studyopenings#readme",
28 | "dependencies": {
29 | "@types/auth0-js": "8.11.7",
30 | "@types/cors": "2.8.8",
31 | "@types/crypto-random-string": "1.0.0",
32 | "@types/dotenv": "^6.1.0",
33 | "@types/express": "4.16.0",
34 | "@types/express-jwt": "0.0.41",
35 | "@types/howler": "2.0.5",
36 | "@types/mongodb": "3.5.31",
37 | "@types/node": "14.11.2",
38 | "@types/toastr": "2.1.35",
39 | "auth0-js": "9.8.2",
40 | "body-parser": "1.18.3",
41 | "chessground": "7.9.2",
42 | "cors": "2.8.5",
43 | "crypto-random-string": "1.0.0",
44 | "dotenv": "6.2.0",
45 | "express": "4.15.2",
46 | "express-jwt": "5.3.1",
47 | "express-jwt-authz": "1.0.0",
48 | "howler": "2.2.1",
49 | "http": "0.0.0",
50 | "jwks-rsa": "1.11.0",
51 | "mongodb": "3.1.10",
52 | "typescript": "3.9.7",
53 | "webpack": "4.27.1",
54 | "webpack-cli": "3.1.2"
55 | },
56 | "engines": {
57 | "node": "10.3.0"
58 | },
59 | "devDependencies": {
60 | "@types/jest": "^23.3.10",
61 | "jest": "^23.6.0",
62 | "jest-cli": "^23.6.0",
63 | "pegjs": "^0.10.0",
64 | "ts-jest": "^23.10.5",
65 | "tslint": "^6.1.3"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/client/about.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Study openings! • About
8 |
9 |
10 |
11 |
12 |
13 |
19 |
20 |
21 |
What is StudyOpenings?
22 |
StudyOpenings is a free, open-sourced tool to help chess players memorize opening repertoires.
23 |
24 |
Where's the source code?
25 |
https://github.com/jven/studyopenings
26 |
Looking to contribute? Send a pull request!
27 |
28 |
I want to donate!
29 |
Thanks so much! You can support me on Patreon here: https://www.patreon.com/studyopenings.
30 |
Of course, this is entirely optional. StudyOpenings.com will always be free and open-sourced and there will never be any "premium" features.
31 |
32 |
I have another question.
33 |
Send an e-mail to jven@jvenezue.la.
34 |
35 |
36 |
--------------------------------------------------------------------------------
/src/client/css/about.css:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: var(--dark-gray-bg);
3 | background-image: var(--dark-gray-gradient);
4 | color: var(--light-gray-fg);
5 | font-family: 'Open Sans', sans-serif;
6 | }
7 |
8 | #title {
9 | float: left;
10 | font-family: 'Niramit', 'Open Sans', sans-serif;
11 | font-size: 26px;
12 | font-weight: 700;
13 | text-shadow: 3px 3px 2px rgba(0, 0, 0, 1);
14 | }
15 |
16 | #title > a {
17 | color: var(--light-gray-fg);
18 | text-decoration: none;
19 | }
20 |
21 | #headerContainer {
22 | margin: 0 auto;
23 | padding: 10px 0;
24 | max-width: 800px;
25 | width: 90%;
26 | }
27 |
28 | #logo {
29 | position: relative;
30 | width: 60px;
31 | }
32 |
33 | #header {
34 | border-bottom: var(--light-gray-border);
35 | float: right;
36 | font-size: 14px;
37 | height: 40px;
38 | text-align: right;
39 | width: calc(100% - 80px);
40 | }
41 |
42 | #content {
43 | background-color: var(--light-gray-bg);
44 | border: var(--very-dark-gray-border);
45 | font-size: 14px;
46 | margin: 0 auto;
47 | max-width: 600px;
48 | padding: 15px;
49 | width: 90%;
50 | }
51 |
52 | #content h1 {
53 | font-size: 20px;
54 | margin: 20px 0 10px 0;
55 | padding: 0;
56 | text-shadow: 0 2px 0 black;
57 | }
58 |
59 | #content p {
60 | margin: 10px 0;
61 | }
62 |
63 | #content a {
64 | color: var(--link-blue-fg);
65 | text-decoration: none;
66 | }
67 |
--------------------------------------------------------------------------------
/src/client/css/board.css:
--------------------------------------------------------------------------------
1 | /** Blue */
2 |
3 | #blueBoardThemeButton,
4 | .blueBoardTheme {
5 | background-image: url('/img/board/blue.svg');
6 | }
7 |
8 | .blueBoardThemePreview {
9 | background-image: url('/img/board/blue.svg') !important;
10 | }
11 |
12 | /** Brown */
13 |
14 | #brownBoardThemeButton,
15 | .brownBoardTheme {
16 | background-image: url('/img/board/brown.svg');
17 | }
18 |
19 | .brownBoardThemePreview {
20 | background-image: url('/img/board/brown.svg') !important;
21 | }
22 |
23 | /** Gray */
24 |
25 | #grayBoardThemeButton,
26 | .grayBoardTheme {
27 | background-image: url('/img/board/gray.jpg');
28 | }
29 |
30 | .grayBoardThemePreview {
31 | background-image: url('/img/board/gray.jpg') !important;
32 | }
33 |
34 | /** Green */
35 |
36 | #greenBoardThemeButton,
37 | .greenBoardTheme {
38 | background-image: url('/img/board/green.svg');
39 | }
40 |
41 | .greenBoardThemePreview {
42 | background-image: url('/img/board/green.svg') !important;
43 | }
44 |
45 | /** Leather */
46 |
47 | #leatherBoardThemeButton,
48 | .leatherBoardTheme {
49 | background-image: url('/img/board/leather.jpg');
50 | }
51 |
52 | .leatherBoardThemePreview {
53 | background-image: url('/img/board/leather.jpg') !important;
54 | }
55 |
56 | /** Marble */
57 |
58 | #marbleBoardThemeButton,
59 | .marbleBoardTheme {
60 | background-image: url('/img/board/marble.jpg');
61 | }
62 |
63 | .marbleBoardThemePreview {
64 | background-image: url('/img/board/marble.jpg') !important;
65 | }
66 |
67 | /** Purple */
68 |
69 | #purpleBoardThemeButton,
70 | .purpleBoardTheme {
71 | background-image: url('/img/board/purple.svg');
72 | }
73 |
74 | .purpleBoardThemePreview {
75 | background-image: url('/img/board/purple.svg') !important;
76 | }
77 |
78 | /** Wood1 */
79 |
80 | #wood1BoardThemeButton,
81 | .wood1BoardTheme {
82 | background-image: url('/img/board/wood1.jpg');
83 | }
84 |
85 | .wood1BoardThemePreview {
86 | background-image: url('/img/board/wood1.jpg') !important;
87 | }
88 |
89 | /** Wood2 */
90 |
91 | #wood2BoardThemeButton,
92 | .wood2BoardTheme {
93 | background-image: url('/img/board/wood2.jpg');
94 | }
95 |
96 | .wood2BoardThemePreview {
97 | background-image: url('/img/board/wood2.jpg') !important;
98 | }
99 |
100 | /** Wood3 */
101 |
102 | #wood3BoardThemeButton,
103 | .wood3BoardTheme {
104 | background-image: url('/img/board/wood3.jpg');
105 | }
106 |
107 | .wood3BoardThemePreview {
108 | background-image: url('/img/board/wood3.jpg') !important;
109 | }
110 |
--------------------------------------------------------------------------------
/src/client/css/chessground.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Chessground base css properties.
3 | *
4 | * You need to include the css files in themes folder in order to have the
5 | * board and pieces displayed!
6 | */
7 |
8 | .cg-wrap {
9 | width: 320px;
10 | height: 320px;
11 | position: relative;
12 | display: block;
13 | }
14 | cg-board {
15 | position: absolute;
16 | top: 0;
17 | left: 0;
18 | width: 100%;
19 | height: 100%;
20 | -webkit-user-select: none;
21 | -moz-user-select: none;
22 | -ms-user-select: none;
23 | user-select: none;
24 | line-height: 0;
25 | background-size: cover;
26 | cursor: pointer;
27 | }
28 | cg-board square {
29 | position: absolute;
30 | top: 0;
31 | left: 0;
32 | width: 12.5%;
33 | height: 12.5%;
34 | pointer-events: none;
35 | }
36 | cg-board square.move-dest {
37 | background: radial-gradient(rgba(20, 85, 30, 0.5) 22%, #208530 0, rgba(0, 0, 0, 0.3) 0, rgba(0, 0, 0, 0) 0);
38 | pointer-events: auto;
39 | }
40 | cg-board square.premove-dest {
41 | background: radial-gradient(rgba(20, 30, 85, 0.5) 22%, #203085 0, rgba(0, 0, 0, 0.3) 0, rgba(0, 0, 0, 0) 0);
42 | }
43 | cg-board square.oc.move-dest {
44 | background: radial-gradient(transparent 0%, transparent 80%, rgba(20, 85, 0, 0.3) 80%);
45 | }
46 | cg-board square.oc.premove-dest {
47 | background: radial-gradient(transparent 0%, transparent 80%, rgba(20, 30, 85, 0.2) 80%);
48 | }
49 | cg-board square.move-dest:hover {
50 | background: rgba(20, 85, 30, 0.3);
51 | }
52 | cg-board square.premove-dest:hover {
53 | background: rgba(20, 30, 85, 0.2);
54 | }
55 | cg-board square.last-move {
56 | will-change: transform;
57 | background-color: rgba(155, 199, 0, 0.41);
58 | }
59 | cg-board square.selected {
60 | background-color: rgba(20, 85, 30, 0.5);
61 | }
62 | cg-board square.check {
63 | background: radial-gradient(ellipse at center, rgba(255, 0, 0, 1) 0%, rgba(231, 0, 0, 1) 25%, rgba(169, 0, 0, 0) 89%, rgba(158, 0, 0, 0) 100%);
64 | }
65 | cg-board square.current-premove {
66 | background-color: rgba(20, 30, 85, 0.5);
67 | }
68 | .cg-wrap piece {
69 | position: absolute;
70 | top: 0;
71 | left: 0;
72 | width: 12.5%;
73 | height: 12.5%;
74 | background-size: cover;
75 | z-index: 2;
76 | will-change: transform;
77 | pointer-events: none;
78 | }
79 | cg-board piece.dragging {
80 | cursor: move;
81 | z-index: 9;
82 | }
83 | cg-board piece.anim {
84 | z-index: 8;
85 | }
86 | cg-board piece.fading {
87 | z-index: 1;
88 | opacity: 0.5;
89 | }
90 | .cg-wrap square.move-dest:hover {
91 | background-color: rgba(20, 85, 30, 0.3);
92 | }
93 | .cg-wrap piece.ghost {
94 | opacity: 0.3;
95 | }
96 | .cg-wrap svg {
97 | overflow: hidden;
98 | position: relative;
99 | top: 0px;
100 | left: 0px;
101 | width: 100%;
102 | height: 100%;
103 | pointer-events: none;
104 | z-index: 2;
105 | opacity: 0.6;
106 | }
107 | .cg-wrap svg image {
108 | opacity: 0.5;
109 | }
110 | .cg-wrap coords {
111 | position: absolute;
112 | display: flex;
113 | pointer-events: none;
114 | opacity: 0.8;
115 | font-size: 9px;
116 | }
117 | .cg-wrap coords.ranks {
118 | right: -15px;
119 | top: 0;
120 | flex-flow: column-reverse;
121 | height: 100%;
122 | width: 12px;
123 | }
124 | .cg-wrap coords.ranks.black {
125 | flex-flow: column;
126 | }
127 | .cg-wrap coords.files {
128 | bottom: -16px;
129 | left: 0;
130 | flex-flow: row;
131 | width: 100%;
132 | height: 16px;
133 | text-transform: uppercase;
134 | text-align: center;
135 | }
136 | .cg-wrap coords.files.black {
137 | flex-flow: row-reverse;
138 | }
139 | .cg-wrap coords coord {
140 | flex: 1 1 auto;
141 | }
142 | .cg-wrap coords.ranks coord {
143 | transform: translateY(39%);
144 | }
145 |
--------------------------------------------------------------------------------
/src/client/css/constants.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --dark-gray-bg: rgb(30, 30, 30);
3 | --light-gray-bg: rgb(40, 40, 40);
4 | --very-light-gray-bg: rgb(100, 100, 100);
5 | --empty-study-bg: rgba(30, 30, 30, 0.8);
6 | --gray-hover-bg: rgb(85, 85, 85);
7 | --blue-hover-bg: rgba(56, 147, 232, 0.4);
8 | --blue-selected-bg: rgb(56, 147, 232);
9 | --modal-gray-bg: #2b2b2b;
10 | --delete-red-bg: #bd3b31;
11 |
12 | --dark-gray-box-shadow: 0 0 4px #dfdfdf inset;
13 | --very-dark-gray-border: solid 1px rgb(50, 50, 50);
14 | --dark-gray-border: solid 1px #8f8f8f;
15 | --light-gray-border: solid 1px #dfdfdf;
16 | --tree-border: solid 1px #555555;
17 | --modal-box-shadow: 0 0 95px 25px rgba(0, 0, 0, 0.6);
18 |
19 | --dark-gray-gradient: linear-gradient(
20 | to bottom,
21 | rgb(50, 50, 50),
22 | rgb(30, 30, 30)
23 | 116px);
24 |
25 | --light-gray-fg: #dfdfdf;
26 | --dark-gray-fg: #8f8f8f;
27 | --link-blue-fg: #3893e8;
28 |
29 | --wrong-move-flash: red;
30 | --right-move-flash: rgb(155, 199, 0);
31 | --finish-line-flash: #ffd700;
32 |
33 | --warning-selected-node-bg: rgb(255, 255, 204);
34 |
35 | --transposition-node-bg: rgba(120, 120, 120, 0.4);
36 | --transposition-hover-node-bg: rgba(120, 120, 120, 0.7);
37 | --transposition-selected-node-bg: rgb(120, 120, 120);
38 |
39 | --warning-tooltip-bg: #ffc;
40 | }
--------------------------------------------------------------------------------
/src/client/css/pieces.css:
--------------------------------------------------------------------------------
1 | .cg-wrap piece.pawn.white {
2 | background-image: url('/img/pieces/merida/wP.svg');
3 | }
4 | .cg-wrap piece.bishop.white {
5 | background-image: url('/img/pieces/merida/wB.svg');
6 | }
7 | .cg-wrap piece.knight.white {
8 | background-image: url('/img/pieces/merida/wN.svg');
9 | }
10 | .cg-wrap piece.rook.white {
11 | background-image: url('/img/pieces/merida/wR.svg');
12 | }
13 | .cg-wrap piece.queen.white {
14 | background-image: url('/img/pieces/merida/wQ.svg');
15 | }
16 | .cg-wrap piece.king.white {
17 | background-image: url('/img/pieces/merida/wK.svg');
18 | }
19 | .cg-wrap piece.pawn.black {
20 | background-image: url('/img/pieces/merida/bP.svg');
21 | }
22 | .cg-wrap piece.bishop.black {
23 | background-image: url('/img/pieces/merida/bB.svg');
24 | }
25 | .cg-wrap piece.knight.black {
26 | background-image: url('/img/pieces/merida/bN.svg');
27 | }
28 | .cg-wrap piece.rook.black {
29 | background-image: url('/img/pieces/merida/bR.svg');
30 | }
31 | .cg-wrap piece.queen.black {
32 | background-image: url('/img/pieces/merida/bQ.svg');
33 | }
34 | .cg-wrap piece.king.black {
35 | background-image: url('/img/pieces/merida/bK.svg');
36 | }
37 |
--------------------------------------------------------------------------------
/src/client/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jven/studyopenings/55ac1b6ad98589744440c4677b9664e56c402db6/src/client/favicon.ico
--------------------------------------------------------------------------------
/src/client/img/board/blue.svg:
--------------------------------------------------------------------------------
1 |
2 |
20 |
--------------------------------------------------------------------------------
/src/client/img/board/brown.svg:
--------------------------------------------------------------------------------
1 |
2 |
20 |
--------------------------------------------------------------------------------
/src/client/img/board/gray.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jven/studyopenings/55ac1b6ad98589744440c4677b9664e56c402db6/src/client/img/board/gray.jpg
--------------------------------------------------------------------------------
/src/client/img/board/green.svg:
--------------------------------------------------------------------------------
1 |
2 |
20 |
--------------------------------------------------------------------------------
/src/client/img/board/leather.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jven/studyopenings/55ac1b6ad98589744440c4677b9664e56c402db6/src/client/img/board/leather.jpg
--------------------------------------------------------------------------------
/src/client/img/board/marble.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jven/studyopenings/55ac1b6ad98589744440c4677b9664e56c402db6/src/client/img/board/marble.jpg
--------------------------------------------------------------------------------
/src/client/img/board/purple.svg:
--------------------------------------------------------------------------------
1 |
2 |
20 |
--------------------------------------------------------------------------------
/src/client/img/board/wood1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jven/studyopenings/55ac1b6ad98589744440c4677b9664e56c402db6/src/client/img/board/wood1.jpg
--------------------------------------------------------------------------------
/src/client/img/board/wood2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jven/studyopenings/55ac1b6ad98589744440c4677b9664e56c402db6/src/client/img/board/wood2.jpg
--------------------------------------------------------------------------------
/src/client/img/board/wood3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jven/studyopenings/55ac1b6ad98589744440c4677b9664e56c402db6/src/client/img/board/wood3.jpg
--------------------------------------------------------------------------------
/src/client/img/export.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jven/studyopenings/55ac1b6ad98589744440c4677b9664e56c402db6/src/client/img/export.png
--------------------------------------------------------------------------------
/src/client/img/faviconbright.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jven/studyopenings/55ac1b6ad98589744440c4677b9664e56c402db6/src/client/img/faviconbright.ico
--------------------------------------------------------------------------------
/src/client/img/faviconbright2.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jven/studyopenings/55ac1b6ad98589744440c4677b9664e56c402db6/src/client/img/faviconbright2.ico
--------------------------------------------------------------------------------
/src/client/img/favicondark.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jven/studyopenings/55ac1b6ad98589744440c4677b9664e56c402db6/src/client/img/favicondark.ico
--------------------------------------------------------------------------------
/src/client/img/import.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jven/studyopenings/55ac1b6ad98589744440c4677b9664e56c402db6/src/client/img/import.png
--------------------------------------------------------------------------------
/src/client/img/left.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jven/studyopenings/55ac1b6ad98589744440c4677b9664e56c402db6/src/client/img/left.png
--------------------------------------------------------------------------------
/src/client/img/logobright.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jven/studyopenings/55ac1b6ad98589744440c4677b9664e56c402db6/src/client/img/logobright.png
--------------------------------------------------------------------------------
/src/client/img/logobright150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jven/studyopenings/55ac1b6ad98589744440c4677b9664e56c402db6/src/client/img/logobright150.png
--------------------------------------------------------------------------------
/src/client/img/logodark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jven/studyopenings/55ac1b6ad98589744440c4677b9664e56c402db6/src/client/img/logodark.png
--------------------------------------------------------------------------------
/src/client/img/logodark150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jven/studyopenings/55ac1b6ad98589744440c4677b9664e56c402db6/src/client/img/logodark150.png
--------------------------------------------------------------------------------
/src/client/img/pencil.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jven/studyopenings/55ac1b6ad98589744440c4677b9664e56c402db6/src/client/img/pencil.png
--------------------------------------------------------------------------------
/src/client/img/pieces/merida/bB.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/client/img/pieces/merida/bN.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/client/img/pieces/merida/bP.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/client/img/pieces/merida/bQ.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/client/img/pieces/merida/bR.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/client/img/pieces/merida/wB.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/client/img/pieces/merida/wK.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/client/img/pieces/merida/wN.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/client/img/pieces/merida/wP.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/client/img/pieces/merida/wQ.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/client/img/pieces/merida/wR.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/client/img/plus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jven/studyopenings/55ac1b6ad98589744440c4677b9664e56c402db6/src/client/img/plus.png
--------------------------------------------------------------------------------
/src/client/img/soundoff.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jven/studyopenings/55ac1b6ad98589744440c4677b9664e56c402db6/src/client/img/soundoff.png
--------------------------------------------------------------------------------
/src/client/img/soundon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jven/studyopenings/55ac1b6ad98589744440c4677b9664e56c402db6/src/client/img/soundon.png
--------------------------------------------------------------------------------
/src/client/img/theme.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jven/studyopenings/55ac1b6ad98589744440c4677b9664e56c402db6/src/client/img/theme.png
--------------------------------------------------------------------------------
/src/client/img/trash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jven/studyopenings/55ac1b6ad98589744440c4677b9664e56c402db6/src/client/img/trash.png
--------------------------------------------------------------------------------
/src/client/img/x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jven/studyopenings/55ac1b6ad98589744440c4677b9664e56c402db6/src/client/img/x.png
--------------------------------------------------------------------------------
/src/client/js/annotation/annotationrenderer.ts:
--------------------------------------------------------------------------------
1 | export interface AnnotationRenderer {
2 | renderAnnotation(
3 | annotation: ANNOTATION, treeNodeElement: HTMLElement): void;
4 | }
5 |
--------------------------------------------------------------------------------
/src/client/js/annotation/annotator.ts:
--------------------------------------------------------------------------------
1 | import { Color } from '../../../protocol/color';
2 | import { FenToPgnMap } from '../tree/fentopgnmap';
3 | import { PgnToNodeMap } from '../tree/pgntonodemap';
4 | import { TreeNode } from '../tree/treenode';
5 |
6 | export interface Annotator {
7 | annotate(
8 | node: TreeNode,
9 | repertoireColor: Color,
10 | pgnToNode: PgnToNodeMap,
11 | fenToPgn: FenToPgnMap): Promise;
12 | }
13 |
--------------------------------------------------------------------------------
/src/client/js/annotation/noopannotationrenderer.ts:
--------------------------------------------------------------------------------
1 | import { AnnotationRenderer } from './annotationrenderer';
2 |
3 | export class NoOpAnnotationRenderer implements AnnotationRenderer {
4 | renderAnnotation(): void {}
5 | }
6 |
--------------------------------------------------------------------------------
/src/client/js/annotation/nullannotator.ts:
--------------------------------------------------------------------------------
1 | import { Annotator } from './annotator';
2 |
3 | export class NullAnnotator implements Annotator {
4 | annotate(): Promise {
5 | return Promise.resolve(null);
6 | }
7 |
8 | static INSTANCE: Annotator = new NullAnnotator();
9 | }
10 |
--------------------------------------------------------------------------------
/src/client/js/board/board.ts:
--------------------------------------------------------------------------------
1 | import { Color } from '../../../protocol/color';
2 |
3 | export interface Board {
4 | redraw(): void;
5 |
6 | setStateFromChess(chess: any): void;
7 |
8 | setInitialPositionImmediately(): void;
9 |
10 | setOrientationForColor(color: Color): void;
11 |
12 | flashRightMove(): void;
13 |
14 | flashWrongMove(): void;
15 |
16 | flashFinishLine(): void;
17 |
18 | drawCircle(square: string, color: string): void;
19 |
20 | drawArrow(from: string, to: string, color: string): void;
21 |
22 | removeDrawings(): void;
23 | }
24 |
--------------------------------------------------------------------------------
/src/client/js/board/boardhandler.ts:
--------------------------------------------------------------------------------
1 | export interface BoardHandler {
2 | /**
3 | * Handles the user dragging a piece from one square to another, before the
4 | * board position is updated.
5 | */
6 | onMove(from: string, to: string): void;
7 |
8 | /** Handles the board position being updated. */
9 | onChange(): void;
10 |
11 | /** Handles the user scrolling on the board. */
12 | onScroll(e: WheelEvent): void;
13 | }
14 |
--------------------------------------------------------------------------------
/src/client/js/board/chessgroundboardfactory.ts:
--------------------------------------------------------------------------------
1 | import { assert } from '../../../util/assert';
2 | import { SoundPlayer } from '../sound/soundplayer';
3 | import { BoardHandler } from './boardhandler';
4 | import { ChessgroundBoard } from './chessgroundboard';
5 | import { DelegatingBoard } from './delegatingboard';
6 |
7 | export class ChessgroundBoardFactory {
8 | private soundPlayer_: SoundPlayer;
9 | private boardEls_: HTMLElement[];
10 |
11 | constructor(soundPlayer: SoundPlayer) {
12 | this.soundPlayer_ = soundPlayer;
13 | this.boardEls_ = [];
14 | }
15 |
16 | createBoardAndSetDelegate(
17 | delegatingBoard: DelegatingBoard,
18 | boardId: string,
19 | handler: BoardHandler,
20 | viewOnly: boolean): void {
21 | const boardEl = assert(document.getElementById(boardId));
22 | const chessgroundBoard = new ChessgroundBoard(
23 | boardEl,
24 | handler,
25 | this.soundPlayer_,
26 | viewOnly);
27 |
28 | delegatingBoard.setDelegate(chessgroundBoard);
29 | this.boardEls_.push(boardEl);
30 | }
31 |
32 | getBoardElements(): HTMLElement[] {
33 | return this.boardEls_;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/client/js/board/delegatingboard.ts:
--------------------------------------------------------------------------------
1 | import { Color } from '../../../protocol/color';
2 | import { Board } from './board';
3 | import { NoOpBoard } from './noopboard';
4 |
5 | export class DelegatingBoard implements Board {
6 | private delegate_: Board;
7 |
8 | constructor() {
9 | this.delegate_ = new NoOpBoard();
10 | }
11 |
12 | setDelegate(delegate: Board): void {
13 | this.delegate_ = delegate;
14 | }
15 |
16 | redraw(): void {
17 | this.delegate_.redraw();
18 | }
19 |
20 | setStateFromChess(chess: any): void {
21 | this.delegate_.setStateFromChess(chess);
22 | }
23 |
24 | setInitialPositionImmediately(): void {
25 | this.delegate_.setInitialPositionImmediately();
26 | }
27 |
28 | setOrientationForColor(color: Color): void {
29 | this.delegate_.setOrientationForColor(color);
30 | }
31 |
32 | flashRightMove(): void {
33 | this.delegate_.flashRightMove();
34 | }
35 |
36 | flashWrongMove(): void {
37 | this.delegate_.flashWrongMove();
38 | }
39 |
40 | flashFinishLine(): void {
41 | this.delegate_.flashFinishLine();
42 | }
43 |
44 | drawCircle(square: string, color: string): void {
45 | this.delegate_.drawCircle(square, color);
46 | }
47 |
48 | drawArrow(from: string, to: string, color: string): void {
49 | this.delegate_.drawArrow(from, to, color);
50 | }
51 |
52 | removeDrawings(): void {
53 | this.delegate_.removeDrawings();
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/client/js/board/noopboard.ts:
--------------------------------------------------------------------------------
1 | import { Color } from '../../../protocol/color';
2 | import { Board } from './board';
3 |
4 | export class NoOpBoard implements Board {
5 | redraw(): void {}
6 |
7 | setStateFromChess(chess: any): void {}
8 |
9 | setInitialPositionImmediately(): void {}
10 |
11 | setOrientationForColor(color: Color): void {}
12 |
13 | flashRightMove(): void {}
14 |
15 | flashWrongMove(): void {}
16 |
17 | flashFinishLine(): void {}
18 |
19 | drawCircle(square: string, color: string): void {}
20 |
21 | drawArrow(from: string, to: string, color: string): void {}
22 |
23 | removeDrawings(): void {}
24 | }
25 |
--------------------------------------------------------------------------------
/src/client/js/build/annotation/buildannotation.ts:
--------------------------------------------------------------------------------
1 | import { DisplayType } from './displaytype';
2 |
3 | export interface BuildAnnotation {
4 | displayType: DisplayType,
5 | title: string,
6 | content: string
7 | }
8 |
--------------------------------------------------------------------------------
/src/client/js/build/annotation/defaultannotationrenderer.ts:
--------------------------------------------------------------------------------
1 | import { assert } from '../../../../util/assert';
2 | import { AnnotationRenderer } from '../../annotation/annotationrenderer';
3 | import { BuildAnnotation } from './buildannotation';
4 | import { DisplayType } from './displaytype';
5 |
6 | declare let tippy: any;
7 |
8 | enum Classes {
9 | TRANSPOSITION_NODE = 'transpositionNode',
10 | WARNING_NODE = 'warningNode'
11 | }
12 |
13 | export class DefaultAnnotationRenderer
14 | implements AnnotationRenderer {
15 | renderAnnotation(
16 | annotation: BuildAnnotation | null,
17 | cell: HTMLElement): void {
18 | if (!annotation) {
19 | return;
20 | }
21 | if (annotation.displayType == DisplayType.WARNING) {
22 | // Indicate warnings.
23 | cell.classList.add(Classes.WARNING_NODE);
24 | const template = assert(
25 | document.getElementById('warningTooltipContentTemplate'));
26 | tippy(cell, {
27 | a11y: false,
28 | animateFill: false,
29 | animation: 'fade',
30 | content() {
31 | const content = document.createElement('div');
32 | content.innerHTML = template.innerHTML;
33 | const contentList =
34 | assert(content.querySelector('.warningTooltipContent-list'));
35 | const newElement = document.createElement('li');
36 | newElement.innerHTML = annotation.content;
37 | contentList.appendChild(newElement);
38 | return content;
39 | },
40 | delay: 0,
41 | duration: 0,
42 | placement: 'bottom',
43 | theme: 'warningTooltip'
44 | });
45 | } else if (annotation.displayType == DisplayType.INFORMATIONAL) {
46 | // Indicate transposition.
47 | cell.classList.add(Classes.TRANSPOSITION_NODE);
48 | const template = assert(document.getElementById(
49 | 'transpositionTooltipContentTemplate'));
50 | tippy(cell, {
51 | a11y: false,
52 | animateFill: false,
53 | animation: 'fade',
54 | content() {
55 | const content = document.createElement('div');
56 | content.innerHTML = template.innerHTML;
57 | assert(content.querySelector('.transpositionTooltipContent-title'))
58 | .innerHTML = assert(annotation).title;
59 | assert(content.querySelector('.transpositionTooltipContent-body'))
60 | .innerHTML = assert(annotation).content;
61 | return content;
62 | },
63 | delay: 0,
64 | duration: 0,
65 | placement: 'bottom',
66 | theme: 'transpositionTooltip'
67 | });
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/client/js/build/annotation/displaytype.ts:
--------------------------------------------------------------------------------
1 | export enum DisplayType {
2 | WARNING,
3 | INFORMATIONAL
4 | }
5 |
--------------------------------------------------------------------------------
/src/client/js/build/buildboardhandler.ts:
--------------------------------------------------------------------------------
1 | import { NullAnnotator } from '../annotation/nullannotator';
2 | import { BoardHandler } from '../board/boardhandler';
3 | import { Config } from '../common/config';
4 | import { RefreshableView } from '../common/refreshableview';
5 | import { Toasts } from '../common/toasts';
6 | import { AddMoveFailureReason } from '../tree/addmoveresult';
7 | import { TreeModel } from '../tree/treemodel';
8 | import { TreeNavigator } from '../tree/treenavigator';
9 | import { CurrentRepertoireUpdater } from './currentrepertoireupdater';
10 |
11 | export class BuildBoardHandler implements BoardHandler {
12 | private treeModel_: TreeModel;
13 | private treeNavigator_: TreeNavigator;
14 | private modeView_: RefreshableView;
15 | private updater_: CurrentRepertoireUpdater;
16 |
17 | constructor(
18 | treeModel: TreeModel,
19 | treeNavigator: TreeNavigator,
20 | modeView: RefreshableView,
21 | updater: CurrentRepertoireUpdater) {
22 | this.treeModel_ = treeModel;
23 | this.treeNavigator_ = treeNavigator;
24 | this.modeView_ = modeView;
25 | this.updater_ = updater;
26 | }
27 |
28 | onMove(fromSquare: string, toSquare: string): void {
29 | let pgn = this.treeModel_.getSelectedViewInfo(NullAnnotator.INSTANCE).pgn;
30 | const result = this.treeModel_.addMove(pgn, {fromSquare, toSquare});
31 | if (result.success && !result.failureReason) {
32 | this.updater_.updateCurrentRepertoire();
33 | return;
34 | }
35 |
36 | switch (result.failureReason) {
37 | case AddMoveFailureReason.ILLEGAL_MOVE:
38 | Toasts.warning('Couldn\'t add move', 'That move is illegal.');
39 | break;
40 | case AddMoveFailureReason.EXCEEDED_MAXIMUM_LINE_DEPTH:
41 | Toasts.warning(
42 | 'Couldn\'t add move',
43 | `Opening lines can\'t be longer than `
44 | + `${Config.MAXIMUM_LINE_DEPTH_IN_PLY} ply.`);
45 | break;
46 | case AddMoveFailureReason.EXCEEDED_MAXIMUM_NUM_NODES:
47 | Toasts.warning(
48 | 'Couldn\'t add move',
49 | `Repertoires can\'t contain more than `
50 | + `${Config.MAXIMUM_TREE_NODES_PER_REPERTOIRE} total moves.`);
51 | break;
52 | default:
53 | throw new Error(`Unknown failure reason: ${result.failureReason}`);
54 | }
55 | }
56 |
57 | onChange(): void {
58 | this.modeView_.refresh();
59 | }
60 |
61 | onScroll(e: WheelEvent): void {
62 | this.treeNavigator_.selectFromWheelEvent(e);
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/client/js/build/colorchooser.ts:
--------------------------------------------------------------------------------
1 | import { Color } from '../../../protocol/color';
2 | import { ImpressionCode } from '../../../protocol/impression/impressioncode';
3 | import { RefreshableView } from '../common/refreshableview';
4 | import { ImpressionSender } from '../impressions/impressionsender';
5 | import { TreeModel } from '../tree/treemodel';
6 | import { CurrentRepertoireUpdater } from './currentrepertoireupdater';
7 |
8 | enum Classes {
9 | SELECTED_COLOR = 'selectedColor'
10 | }
11 |
12 | export class ColorChooser implements RefreshableView {
13 | private whiteButton_: HTMLElement;
14 | private blackButton_: HTMLElement;
15 | private impressionSender_: ImpressionSender;
16 | private treeModel_: TreeModel;
17 | private modeView_: RefreshableView;
18 | private updater_: CurrentRepertoireUpdater;
19 |
20 | constructor(
21 | whiteButton: HTMLElement,
22 | blackButton: HTMLElement,
23 | impressionSender: ImpressionSender,
24 | treeModel: TreeModel,
25 | modeView: RefreshableView,
26 | updater: CurrentRepertoireUpdater) {
27 | this.whiteButton_ = whiteButton;
28 | this.blackButton_ = blackButton;
29 | this.impressionSender_ = impressionSender;
30 | this.treeModel_ = treeModel;
31 | this.modeView_ = modeView;
32 | this.updater_ = updater;
33 |
34 | whiteButton.onclick = () => this.handleClick_(Color.WHITE);
35 | blackButton.onclick = () => this.handleClick_(Color.BLACK);
36 | }
37 |
38 | refresh(): void {
39 | const color = this.treeModel_.getRepertoireColor();
40 | this.whiteButton_.classList.toggle(
41 | Classes.SELECTED_COLOR, color == Color.WHITE);
42 | this.blackButton_.classList.toggle(
43 | Classes.SELECTED_COLOR, color == Color.BLACK);
44 | }
45 |
46 | private handleClick_(color: Color): void {
47 | this.impressionSender_.sendImpression(
48 | ImpressionCode.TREE_SET_REPERTOIRE_COLOR, {color});
49 | this.treeModel_.setRepertoireColor(color);
50 | this.modeView_.refresh();
51 | this.updater_.updateCurrentRepertoire();
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/client/js/build/currentrepertoireexporter.ts:
--------------------------------------------------------------------------------
1 | import { getUtcDate, getUtcTime } from '../../../util/datetime';
2 | import { TreeModel } from '../tree/treemodel';
3 |
4 | export class CurrentRepertoireExporter {
5 | private treeModel_: TreeModel;
6 |
7 | constructor(treeModel: TreeModel) {
8 | this.treeModel_ = treeModel;
9 | }
10 |
11 | exportCurrentRepertoire(): void {
12 | const now = new Date();
13 | const linkEl = document.createElement('a');
14 | linkEl.style.display = 'none';
15 | linkEl.download = this.getExportFilename_(now);
16 |
17 | const contents = this.getExportContents_(now);
18 | linkEl.href = `data:application/x-chess-pgn,${contents}`;
19 |
20 | document.body.appendChild(linkEl);
21 | linkEl.click();
22 | document.body.removeChild(linkEl);
23 | }
24 |
25 | private getExportContents_(now: Date): string {
26 | const tags = this.getExportTags_(now);
27 | let tagsString = '';
28 | for (const key in tags) {
29 | tagsString += `[${key} "${tags[key]}"]\n`;
30 | }
31 |
32 | const moves = this.treeModel_.exportToPgn();
33 | return encodeURIComponent(`${tagsString}\n${moves}`);
34 | }
35 |
36 | private getExportTags_(now: Date): {[key: string]: string} {
37 | return {
38 | 'Event': this.treeModel_.getRepertoireName(),
39 | 'Site': 'http://studyopenings.com',
40 | 'UTCDate': getUtcDate(now),
41 | 'UTCTime': getUtcTime(now),
42 | 'Result': '*'
43 | };
44 | }
45 |
46 | private getExportFilename_(now: Date): string {
47 | const name = this.treeModel_.getRepertoireName();
48 | const formattedName = name.toLocaleLowerCase()
49 | .trim()
50 | .replace(/[^a-z0-9\s]+/g, '')
51 | .replace(/\s+/g, '-');
52 | const utcDate = getUtcDate(now);
53 |
54 | return `studyopenings_${formattedName}_${utcDate}.pgn`;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/client/js/build/currentrepertoireupdater.ts:
--------------------------------------------------------------------------------
1 | import { PickerController } from '../picker/pickercontroller';
2 | import { ServerWrapper } from '../server/serverwrapper';
3 | import { TreeModel } from '../tree/treemodel';
4 |
5 | export class CurrentRepertoireUpdater {
6 | private server_: ServerWrapper;
7 | private pickerController_: PickerController;
8 | private treeModel_: TreeModel;
9 |
10 | constructor(
11 | server: ServerWrapper,
12 | pickerController: PickerController,
13 | treeModel: TreeModel) {
14 | this.server_ = server;
15 | this.pickerController_ = pickerController;
16 | this.treeModel_ = treeModel;
17 | }
18 |
19 | updateCurrentRepertoire(): Promise {
20 | const repertoireId = this.pickerController_.getSelectedMetadataId();
21 | const repertoire = this.treeModel_.serializeAsRepertoire();
22 | return this.server_.updateRepertoire(repertoireId, repertoire);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/client/js/build/examplerepertoirehandler.ts:
--------------------------------------------------------------------------------
1 | import { ImpressionCode } from '../../../protocol/impression/impressioncode';
2 | import { RefreshableView } from '../common/refreshableview';
3 | import { ImpressionSender } from '../impressions/impressionsender';
4 | import { PickerController } from '../picker/pickercontroller';
5 | import { TreeModel } from '../tree/treemodel';
6 | import { CurrentRepertoireUpdater } from './currentrepertoireupdater';
7 | import { ExampleRepertoires } from './examplerepertoires';
8 |
9 | export class ExampleRepertoireHandler {
10 | private impressionSender_: ImpressionSender;
11 | private treeModel_: TreeModel;
12 | private modeView_: RefreshableView;
13 | private pickerController_: PickerController;
14 | private updater_: CurrentRepertoireUpdater;
15 |
16 | constructor(
17 | impressionSender: ImpressionSender,
18 | treeModel: TreeModel,
19 | modeView: RefreshableView,
20 | pickerController: PickerController,
21 | updater: CurrentRepertoireUpdater) {
22 | this.impressionSender_ = impressionSender;
23 | this.treeModel_ = treeModel;
24 | this.modeView_ = modeView;
25 | this.pickerController_ = pickerController;
26 | this.updater_ = updater;
27 | }
28 |
29 | handleButtonClicks(exampleRepertoireElement: HTMLElement): void {
30 | exampleRepertoireElement.onclick = this.handleClick_.bind(this);
31 | }
32 |
33 | private handleClick_(): void {
34 | this.impressionSender_.sendImpression(
35 | ImpressionCode.LOAD_EXAMPLE_REPERTOIRE);
36 | const exampleJson = JSON.parse(ExampleRepertoires.KINGS_GAMBIT);
37 | this.treeModel_.loadRepertoire(exampleJson);
38 | this.modeView_.refresh();
39 |
40 | this.updater_.updateCurrentRepertoire()
41 | .then(() => this.pickerController_.updatePicker());
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/client/js/build/import/converterstatus.ts:
--------------------------------------------------------------------------------
1 | export class ConverterStatus {
2 | private wasAnyLongLineTruncated_: boolean;
3 | private wasMaximumNumNodesReached_: boolean;
4 | private label_: string;
5 | private errors_: string[];
6 |
7 | constructor() {
8 | this.wasAnyLongLineTruncated_ = false;
9 | this.wasMaximumNumNodesReached_ = false;
10 | this.label_ = '';
11 | this.errors_ = [];
12 | }
13 |
14 | getLabel(): string {
15 | return this.label_;
16 | }
17 |
18 | setLabel(label: string): void {
19 | this.label_ = label;
20 | }
21 |
22 | markLongLineTruncated(): void {
23 | this.wasAnyLongLineTruncated_ = true;
24 | }
25 |
26 | wasAnyLongLineTruncated(): boolean {
27 | return this.wasAnyLongLineTruncated_;
28 | }
29 |
30 | markMaximumNumNodesReached(): void {
31 | this.wasMaximumNumNodesReached_ = true;
32 | }
33 |
34 | wasMaximumNumNodesReached(): boolean {
35 | return this.wasMaximumNumNodesReached_;
36 | }
37 |
38 | addError(error: string): void {
39 | this.errors_.push(error);
40 | }
41 |
42 | getErrors(): string[] {
43 | return this.errors_;
44 | }
45 | }
46 |
47 |
--------------------------------------------------------------------------------
/src/client/js/build/import/currentrepertoireimporter.ts:
--------------------------------------------------------------------------------
1 | import { RefreshableView } from '../../common/refreshableview';
2 | import { PickerController } from '../../picker/pickercontroller';
3 | import { TreeModel } from '../../tree/treemodel';
4 | import { CurrentRepertoireUpdater } from '../currentrepertoireupdater';
5 | import { ImportDialog } from './importdialog';
6 | import { PgnImporter } from './pgnimporter';
7 | import { PgnImportProgress } from './pgnimportprogress';
8 |
9 | export class CurrentRepertoireImporter {
10 | private importDialog_: ImportDialog;
11 | private treeModel_: TreeModel;
12 | private modeView_: RefreshableView;
13 | private pickerController_: PickerController;
14 | private updater_: CurrentRepertoireUpdater;
15 |
16 | private currentProgress_: PgnImportProgress | null;
17 |
18 | constructor(
19 | importDialog: ImportDialog,
20 | treeModel: TreeModel,
21 | modeView: RefreshableView,
22 | pickerController: PickerController,
23 | updater: CurrentRepertoireUpdater) {
24 | this.importDialog_ = importDialog;
25 | this.treeModel_ = treeModel;
26 | this.modeView_ = modeView;
27 | this.pickerController_ = pickerController;
28 | this.updater_ = updater;
29 |
30 | this.currentProgress_ = null;
31 | }
32 |
33 | startPgnImport(pgn: string): void {
34 | if (this.currentProgress_) {
35 | throw new Error('An import is already in progress!');
36 | }
37 |
38 | this.currentProgress_ = PgnImporter.startPgnImport(pgn);
39 | this.currentProgress_
40 | .getCompletionPromise()
41 | .then(repertoire => {
42 | this.importDialog_.hide();
43 | this.treeModel_.loadRepertoire(repertoire);
44 | this.modeView_.refresh();
45 |
46 | this.updater_.updateCurrentRepertoire().then(
47 | () => this.pickerController_.updatePicker());
48 | this.currentProgress_ = null;
49 | })
50 | .catch(err => {
51 | this.importDialog_.hideProgress();
52 | this.currentProgress_ = null;
53 | });
54 |
55 | this.maybeUpdateProgressText_();
56 | }
57 |
58 | cancelCurrentProgress(): void {
59 | if (this.currentProgress_) {
60 | this.importDialog_.hideProgress();
61 | this.currentProgress_.cancel();
62 | this.currentProgress_ = null;
63 | }
64 | }
65 |
66 | private maybeUpdateProgressText_(): void {
67 | if (!this.currentProgress_) {
68 | return;
69 | }
70 |
71 | this.importDialog_.showProgress(this.currentProgress_.getStatusString());
72 | setTimeout(() => this.maybeUpdateProgressText_(), 500);
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/client/js/build/import/importdialog.ts:
--------------------------------------------------------------------------------
1 | import { ImpressionCode } from '../../../../protocol/impression/impressioncode';
2 | import { Toasts } from '../../common/toasts';
3 | import { ImpressionSender } from '../../impressions/impressionsender';
4 | import { CurrentRepertoireImporter } from './currentrepertoireimporter';
5 |
6 | export class ImportDialog {
7 | private impressionSender_: ImpressionSender;
8 | private dialogEl_: HTMLElement;
9 | private textAreaEl_: HTMLTextAreaElement;
10 | private uploadEl_: HTMLInputElement;
11 | private okButtonEl_: HTMLElement;
12 | private progressEl_: HTMLElement;
13 | private importer_: CurrentRepertoireImporter | null;
14 |
15 | constructor(
16 | impressionSender: ImpressionSender,
17 | dialogEl: HTMLElement,
18 | textAreaEl: HTMLTextAreaElement,
19 | uploadEl: HTMLInputElement,
20 | okButtonEl: HTMLElement,
21 | cancelButtonEl: HTMLElement,
22 | progressEl: HTMLElement) {
23 | this.impressionSender_ = impressionSender;
24 | this.dialogEl_ = dialogEl;
25 | this.textAreaEl_ = textAreaEl;
26 | this.uploadEl_ = uploadEl;
27 | this.okButtonEl_ = okButtonEl;
28 | this.progressEl_ = progressEl;
29 | this.importer_ = null;
30 |
31 | textAreaEl.oninput = () => this.onTextAreaInput_();
32 | uploadEl.onchange = () => this.onUpload_();
33 | okButtonEl.onclick = () => this.onOkClick_();
34 | cancelButtonEl.onclick = () => this.onCancelClick_();
35 | }
36 |
37 | setImporter(importer: CurrentRepertoireImporter): void {
38 | this.importer_ = importer;
39 | }
40 |
41 | isVisible(): boolean {
42 | return !this.dialogEl_.classList.contains('hidden');
43 | }
44 |
45 | show() {
46 | this.hideProgress();
47 | this.dialogEl_.classList.remove('hidden');
48 | // Initialize the state of the OK button.
49 | this.onTextAreaInput_();
50 | }
51 |
52 | hide() {
53 | this.dialogEl_.classList.add('hidden');
54 | }
55 |
56 | showProgress(progressText: string): void {
57 | this.textAreaEl_.setAttribute('disabled', 'disabled');
58 | this.okButtonEl_.classList.add('disabled');
59 | this.okButtonEl_.classList.remove('selectable');
60 | this.progressEl_.classList.remove('hidden');
61 | this.progressEl_.innerText = progressText;
62 | }
63 |
64 | hideProgress(): void {
65 | this.textAreaEl_.removeAttribute('disabled');
66 | this.okButtonEl_.classList.remove('disabled');
67 | this.okButtonEl_.classList.add('selectable');
68 | this.progressEl_.classList.add('hidden');
69 | }
70 |
71 | onKeyDown(e: KeyboardEvent): void {
72 | if (e.keyCode == 27) {
73 | this.hide(); // Esc
74 | }
75 | }
76 |
77 | private onTextAreaInput_(): void {
78 | const isTextAreaEmpty = !this.textAreaEl_.value;
79 | this.okButtonEl_.classList.toggle('disabled', isTextAreaEmpty);
80 | this.okButtonEl_.classList.toggle('selectable', !isTextAreaEmpty);
81 | }
82 |
83 | private onUpload_(): void {
84 | const files = this.uploadEl_.files;
85 | if (!files || !files.length) {
86 | return;
87 | }
88 |
89 | const fileToRead = files[0];
90 | const fileReader = new FileReader();
91 | fileReader.onload = (readEvent: any) => {
92 | this.textAreaEl_.value = readEvent.target.result;
93 | };
94 | fileReader.onerror = () => {
95 | Toasts.error(
96 | 'Couldn\'t load PGN file',
97 | `There was a problem loading '${fileToRead.name}'. Make sure this is `
98 | + 'a valid PGN file.');
99 | };
100 | fileReader.readAsText(fileToRead);
101 | }
102 |
103 | private onOkClick_(): void {
104 | if (!this.importer_) {
105 | throw new Error('No importer!');
106 | }
107 | if (!this.okButtonEl_.classList.contains('disabled')) {
108 | const pgnToImport = this.textAreaEl_.value;
109 | this.impressionSender_.sendImpression(
110 | ImpressionCode.START_PGN_IMPORT,
111 | {importedPgn: pgnToImport});
112 | this.importer_.startPgnImport(pgnToImport);
113 | }
114 | }
115 |
116 | private onCancelClick_(): void {
117 | if (!this.importer_) {
118 | throw new Error('No importer!');
119 | }
120 | this.importer_.cancelCurrentProgress();
121 | this.hide();
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/src/client/js/build/import/pgnimporter.ts:
--------------------------------------------------------------------------------
1 | import { Repertoire } from '../../../../protocol/storage';
2 | import { Config } from '../../common/config';
3 | import { Toasts } from '../../common/toasts';
4 | import { ConverterStatus } from './converterstatus';
5 | import { PgnImportProgress } from './pgnimportprogress';
6 | import { RepertoireIncrementalConverter } from './repertoireincrementalconverter';
7 |
8 | export class PgnImporter {
9 | static startPgnImport(pgn: string): PgnImportProgress {
10 | const status = new ConverterStatus();
11 | const converter = new RepertoireIncrementalConverter(pgn, status);
12 | const progress = new FinishablePgnImportProgress(status);
13 |
14 | setTimeout(() => this.doModeWork_(progress, converter, status), 0);
15 | return progress;
16 | }
17 |
18 | private static doModeWork_(
19 | progress: FinishablePgnImportProgress,
20 | converter: RepertoireIncrementalConverter,
21 | status: ConverterStatus): void {
22 | if (progress.isComplete()) {
23 | return;
24 | }
25 |
26 | if (converter.isComplete()) {
27 | PgnImporter.maybeShowInfoToasts_(status);
28 | progress.markFinished(converter.getRepertoire());
29 | return;
30 | }
31 |
32 | const errors = status.getErrors();
33 | if (errors.length) {
34 | errors.forEach(e => Toasts.error('Error importing PGN.', e));
35 | progress.cancel();
36 | return;
37 | }
38 |
39 | converter.doIncrementalWork();
40 | setTimeout(() => PgnImporter.doModeWork_(progress, converter, status), 0);
41 | }
42 |
43 | private static maybeShowInfoToasts_(status: ConverterStatus): void {
44 | if (status.wasAnyLongLineTruncated()) {
45 | Toasts.info(
46 | 'Some lines were shortened',
47 | `Opening lines can\'t be longer than `
48 | + `${Config.MAXIMUM_LINE_DEPTH_IN_PLY} ply.`);
49 | }
50 | if (status.wasMaximumNumNodesReached()) {
51 | Toasts.info(
52 | 'Some moves were not imported',
53 | `Repertoires can\'t contain more than `
54 | + `${Config.MAXIMUM_TREE_NODES_PER_REPERTOIRE} total moves.`);
55 | }
56 | }
57 | }
58 |
59 | class FinishablePgnImportProgress implements PgnImportProgress {
60 | private status_: ConverterStatus;
61 | private promise_: Promise;
62 | private resolveFn_: (repertoire: Repertoire) => void;
63 | private rejectFn_: () => void;
64 | private completed_: boolean;
65 |
66 | constructor(status: ConverterStatus) {
67 | this.status_ = status;
68 | this.resolveFn_ = () => {};
69 | this.rejectFn_ = () => {};
70 | this.promise_ = new Promise((resolve, reject) => {
71 | this.resolveFn_ = resolve;
72 | this.rejectFn_ = reject;
73 | });
74 | this.completed_ = false;
75 | }
76 |
77 | getStatusString(): string {
78 | return this.status_.getLabel();
79 | }
80 |
81 | getCompletionPromise(): Promise {
82 | return this.promise_;
83 | }
84 |
85 | isComplete(): boolean {
86 | return this.completed_;
87 | }
88 |
89 | cancel(): void {
90 | if (this.completed_) {
91 | throw new Error('Import has already completed.');
92 | }
93 |
94 | this.rejectFn_();
95 | this.completed_ = true;
96 | }
97 |
98 | markFinished(repertoire: Repertoire): void {
99 | if (this.completed_) {
100 | throw new Error('Import has already completed.');
101 | }
102 |
103 | this.resolveFn_(repertoire);
104 | this.completed_ = true;
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/client/js/build/import/pgnimportprogress.ts:
--------------------------------------------------------------------------------
1 | import { Repertoire } from '../../../../protocol/storage';
2 |
3 | export interface PgnImportProgress {
4 | getStatusString(): string;
5 |
6 | getCompletionPromise(): Promise;
7 |
8 | cancel(): void;
9 | }
10 |
--------------------------------------------------------------------------------
/src/client/js/build/import/pgnparser.ts:
--------------------------------------------------------------------------------
1 | import * as pgnparser from '../../lib/pgnparser';
2 |
3 | export interface ParsedVariation {
4 | moves: ParsedNode[]
5 | }
6 |
7 | export interface ParsedNode {
8 | move: string,
9 | ravs?: ParsedVariation[]
10 | }
11 |
12 | export class PgnParser {
13 | static parse(pgn: string): ParsedVariation[] {
14 | if (!pgn) {
15 | throw new Error('PGN is empty.');
16 | }
17 | const result = pgnparser.parse(pgn);
18 | if (!result || !result.length) {
19 | throw new Error('Unknown parsing error.');
20 | }
21 | return result;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/client/js/build/import/repertoireincrementalconverter.ts:
--------------------------------------------------------------------------------
1 | import { Repertoire } from '../../../../protocol/storage';
2 | import { ConverterStatus } from './converterstatus';
3 | import { ParsedVariation, PgnParser } from './pgnparser';
4 | import { TreeModelPopulator } from './treemodelpopulator';
5 |
6 | /**
7 | * A class which incrementally converts a PGN string into a Repertoire object.
8 | *
9 | * This conversion is done by constructing this Converter with the PGN string to
10 | * convert then repeatedly calling #doIncrementalWork until #isComplete returns
11 | * true. At that point, #getRepertoire will return the resulting repertoire.
12 | *
13 | * Since PGN parsing and conversion is expensive, this flow is intended to allow
14 | * the caller to only perform the conversion work when there is nothing else to
15 | * do (e.g. handling user input).
16 | */
17 | export class RepertoireIncrementalConverter {
18 | private pgn_: string;
19 | private status_: ConverterStatus;
20 | private parsedVariations_: ParsedVariation[] | null;
21 | private populator_: TreeModelPopulator | null;
22 | private repertoire_: Repertoire | null;
23 |
24 | constructor(pgn: string, status: ConverterStatus) {
25 | this.pgn_ = pgn;
26 | this.status_ = status;
27 | this.parsedVariations_ = null;
28 | this.populator_ = null;
29 | this.repertoire_ = null;
30 |
31 | status.setLabel('Parsing PGN...');
32 | }
33 |
34 | doIncrementalWork(): void {
35 | if (this.repertoire_) {
36 | throw new Error('Already done generating!');
37 | }
38 | if (!this.parsedVariations_) {
39 | try {
40 | this.parsedVariations_ = PgnParser.parse(this.pgn_);
41 | } catch (e) {
42 | let message = e.message;
43 | if (e.location && e.location.start) {
44 | const l = e.location.start;
45 | message = `At line ${l.line}, column ${l.column}: ${message}`;
46 | }
47 | this.status_.addError(message);
48 | }
49 | return;
50 | }
51 | if (!this.populator_) {
52 | this.populator_ = new TreeModelPopulator(
53 | this.parsedVariations_, this.status_);
54 | return;
55 | }
56 | if (!this.populator_.isComplete()) {
57 | this.populator_.doIncrementalWork();
58 | this.status_.setLabel(`Imported ${this.populator_.numPopulatedMoves()} / `
59 | + `${this.populator_.numTotalMoves()} moves...`);
60 | return;
61 | }
62 |
63 | const treeModel = this.populator_.getPopulatedTreeModel();
64 | this.repertoire_ = treeModel.serializeAsRepertoire();
65 | }
66 |
67 | isComplete(): boolean {
68 | return !!this.repertoire_;
69 | }
70 |
71 | getRepertoire(): Repertoire {
72 | if (!this.repertoire_) {
73 | throw new Error('Not done generating yet!');
74 | }
75 |
76 | return this.repertoire_;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/client/js/build/renameinput.ts:
--------------------------------------------------------------------------------
1 | import { Debouncer } from '../common/debouncer';
2 | import { RefreshableView } from '../common/refreshableview';
3 | import { PickerController } from '../picker/pickercontroller';
4 | import { TreeModel } from '../tree/treemodel';
5 | import { CurrentRepertoireUpdater } from './currentrepertoireupdater';
6 |
7 | const UPDATE_DEBOUNCE_INTERVAL_MS_: number = 1000;
8 |
9 | export class RenameInput implements RefreshableView {
10 | private renameInputElement_: HTMLInputElement;
11 | private treeModel_: TreeModel;
12 | private pickerController_: PickerController;
13 | private updater_: CurrentRepertoireUpdater;
14 | private updateDebouncer_: Debouncer;
15 |
16 | constructor(
17 | renameInputElement: HTMLInputElement,
18 | treeModel: TreeModel,
19 | pickerController: PickerController,
20 | updater: CurrentRepertoireUpdater) {
21 | this.renameInputElement_ = renameInputElement;
22 | this.treeModel_ = treeModel;
23 | this.pickerController_ = pickerController;
24 | this.updater_ = updater;
25 | this.updateDebouncer_ = new Debouncer(
26 | () => this.update_(), UPDATE_DEBOUNCE_INTERVAL_MS_);
27 |
28 | this.renameInputElement_.oninput = () => this.onInputChange_();
29 | }
30 |
31 | refresh(): void {
32 | this.renameInputElement_.value = this.treeModel_.getRepertoireName();
33 | }
34 |
35 | isFocused(): boolean {
36 | return this.renameInputElement_ == document.activeElement;
37 | }
38 |
39 | private onInputChange_(): void {
40 | if (!this.renameInputElement_.value) {
41 | this.renameInputElement_.value = 'Untitled repertoire';
42 | }
43 |
44 | this.treeModel_.setRepertoireName(this.renameInputElement_.value);
45 | this.updateDebouncer_.fire();
46 | }
47 |
48 | private update_(): void {
49 | this.updater_.updateCurrentRepertoire()
50 | .then(() => this.pickerController_.updatePicker());
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/client/js/build/treecontroller.ts:
--------------------------------------------------------------------------------
1 | import { ImpressionCode } from '../../../protocol/impression/impressioncode';
2 | import { RefreshableView } from '../common/refreshableview';
3 | import { ImpressionSender } from '../impressions/impressionsender';
4 | import { TreeModel } from '../tree/treemodel';
5 | import { CurrentRepertoireExporter } from './currentrepertoireexporter';
6 | import { CurrentRepertoireUpdater } from './currentrepertoireupdater';
7 |
8 | export class TreeController {
9 | private impressionSender_: ImpressionSender;
10 | private treeModel_: TreeModel;
11 | private modeView_: RefreshableView;
12 | private updater_: CurrentRepertoireUpdater;
13 | private exporter_: CurrentRepertoireExporter;
14 |
15 | constructor(
16 | impressionSender: ImpressionSender,
17 | treeModel: TreeModel,
18 | modeView: RefreshableView,
19 | updater: CurrentRepertoireUpdater,
20 | exporter: CurrentRepertoireExporter) {
21 | this.impressionSender_ = impressionSender;
22 | this.treeModel_ = treeModel;
23 | this.modeView_ = modeView;
24 | this.updater_ = updater;
25 | this.exporter_ = exporter;
26 | }
27 |
28 | flipRepertoireColor(): void {
29 | this.impressionSender_.sendImpression(
30 | ImpressionCode.TREE_FLIP_REPERTOIRE_COLOR);
31 | this.treeModel_.flipRepertoireColor();
32 | this.modeView_.refresh();
33 | this.updater_.updateCurrentRepertoire();
34 | }
35 |
36 | trash(): void {
37 | if (!this.treeModel_.canRemoveSelectedPgn()) {
38 | return;
39 | }
40 |
41 | this.impressionSender_.sendImpression(ImpressionCode.TREE_TRASH_SELECTED);
42 | this.treeModel_.removeSelectedPgn();
43 | this.modeView_.refresh();
44 | this.updater_.updateCurrentRepertoire();
45 | }
46 |
47 | export(): void {
48 | this.impressionSender_.sendImpression(ImpressionCode.PGN_EXPORT);
49 | this.exporter_.exportCurrentRepertoire();
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/client/js/common/config.ts:
--------------------------------------------------------------------------------
1 | export const Config = {
2 | AUTH0_AUDIENCE: 'studyopenings-api',
3 | AUTH0_DOMAIN: 'studyopenings.auth0.com',
4 | AUTH0_CLIENT_ID: '4N3xZuND2puazbhvLdpLpp17Bv19Fn0g',
5 |
6 | OPPONENT_FIRST_MOVE_DELAY_MS: 600,
7 | OPPONENT_REPLY_DELAY_MS: 600,
8 |
9 | WRONG_MOVES_FOR_ANSWER: 2,
10 | WRONG_MOVES_FOR_HINT: 1,
11 |
12 | MAXIMUM_LINE_DEPTH_IN_PLY: 80,
13 | MAXIMUM_TREE_NODES_PER_REPERTOIRE: 5000,
14 | };
15 |
--------------------------------------------------------------------------------
/src/client/js/common/debouncer.test.ts:
--------------------------------------------------------------------------------
1 | import { Debouncer } from './debouncer';
2 |
3 | let callbackFn: () => void;
4 | let debouncer: Debouncer;
5 |
6 | const DEBOUNCE_INTERVAL_MS = 10;
7 |
8 | beforeEach(() => {
9 | jest.useFakeTimers();
10 | callbackFn = jest.fn();
11 | debouncer = new Debouncer(callbackFn, DEBOUNCE_INTERVAL_MS);
12 | });
13 |
14 | it('no fire should not call callback', () => {
15 | jest.advanceTimersByTime(DEBOUNCE_INTERVAL_MS * 2);
16 | expect(callbackFn).toBeCalledTimes(0);
17 | });
18 |
19 | it('fire should call callback immediately and only once', () => {
20 | debouncer.fire();
21 | expect(callbackFn).toBeCalledTimes(1);
22 |
23 | jest.advanceTimersByTime(DEBOUNCE_INTERVAL_MS * 2);
24 | expect(callbackFn).toBeCalledTimes(1);
25 | });
26 |
27 | it('fire twice slowly should call twice immediately', () => {
28 | debouncer.fire();
29 | jest.advanceTimersByTime(DEBOUNCE_INTERVAL_MS * 2);
30 | debouncer.fire();
31 | expect(callbackFn).toBeCalledTimes(2);
32 |
33 | jest.advanceTimersByTime(DEBOUNCE_INTERVAL_MS * 2);
34 | expect(callbackFn).toBeCalledTimes(2);
35 | });
36 |
37 | it('fire twice immediately should call once immediately, once delayed', () => {
38 | debouncer.fire();
39 | debouncer.fire();
40 | expect(callbackFn).toBeCalledTimes(1);
41 |
42 | jest.advanceTimersByTime(DEBOUNCE_INTERVAL_MS * 2);
43 | expect(callbackFn).toBeCalledTimes(2);
44 |
45 | jest.advanceTimersByTime(DEBOUNCE_INTERVAL_MS * 2);
46 | expect(callbackFn).toBeCalledTimes(2);
47 | });
48 |
49 | it('fire twice quickly should call once immediately, once delayed', () => {
50 | debouncer.fire();
51 | jest.advanceTimersByTime(DEBOUNCE_INTERVAL_MS - 1);
52 | debouncer.fire();
53 | expect(callbackFn).toBeCalledTimes(1);
54 |
55 | jest.advanceTimersByTime(DEBOUNCE_INTERVAL_MS * 2);
56 | expect(callbackFn).toBeCalledTimes(2);
57 |
58 | jest.advanceTimersByTime(DEBOUNCE_INTERVAL_MS * 2);
59 | expect(callbackFn).toBeCalledTimes(2);
60 | });
61 |
62 | it('fire many times quickly should call once immediately, once delayed', () => {
63 | const numQuickFires = 100;
64 | for (let i = 0; i < numQuickFires; i++) {
65 | debouncer.fire();
66 | }
67 | expect(callbackFn).toBeCalledTimes(1);
68 |
69 | jest.advanceTimersByTime(DEBOUNCE_INTERVAL_MS * 2);
70 | expect(callbackFn).toBeCalledTimes(2);
71 |
72 | jest.advanceTimersByTime(DEBOUNCE_INTERVAL_MS * 2);
73 | expect(callbackFn).toBeCalledTimes(2);
74 | });
75 |
76 | it('fire many times slowly should space out calls', () => {
77 | const numLoops = 100;
78 | for (let i = 0; i < numLoops; i++) {
79 | debouncer.fire();
80 | jest.advanceTimersByTime(1);
81 | debouncer.fire();
82 | jest.advanceTimersByTime(DEBOUNCE_INTERVAL_MS * 2);
83 | }
84 |
85 | expect(callbackFn).toBeCalledTimes(numLoops * 2);
86 | });
87 |
--------------------------------------------------------------------------------
/src/client/js/common/debouncer.ts:
--------------------------------------------------------------------------------
1 | export class Debouncer {
2 | private callbackFn_: () => void;
3 | private debounceIntervalMs_: number;
4 | private currentTimeout_: number | null;
5 | private firedDuringTimeout_: boolean;
6 |
7 | constructor(callbackFn: () => void, debounceIntervalMs: number) {
8 | this.callbackFn_ = callbackFn;
9 | this.debounceIntervalMs_ = debounceIntervalMs;
10 | this.currentTimeout_ = null;
11 | this.firedDuringTimeout_ = false;
12 | }
13 |
14 | fire(): void {
15 | if (this.currentTimeout_ != null) {
16 | this.firedDuringTimeout_ = true;
17 | return;
18 | }
19 |
20 | this.callbackFn_();
21 | this.firedDuringTimeout_ = false;
22 | this.currentTimeout_ = window.setTimeout(
23 | () => this.maybeFireAfterTimeout_(), this.debounceIntervalMs_);
24 | }
25 |
26 | private maybeFireAfterTimeout_(): void {
27 | this.currentTimeout_ = null;
28 | if (!this.firedDuringTimeout_) {
29 | return;
30 | }
31 |
32 | this.firedDuringTimeout_ = false;
33 | this.fire();
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/client/js/common/listrefreshableview.ts:
--------------------------------------------------------------------------------
1 | import { RefreshableView } from './refreshableview';
2 |
3 | export class ListRefreshableView implements RefreshableView {
4 | private views_: RefreshableView[];
5 |
6 | constructor() {
7 | this.views_ = [];
8 | }
9 |
10 | addView(view: RefreshableView): void {
11 | this.views_.push(view);
12 | }
13 |
14 | refresh(): void {
15 | this.views_.forEach(v => v.refresh());
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/client/js/common/refreshableview.ts:
--------------------------------------------------------------------------------
1 | export interface RefreshableView {
2 | refresh(): void;
3 | }
4 |
--------------------------------------------------------------------------------
/src/client/js/common/toasts.ts:
--------------------------------------------------------------------------------
1 | export class Toasts {
2 | static initialize() {
3 | toastr.options.showDuration = 300;
4 | toastr.options.hideDuration = 300;
5 | toastr.options.timeOut = 0;
6 | toastr.options.extendedTimeOut = 0;
7 | toastr.options.preventDuplicates = true;
8 | toastr.options.positionClass = 'toast-top-center';
9 | }
10 |
11 | static error(title: string, message: string) {
12 | toastr.error(message, title);
13 | }
14 |
15 | static warning(title: string, message: string) {
16 | toastr.warning(message, title);
17 | }
18 |
19 | static info(title: string, message: string) {
20 | toastr.info(message, title);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/client/js/common/tooltips.ts:
--------------------------------------------------------------------------------
1 | declare var tippy: any;
2 |
3 | export class Tooltips {
4 | static initialize() {
5 | const tooltipElements = document.querySelectorAll('[data-tippy-content]');
6 | tooltipElements.forEach(e => Tooltips.addTo_(e));
7 | }
8 |
9 | private static addTo_(element: Element) {
10 | tippy(element, {
11 | a11y: false,
12 | arrow: true,
13 | delay: [10, 20],
14 | animation: 'shift-away'
15 | });
16 | }
17 |
18 | static hideAll() {
19 | tippy.hideAllPoppers();
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/client/js/common/viewinfo.ts:
--------------------------------------------------------------------------------
1 | import { Color } from '../../../protocol/color';
2 | import { Move } from '../../../protocol/move';
3 |
4 | export interface ViewInfo {
5 | position: string,
6 | pgn: string,
7 | parentPgn: string | null,
8 | numLegalMoves: number,
9 | colorToMove: Color,
10 | lastMove: Move | null,
11 | lastMoveString: string,
12 | lastMoveVerboseString: string,
13 | lastMovePly: number,
14 | lastMoveNumber: number,
15 | lastMoveColor: Color,
16 | numChildren: number,
17 | childPgns: string[],
18 | childMoves: Move[],
19 | isSelected: boolean,
20 | annotationPromise: Promise
21 | }
22 |
--------------------------------------------------------------------------------
/src/client/js/evaluate/annotation/statisticannotation.ts:
--------------------------------------------------------------------------------
1 | export interface StatisticAnnotation {
2 | rightMoveCount: number,
3 | wrongMoveCount: number
4 | }
5 |
--------------------------------------------------------------------------------
/src/client/js/evaluate/annotation/statisticannotationrenderer.ts:
--------------------------------------------------------------------------------
1 | import { AnnotationRenderer } from '../../annotation/annotationrenderer';
2 | import { StatisticAnnotation } from './statisticannotation';
3 |
4 | export class StatisticAnnotationRenderer implements
5 | AnnotationRenderer {
6 | renderAnnotation(
7 | annotation: StatisticAnnotation, treeNodeElement: HTMLElement): void {
8 | const totalCount = annotation.rightMoveCount + annotation.wrongMoveCount;
9 | if (!totalCount) {
10 | return;
11 | }
12 |
13 | const rightRatio = annotation.rightMoveCount / totalCount;
14 | const rightRed = 155;
15 | const rightGreen = 199;
16 | const wrongRed = 255;
17 | const wrongGreen = 0;
18 | const red = Math.floor(
19 | rightRatio * rightRed + (1 - rightRatio) * wrongRed);
20 | const green = Math.floor(
21 | rightRatio * rightGreen + (1 - rightRatio) * wrongGreen);
22 | const color = `rgba(${red}, ${green}, 0, 0.8)`;
23 |
24 | treeNodeElement.style.backgroundColor = color;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/client/js/evaluate/annotation/statisticannotator.ts:
--------------------------------------------------------------------------------
1 | import { Color } from '../../../../protocol/color';
2 | import { Annotator } from '../../annotation/annotator';
3 | import { StatisticsModel } from '../../statistics/statisticsmodel';
4 | import { FenToPgnMap } from '../../tree/fentopgnmap';
5 | import { PgnToNodeMap } from '../../tree/pgntonodemap';
6 | import { TreeNode } from '../../tree/treenode';
7 | import { StatisticAnnotation } from './statisticannotation';
8 |
9 | export class StatisticAnnotator implements Annotator {
10 | private statisticsModel_: StatisticsModel;
11 |
12 | constructor(statisticsModel: StatisticsModel) {
13 | this.statisticsModel_ = statisticsModel;
14 | }
15 |
16 | annotate(
17 | node: TreeNode,
18 | repertoireColor: Color,
19 | pgnToNode: PgnToNodeMap,
20 | fenToPgn: FenToPgnMap): Promise {
21 | return Promise
22 | .all([
23 | this.statisticsModel_.getRightMoveCount(node.pgn),
24 | this.statisticsModel_.getWrongMoveCount(node.pgn)
25 | ])
26 | .then(counts => {
27 | return {
28 | rightMoveCount: counts[0],
29 | wrongMoveCount: counts[1]
30 | };
31 | });
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/client/js/evaluate/childmovedrawer.ts:
--------------------------------------------------------------------------------
1 | import { NullAnnotator } from '../annotation/nullannotator';
2 | import { Board } from '../board/board';
3 | import { RefreshableView } from '../common/refreshableview';
4 | import { TreeModel } from '../tree/treemodel';
5 |
6 | export class ChildMoveDrawer implements RefreshableView {
7 | private treeModel_: TreeModel;
8 | private board_: Board;
9 |
10 | constructor(treeModel: TreeModel, board: Board) {
11 | this.treeModel_ = treeModel;
12 | this.board_ = board;
13 | }
14 |
15 | refresh(): void {
16 | this.board_.removeDrawings();
17 | const selectedViewInfo
18 | = this.treeModel_.getSelectedViewInfo(NullAnnotator.INSTANCE);
19 | selectedViewInfo.childMoves.forEach(m =>
20 | this.board_.drawArrow(m.fromSquare, m.toSquare, 'green'));
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/client/js/evaluate/evaluateboardhandler.ts:
--------------------------------------------------------------------------------
1 | import { BoardHandler } from '../board/boardhandler';
2 | import { TreeNavigator } from '../tree/treenavigator';
3 |
4 | export class EvaluateBoardHandler implements BoardHandler {
5 | private treeNavigator_: TreeNavigator;
6 |
7 | constructor(treeNavigator: TreeNavigator) {
8 | this.treeNavigator_ = treeNavigator;
9 | }
10 |
11 | onMove(from: string, to: string): void {}
12 |
13 | onChange(): void {}
14 |
15 | onScroll(e: WheelEvent): void {
16 | this.treeNavigator_.selectFromWheelEvent(e);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/client/js/evaluate/insights/insight.ts:
--------------------------------------------------------------------------------
1 | export interface Insight {
2 | title: string,
3 | value: Promise,
4 | color: string
5 | }
6 |
--------------------------------------------------------------------------------
/src/client/js/evaluate/insights/insightcalculator.ts:
--------------------------------------------------------------------------------
1 | import { NullAnnotator } from '../../annotation/nullannotator';
2 | import { StatisticsModel } from '../../statistics/statisticsmodel';
3 | import { TreeModel } from '../../tree/treemodel';
4 | import { Insight } from './insight';
5 |
6 | enum InsightColor {
7 | GRAY = '#dfdfdf',
8 | YELLOW = '#ffd700',
9 | GREEN = 'rgb(155, 199, 0)',
10 | RED = 'red'
11 | }
12 |
13 | export class InsightCalculator {
14 | private treeModel_: TreeModel;
15 | private statisticsModel_: StatisticsModel;
16 |
17 | constructor(
18 | treeModel: TreeModel,
19 | statisticsModel: StatisticsModel) {
20 | this.treeModel_ = treeModel;
21 | this.statisticsModel_ = statisticsModel;
22 | }
23 |
24 | calculate(): (Insight | string)[] {
25 | let lineCount = 0;
26 | let positionCount = 0;
27 | this.treeModel_.traverseDepthFirst(viewInfo => {
28 | positionCount++;
29 | if (!viewInfo.numChildren) {
30 | lineCount++;
31 | }
32 | }, NullAnnotator.INSTANCE);
33 |
34 | return [
35 | 'In this repertoire...',
36 | {
37 | title: 'Number of lines',
38 | value: Promise.resolve(lineCount),
39 | color: InsightColor.GRAY
40 | },
41 | {
42 | title: 'Number of positions',
43 | value: Promise.resolve(positionCount),
44 | color: InsightColor.GRAY
45 | },
46 | {
47 | title: 'Times you finished studying a line',
48 | value: this.statisticsModel_.getRepertoireFinishLineCount(),
49 | color: InsightColor.YELLOW
50 | },
51 | {
52 | title: 'Times you played a correct move while studying',
53 | value: this.statisticsModel_.getRepertoireRightMoveCount(),
54 | color: InsightColor.GREEN
55 | },
56 | {
57 | title: 'Times you played a wrong move while studying',
58 | value: this.statisticsModel_.getRepertoireWrongMoveCount(),
59 | color: InsightColor.RED
60 | },
61 | 'For the selected position...',
62 | {
63 | title: 'Times you played the correct move',
64 | value: this.statisticsModel_.getRightMoveCount(
65 | this.treeModel_.getSelectedViewInfo(NullAnnotator.INSTANCE).pgn),
66 | color: InsightColor.GREEN
67 | },
68 | {
69 | title: 'Times you played the wrong move',
70 | value: this.statisticsModel_.getWrongMoveCount(
71 | this.treeModel_.getSelectedViewInfo(NullAnnotator.INSTANCE).pgn),
72 | color: InsightColor.RED
73 | }
74 | ];
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/client/js/evaluate/insights/insightspanel.ts:
--------------------------------------------------------------------------------
1 | import { RefreshableView } from '../../common/refreshableview';
2 | import { Insight } from './insight';
3 | import { InsightCalculator } from './insightcalculator';
4 |
5 | enum CssClass {
6 | INSIGHT = 'insight',
7 | INSIGHT_TITLE = 'insightTitle',
8 | INSIGHT_VALUE = 'insightValue',
9 | INSIGHT_LABEL = 'insightLabel'
10 | }
11 |
12 | export class InsightsPanel implements RefreshableView {
13 | private insightsEl_: HTMLElement;
14 | private calculator_: InsightCalculator;
15 |
16 | constructor(
17 | insightsEl: HTMLElement,
18 | calculator: InsightCalculator) {
19 | this.insightsEl_ = insightsEl;
20 | this.calculator_ = calculator;
21 | }
22 |
23 | refresh(): void {
24 | this.insightsEl_.innerHTML = '';
25 | this.calculator_.calculate().forEach(e => this.handleElement_(e));
26 | }
27 |
28 | private handleElement_(e: Insight | string): void {
29 | typeof e == 'string'
30 | ? this.handleInsightLabel_(e)
31 | : this.handleInsight_(e);
32 | }
33 |
34 | private handleInsight_(insight: Insight): void {
35 | const titleEl = document.createElement('div');
36 | titleEl.classList.add(CssClass.INSIGHT_TITLE);
37 | titleEl.innerText = insight.title;
38 | titleEl.style.color = insight.color;
39 | titleEl.style.borderColor = insight.color;
40 |
41 | const valueEl = document.createElement('div');
42 | valueEl.classList.add(CssClass.INSIGHT_VALUE);
43 | valueEl.innerText = '...';
44 | valueEl.style.color = insight.color;
45 | insight.value.then(v => { valueEl.innerText = `${v}`; });
46 |
47 | const insightEl = document.createElement('div');
48 | insightEl.classList.add(CssClass.INSIGHT);
49 | insightEl.appendChild(titleEl);
50 | insightEl.appendChild(valueEl);
51 |
52 | this.insightsEl_.appendChild(insightEl);
53 | }
54 |
55 | private handleInsightLabel_(insightLabel: string): void {
56 | const labelEl = document.createElement('div');
57 | labelEl.classList.add(CssClass.INSIGHT_LABEL);
58 | labelEl.innerText = insightLabel;
59 |
60 | this.insightsEl_.appendChild(labelEl);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/client/js/evaluate/repertoirenamelabel.ts:
--------------------------------------------------------------------------------
1 | import { RefreshableView } from '../common/refreshableview';
2 | import { TreeModel } from '../tree/treemodel';
3 |
4 | export class RepertoireNameLabel implements RefreshableView {
5 | private labelEl_: HTMLElement;
6 | private treeModel_: TreeModel;
7 |
8 | constructor(labelEl: HTMLElement, treeModel: TreeModel) {
9 | this.labelEl_ = labelEl;
10 | this.treeModel_ = treeModel;
11 | }
12 |
13 | refresh(): void {
14 | this.labelEl_.innerText = this.treeModel_.getRepertoireName();
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/client/js/footer/footerlinks.ts:
--------------------------------------------------------------------------------
1 | import { ImpressionCode } from '../../../protocol/impression/impressioncode';
2 | import { ImpressionSender } from '../impressions/impressionsender';
3 |
4 | export class FooterLinks {
5 | static logImpressionsForClicks(
6 | impressionSender: ImpressionSender,
7 | aboutLinkEl: HTMLElement,
8 | sourceCodeLinkEl: HTMLElement): void {
9 | aboutLinkEl.onclick = () => impressionSender.sendImpression(
10 | ImpressionCode.OPEN_ABOUT_PAGE);
11 | sourceCodeLinkEl.onclick = () => impressionSender.sendImpression(
12 | ImpressionCode.OPEN_SOURCE_CODE);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/client/js/impressions/debouncingimpressionsender.ts:
--------------------------------------------------------------------------------
1 | import { LogImpressionsRequest } from '../../../protocol/actions';
2 | import { ExtraData } from '../../../protocol/impression/extradata';
3 | import { Impression } from '../../../protocol/impression/impression';
4 | import { ImpressionCode } from '../../../protocol/impression/impressioncode';
5 | import { AuthManager } from '../auth/authmanager';
6 | import { Debouncer } from '../common/debouncer';
7 | import { ImpressionSender } from './impressionsender';
8 |
9 | export class DebouncingImpressionSender implements ImpressionSender {
10 | private impressionSessionId_: string;
11 | private authManager_: AuthManager;
12 | private impressionsToSend_: Impression[];
13 | private debouncer_: Debouncer;
14 |
15 | constructor(
16 | impressionSessionId: string,
17 | authManager: AuthManager,
18 | debounceIntervalMs: number) {
19 | this.impressionSessionId_ = impressionSessionId;
20 | this.authManager_ = authManager;
21 | this.impressionsToSend_ = [];
22 | this.debouncer_ = new Debouncer(
23 | () => this.sendImpressions_(), debounceIntervalMs);
24 | }
25 |
26 | sendImpression(impressionCode: ImpressionCode, extraData?: ExtraData): void {
27 | const sessionInfo = this.authManager_.getSessionInfo();
28 | const user = sessionInfo ? sessionInfo.userId : '(anonymous)';
29 | const impression: Impression = {
30 | impressionCode: impressionCode,
31 | user: user,
32 | timestampMs: Date.now(),
33 | sessionId: this.impressionSessionId_,
34 | userAgent: navigator.userAgent,
35 | extraData: extraData || {}
36 | };
37 |
38 | this.impressionsToSend_.push(impression);
39 | this.debouncer_.fire();
40 | }
41 |
42 | private sendImpressions_(): void {
43 | const options = {
44 | method: 'POST',
45 | headers: {
46 | 'Content-Type': 'application/json'
47 | },
48 | body: JSON.stringify(this.makeRequest_())
49 | };
50 |
51 | // Errors are intentionally not handled: impressions are not critical so
52 | // failures are tolerated gracefully by the client.
53 | fetch('/impressions', options).catch(() => { });
54 |
55 | this.impressionsToSend_ = [];
56 | }
57 |
58 | private makeRequest_(): LogImpressionsRequest {
59 | return {impressions: this.impressionsToSend_};
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/client/js/impressions/impressionsender.ts:
--------------------------------------------------------------------------------
1 | import { ExtraData } from '../../../protocol/impression/extradata';
2 | import { ImpressionCode } from '../../../protocol/impression/impressioncode';
3 |
4 | export interface ImpressionSender {
5 | sendImpression(impressionCode: ImpressionCode): void,
6 | sendImpression(impressionCode: ImpressionCode, extraData: ExtraData): void;
7 | }
8 |
--------------------------------------------------------------------------------
/src/client/js/mode/mode.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * A single mode of the application.
3 | *
4 | * The application has exactly one selected mode at any given time which handles
5 | * user input, receives events from the picker, etc.. At any point, the
6 | * application may exit the currently selected mode and enter a different mode,
7 | * by which process the newly entered mode becomes selected.
8 | */
9 | export interface Mode {
10 | /**
11 | * Performs any logic necessary before the mode is entered and before the
12 | * currently selected mode is exited.
13 | *
14 | * Returns a promise which is fulfilled when the mode is ready to be selected.
15 | */
16 | preEnter(): Promise;
17 |
18 | /**
19 | * Performs any logic necessary before entering another mode.
20 | *
21 | * Returns a promise which is fulfilled when the mode is ready to be
22 | * unselected.
23 | */
24 | exit(): Promise;
25 |
26 | /**
27 | * Performs any logic necessary after the mode is entered and after the
28 | * previously selected mode is exited.
29 | *
30 | * Returns a promise which is fulfilled when the mode is finished being
31 | * selected.
32 | */
33 | postEnter(): Promise;
34 |
35 | /**
36 | * Handles the given key event when the mode is selected.
37 | */
38 | onKeyDown(e: KeyboardEvent): void;
39 |
40 | /** Notifies the mode that a new repertoire metadata has been selected. */
41 | notifySelectedMetadata(): Promise;
42 | }
43 |
--------------------------------------------------------------------------------
/src/client/js/mode/modemanager.test.ts:
--------------------------------------------------------------------------------
1 | import { Mode } from './mode';
2 | import { ModeManager } from './modemanager';
3 |
4 | const MockMode = jest.fn(() => ({
5 | preEnter: jest.fn(() => Promise.resolve()),
6 | exit: jest.fn(() => Promise.resolve()),
7 | postEnter: jest.fn(() => Promise.resolve()),
8 | onKeyDown: jest.fn(),
9 | notifySelectedMetadata: jest.fn(() => Promise.resolve())
10 | }));
11 |
12 | describe('register', () => {
13 | test('register twice throws', () => {
14 | const modeManager = new ModeManager();
15 | modeManager.registerMode('mode', new MockMode());
16 | expect(() => modeManager.registerMode('mode', new MockMode()))
17 | .toThrow();
18 | });
19 |
20 | test('register does not select', () => {
21 | const modeManager = new ModeManager();
22 | const mode = new MockMode();
23 | modeManager.registerMode('mode', mode);
24 | expect(mode.preEnter).toHaveBeenCalledTimes(0);
25 | expect(mode.exit).toHaveBeenCalledTimes(0);
26 | expect(mode.postEnter).toHaveBeenCalledTimes(0);
27 | });
28 | });
29 |
30 | describe('select', () => {
31 | test('get selected without selecting throws', () => {
32 | const modeManager = new ModeManager();
33 | modeManager.registerMode('mode', new MockMode());
34 | expect(() => modeManager.getSelectedMode()).toThrow();
35 | });
36 |
37 | test('select unregistered mode throws', () => {
38 | const modeManager = new ModeManager();
39 | modeManager.registerMode('mode1', new MockMode());
40 | expect(() => modeManager.selectModeType('mode2'))
41 | .toThrow();
42 | });
43 |
44 | test('select single mode', () => {
45 | const modeManager = new ModeManager();
46 | const mode = new MockMode();
47 | modeManager.registerMode('mode', mode);
48 | return modeManager.selectModeType('mode')
49 | .then(() => {
50 | expect(modeManager.getSelectedMode()).toBe(mode);
51 |
52 | expect(mode.preEnter).toHaveBeenCalledTimes(1);
53 | expect(mode.exit).toHaveBeenCalledTimes(0);
54 | expect(mode.postEnter).toHaveBeenCalledTimes(1);
55 | });
56 | });
57 |
58 | test('switch modes', () => {
59 | const modeManager = new ModeManager();
60 | const mode1 = new MockMode();
61 | const mode2 = new MockMode();
62 | modeManager
63 | .registerMode('mode1', mode1)
64 | .registerMode('mode2', mode2);
65 | return modeManager.selectModeType('mode1')
66 | .then(() => modeManager.selectModeType('mode2'))
67 | .then(() => {
68 | expect(modeManager.getSelectedMode()).toBe(mode2);
69 |
70 | expect(mode1.preEnter).toHaveBeenCalledTimes(1);
71 | expect(mode1.exit).toHaveBeenCalledTimes(1);
72 | expect(mode1.postEnter).toHaveBeenCalledTimes(1);
73 |
74 | expect(mode2.preEnter).toHaveBeenCalledTimes(1);
75 | expect(mode2.exit).toHaveBeenCalledTimes(0);
76 | expect(mode2.postEnter).toHaveBeenCalledTimes(1);
77 | });
78 | });
79 | });
80 |
--------------------------------------------------------------------------------
/src/client/js/mode/modemanager.ts:
--------------------------------------------------------------------------------
1 | import { Mode } from './mode';
2 |
3 | export class ModeManager {
4 | private modes_: Map;
5 | private selectedModeType_: string | null;
6 |
7 | constructor() {
8 | this.modes_ = new Map();
9 | this.selectedModeType_ = null;
10 | }
11 |
12 | registerMode(modeType: string, mode: Mode): ModeManager {
13 | if (this.modes_.has(modeType)) {
14 | throw new Error('Mode type registered twice!');
15 | }
16 | this.modes_.set(modeType, mode);
17 | return this;
18 | }
19 |
20 | selectModeType(modeType: string): Promise {
21 | if (modeType == this.selectedModeType_) {
22 | // This mode is already selected.
23 | return Promise.resolve();
24 | }
25 |
26 | const newMode = this.mode_(modeType);
27 | return newMode.preEnter()
28 | .then(() => this.selectedModeType_
29 | ? this.mode_(this.selectedModeType_).exit()
30 | : Promise.resolve())
31 | .then(() => newMode.postEnter())
32 | .then(() => {
33 | this.selectedModeType_ = modeType;
34 | });
35 | }
36 |
37 | getSelectedMode(): Mode {
38 | if (!this.selectedModeType_) {
39 | throw new Error('No mode selected yet.');
40 | }
41 |
42 | return this.mode_(this.selectedModeType_);
43 | }
44 |
45 | private mode_(modeType: string): Mode {
46 | const mode = this.modes_.get(modeType);
47 | if (!mode) {
48 | throw new Error('Unregistered mode type: ' + modeType);
49 | }
50 | return mode;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/client/js/mode/modetype.ts:
--------------------------------------------------------------------------------
1 | export enum ModeType {
2 | INITIAL = 'initialMode',
3 | BUILD = 'buildMode',
4 | STUDY = 'studyMode',
5 | EVALUATE = 'evaluateMode'
6 | }
7 |
--------------------------------------------------------------------------------
/src/client/js/mode/noopmode.ts:
--------------------------------------------------------------------------------
1 | import { Mode } from './mode';
2 |
3 | export class NoOpMode implements Mode {
4 | preEnter(): Promise {
5 | return Promise.resolve();
6 | }
7 |
8 | exit(): Promise {
9 | return Promise.resolve();
10 | }
11 |
12 | postEnter(): Promise {
13 | return Promise.resolve();
14 | }
15 | onKeyDown(e: KeyboardEvent): void {}
16 |
17 | notifySelectedMetadata(): Promise {
18 | return Promise.resolve();
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/client/js/picker/confirmdeletedialog.ts:
--------------------------------------------------------------------------------
1 | import { PickerController } from './pickercontroller';
2 |
3 | export class ConfirmDeleteDialog {
4 | private pickerController_: PickerController;
5 | private dialogEl_: HTMLElement;
6 | private dialogNameEl_: HTMLElement;
7 | private okButton_: HTMLElement;
8 | private repertoireIdToDelete_: string;
9 |
10 | constructor(
11 | pickerController: PickerController,
12 | dialogEl: HTMLElement,
13 | dialogNameEl: HTMLElement,
14 | okButton: HTMLElement,
15 | cancelButton: HTMLElement) {
16 | this.pickerController_ = pickerController;
17 | this.dialogEl_ = dialogEl;
18 | this.dialogNameEl_ = dialogNameEl;
19 | this.okButton_ = okButton;
20 | this.repertoireIdToDelete_ = '';
21 |
22 | cancelButton.onclick = () => this.hide_();
23 | }
24 |
25 | isVisible(): boolean {
26 | return !this.dialogEl_.classList.contains('hidden');
27 | }
28 |
29 | showForRepertoire(
30 | repertoireId: string,
31 | repertoireName: string): void {
32 | this.dialogNameEl_.innerText = repertoireName;
33 | this.dialogEl_.classList.remove('hidden');
34 | this.repertoireIdToDelete_ = repertoireId;
35 | this.okButton_.onclick = () => this.onOkClick_();
36 | }
37 |
38 | onKeyDown(e: KeyboardEvent): void {
39 | if (e.keyCode == 27) {
40 | this.hide_(); // Esc
41 | }
42 | }
43 |
44 | private onOkClick_(): void {
45 | this.pickerController_.deleteMetadataId(this.repertoireIdToDelete_)
46 | .then(() => this.hide_());
47 | }
48 |
49 | private hide_(): void {
50 | this.dialogEl_.classList.add('hidden');
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/client/js/picker/pickercontroller.ts:
--------------------------------------------------------------------------------
1 | import { ImpressionCode } from '../../../protocol/impression/impressioncode';
2 | import { Metadata } from '../../../protocol/storage';
3 | import { ImpressionSender } from '../impressions/impressionsender';
4 | import { ModeManager } from '../mode/modemanager';
5 | import { PickerModel } from '../picker/pickermodel';
6 | import { PickerView } from '../picker/pickerview';
7 | import { ServerWrapper } from '../server/serverwrapper';
8 |
9 | export class PickerController {
10 | private impressionSender_: ImpressionSender;
11 | private server_: ServerWrapper;
12 | private modeManager_: ModeManager;
13 | private model_: PickerModel | null;
14 | private view_: PickerView | null;
15 |
16 | constructor(
17 | impressionSender: ImpressionSender,
18 | server: ServerWrapper,
19 | modeManager: ModeManager) {
20 | this.impressionSender_ = impressionSender;
21 | this.server_ = server;
22 | this.modeManager_ = modeManager;
23 | this.model_ = null;
24 | this.view_ = null;
25 | }
26 |
27 | initialize(model: PickerModel, view: PickerView) {
28 | this.model_ = model;
29 | this.view_ = view;
30 | }
31 |
32 | addMetadata(): Promise {
33 | this.impressionSender_.sendImpression(ImpressionCode.PICKER_NEW_REPERTOIRE);
34 | return this.server_.createRepertoire().then(newRepertoireId => {
35 | this.updatePicker().then(() => {
36 | this.selectMetadataId(newRepertoireId);
37 | this.modeManager_.getSelectedMode().notifySelectedMetadata();
38 | });
39 | });
40 | }
41 |
42 | selectMetadataId(metadataId: string): void {
43 | if (!this.model_ || !this.view_) {
44 | throw new Error('Not initialized yet.');
45 | }
46 | if (this.model_.getSelectedMetadata().id == metadataId) {
47 | // No-op if the clicked metadata is already selected.
48 | return;
49 | }
50 | this.impressionSender_.sendImpression(
51 | ImpressionCode.PICKER_SELECT_REPERTOIRE);
52 | this.model_.selectMetadataId(metadataId);
53 | this.view_.refresh();
54 | this.modeManager_.getSelectedMode().notifySelectedMetadata();
55 | }
56 |
57 | deleteMetadataId(metadataId: string): Promise {
58 | if (!this.model_ || !this.view_) {
59 | throw new Error('Not initialized yet.');
60 | }
61 |
62 | this.impressionSender_.sendImpression(
63 | ImpressionCode.PICKER_DELETE_REPERTOIRE);
64 | const notifyMode = metadataId == this.getSelectedMetadataId();
65 | return this.server_.deleteRepertoire(metadataId)
66 | .then(() => this.updatePicker())
67 | .then(() => {
68 | if (notifyMode) {
69 | this.modeManager_.getSelectedMode().notifySelectedMetadata();
70 | }
71 | });
72 | }
73 |
74 | isModelEmpty(): boolean {
75 | if (!this.model_) {
76 | throw new Error('Not initialized yet.');
77 | }
78 | return this.model_.isEmpty();
79 | }
80 |
81 | getSelectedMetadataId(): string {
82 | if (!this.model_) {
83 | throw new Error('Not initialized yet.');
84 | }
85 | return this.model_.getSelectedMetadata().id;
86 | }
87 |
88 | updatePicker(): Promise {
89 | if (!this.model_ || !this.view_) {
90 | throw new Error('Not initialized yet.');
91 | }
92 |
93 | const lastSelectedMetadataId = this.model_.isEmpty()
94 | ? null
95 | : this.model_.getSelectedMetadata().id;
96 | return this.server_.getAllRepertoireMetadata()
97 | // If there are no repertoires, create a new one first.
98 | .then(metadataList => !metadataList.length
99 | ? this.addMetadata()
100 | .then(() => this.server_.getAllRepertoireMetadata())
101 | : Promise.resolve(metadataList))
102 | .then(metadataList => this.populatePicker_(
103 | lastSelectedMetadataId, metadataList));
104 | }
105 |
106 | private populatePicker_(
107 | lastSelectedMetadataId: string | null,
108 | metadataList: Metadata[]): void {
109 | if (!this.model_ || !this.view_) {
110 | throw new Error('Not initialized yet.');
111 | }
112 |
113 | this.model_.setMetadataList(metadataList, lastSelectedMetadataId);
114 | this.view_.refresh();
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/src/client/js/picker/pickerfeature.ts:
--------------------------------------------------------------------------------
1 | import { EvaluatedFlags } from '../../../protocol/evaluatedflags';
2 | import { assert } from '../../../util/assert';
3 | import { ConfirmDeleteDialog } from './confirmdeletedialog';
4 | import { PickerController } from './pickercontroller';
5 | import { PickerModel } from './pickermodel';
6 | import { PickerView } from './pickerview';
7 |
8 | export class PickerFeature {
9 | static install(
10 | flags: EvaluatedFlags,
11 | controller: PickerController,
12 | confirmDeleteDialog: ConfirmDeleteDialog): void {
13 | const pickerElement = assert(document.getElementById('picker'));
14 | const addMetadataElement = assert(document.getElementById('addMetadata'));
15 |
16 | const model = new PickerModel();
17 | const view = new PickerView(
18 | flags,
19 | model,
20 | controller,
21 | confirmDeleteDialog,
22 | pickerElement,
23 | addMetadataElement);
24 |
25 | controller.initialize(model, view);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/client/js/picker/pickermodel.test.ts:
--------------------------------------------------------------------------------
1 | import { PickerModel } from './pickermodel';
2 |
3 | describe('errors', () => {
4 | test('getSelected without set', () => {
5 | const model = new PickerModel();
6 | expect(() => model.getSelectedMetadata()).toThrow();
7 | expect(() => model.getSelectedIndex()).toThrow();
8 | });
9 | });
10 |
11 | describe('set', () => {
12 | test('set without id', () => {
13 | const model = new PickerModel();
14 | const metadata = [
15 | {id: 'id1', name: 'name1'},
16 | {id: 'id2', name: 'name2'},
17 | {id: 'id3', name: 'name3'}
18 | ];
19 | model.setMetadataList(metadata, null /* selectMetadataId */);
20 |
21 | expect(model.getSelectedIndex()).toBe(0);
22 | expect(model.getSelectedMetadata()).toBe(metadata[0]);
23 | });
24 |
25 | test('set with id', () => {
26 | const model = new PickerModel();
27 | const metadata = [
28 | {id: 'id1', name: 'name1'},
29 | {id: 'id2', name: 'name2'},
30 | {id: 'id3', name: 'name3'}
31 | ];
32 | model.setMetadataList(metadata, 'id2');
33 |
34 | expect(model.getSelectedIndex()).toBe(1);
35 | expect(model.getSelectedMetadata()).toBe(metadata[1]);
36 | });
37 |
38 | test('set with unknown id', () => {
39 | const model = new PickerModel();
40 | const metadata = [
41 | {id: 'id1', name: 'name1'},
42 | {id: 'id2', name: 'name2'},
43 | {id: 'id3', name: 'name3'}
44 | ];
45 | model.setMetadataList(metadata, 'id4');
46 |
47 | expect(model.getSelectedIndex()).toBe(0);
48 | expect(model.getSelectedMetadata()).toBe(metadata[0]);
49 | });
50 |
51 | test('set twice with id', () => {
52 | const model = new PickerModel();
53 | const metadata1 = [
54 | {id: 'id1', name: 'name1'},
55 | {id: 'id2', name: 'name2'},
56 | {id: 'id3', name: 'name3'}
57 | ];
58 | model.setMetadataList(metadata1, null);
59 | const metadata2 = [
60 | {id: 'id4', name: 'name4'},
61 | {id: 'id5', name: 'name5'},
62 | {id: 'id6', name: 'name6'}
63 | ];
64 | model.setMetadataList(metadata2, 'id6');
65 |
66 | expect(model.getSelectedIndex()).toBe(2);
67 | expect(model.getSelectedMetadata()).toBe(metadata2[2]);
68 | });
69 | });
70 |
71 | describe('isEmpty', () => {
72 | test('empty', () => {
73 | const model = new PickerModel();
74 | model.setMetadataList([], null);
75 | expect(model.isEmpty()).toBe(true);
76 | });
77 |
78 | test('not empty', () => {
79 | const model = new PickerModel();
80 | model.setMetadataList([{id: 'id1', name: 'name1'}], null);
81 | expect(model.isEmpty()).toBe(false);
82 | });
83 | });
84 |
--------------------------------------------------------------------------------
/src/client/js/picker/pickermodel.ts:
--------------------------------------------------------------------------------
1 | import { Metadata } from '../../../protocol/storage';
2 |
3 | export class PickerModel {
4 | private metadataList_: Metadata[];
5 | private selectedIndex_: number;
6 |
7 | constructor() {
8 | this.metadataList_ = [];
9 | this.selectedIndex_ = -1;
10 | }
11 |
12 | isEmpty(): boolean {
13 | return !this.metadataList_.length;
14 | }
15 |
16 | setMetadataList(
17 | metadataList: Metadata[],
18 | selectedMetadataId: string | null): void {
19 | this.metadataList_ = metadataList;
20 | this.selectMetadataId(selectedMetadataId);
21 | }
22 |
23 | selectMetadataId(metadataId: string | null): void {
24 | if (metadataId) {
25 | const metadataIndex
26 | = this.metadataList_.findIndex(m => m.id == metadataId);
27 | if (metadataIndex >= 0) {
28 | this.selectedIndex_ = metadataIndex;
29 | return;
30 | }
31 | }
32 | this.selectedIndex_ = 0;
33 | }
34 |
35 | getMetadataList(): Metadata[] {
36 | return this.metadataList_;
37 | }
38 |
39 | getSelectedMetadata(): Metadata {
40 | return this.metadataList_[this.getSelectedIndex()];
41 | }
42 |
43 | getSelectedIndex(): number {
44 | if (this.selectedIndex_ == -1) {
45 | throw new Error('No metadata selected yet.');
46 | }
47 | return this.selectedIndex_;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/client/js/picker/pickerview.ts:
--------------------------------------------------------------------------------
1 | import { FlagName } from '../../../flag/flags';
2 | import { EvaluatedFlags } from '../../../protocol/evaluatedflags';
3 | import { Metadata } from '../../../protocol/storage';
4 | import { ConfirmDeleteDialog } from './confirmdeletedialog';
5 | import { PickerController } from './pickercontroller';
6 | import { PickerModel } from './pickermodel';
7 |
8 | const MAX_REPERTOIRES_PER_USER = 20;
9 |
10 | enum CssClasses {
11 | DELETE_BUTTON = 'deleteButton',
12 | HOVER_BUTTON = 'hoverButton',
13 | METADATA = 'metadata',
14 | SELECTED_METADATA = 'selected'
15 | }
16 |
17 | export class PickerView {
18 | private flags_: EvaluatedFlags;
19 | private pickerModel_: PickerModel;
20 | private pickerController_: PickerController;
21 | private confirmDeleteDialog_: ConfirmDeleteDialog;
22 | private pickerElement_: HTMLElement;
23 | private addMetadataElement_: HTMLElement;
24 |
25 | constructor(
26 | flags: EvaluatedFlags,
27 | pickerModel: PickerModel,
28 | pickerController: PickerController,
29 | confirmDeleteDialog: ConfirmDeleteDialog,
30 | pickerElement: HTMLElement,
31 | addMetadataElement: HTMLElement) {
32 | this.flags_ = flags;
33 | this.pickerModel_ = pickerModel;
34 | this.pickerController_ = pickerController;
35 | this.confirmDeleteDialog_ = confirmDeleteDialog;
36 | this.pickerElement_ = pickerElement;
37 | this.addMetadataElement_ = addMetadataElement;
38 |
39 | this.addMetadataElement_.onclick
40 | = () => this.pickerController_.addMetadata();
41 | }
42 |
43 | refresh() {
44 | // Remove all metadata children of the picker.
45 | const metadataChildren = document.querySelectorAll(
46 | '#picker > div.metadata');
47 | for (let i = 0; i < metadataChildren.length; i++) {
48 | this.pickerElement_.removeChild(metadataChildren.item(i));
49 | }
50 |
51 | // Insert the new metadata children before the add metadata button.
52 | const metadataList = this.pickerModel_.getMetadataList();
53 | const selectedIndex = this.pickerModel_.getSelectedIndex();
54 | for (let j = 0; j < metadataList.length; j++) {
55 | const newChild = this.createMetadataElement_(
56 | metadataList[j], j == selectedIndex /* isSelected */);
57 | this.pickerElement_.insertBefore(newChild, this.addMetadataElement_);
58 | }
59 |
60 | // Hide the add metadata button if the user has the maximum number of
61 | // repertoires.
62 | const hideAddMetadataButton =
63 | metadataList.length >= MAX_REPERTOIRES_PER_USER;
64 | this.addMetadataElement_.classList.toggle('hidden', hideAddMetadataButton);
65 | }
66 |
67 | private createMetadataElement_(
68 | metadata: Metadata, isSelected: boolean): HTMLElement {
69 | const newElement = document.createElement('div');
70 | newElement.classList.add(CssClasses.METADATA);
71 | if (isSelected) {
72 | newElement.classList.add(CssClasses.SELECTED_METADATA);
73 | }
74 |
75 | const label = document.createElement('div');
76 | label.classList.add('metadataName');
77 | label.innerText = metadata.name;
78 |
79 | const deleteButton = document.createElement('div');
80 | deleteButton.onclick = (e) => this.handleDeleteButton_(
81 | e, metadata.id, metadata.name);
82 |
83 | deleteButton.classList.add(
84 | CssClasses.HOVER_BUTTON, CssClasses.DELETE_BUTTON);
85 |
86 | newElement.append(label, deleteButton);
87 |
88 | newElement.onclick = () =>
89 | this.pickerController_.selectMetadataId(metadata.id);
90 | return newElement;
91 | }
92 |
93 | private handleDeleteButton_(
94 | e: MouseEvent, metadataId: string, metadataName: string): void {
95 | if (!this.flags_[FlagName.ENABLE_PICKER_DELETE_CONFIRM]) {
96 | this.pickerController_.deleteMetadataId(metadataId);
97 | } else {
98 | this.confirmDeleteDialog_.showForRepertoire(metadataId, metadataName);
99 | }
100 |
101 | // The click should not propagate to the parent metadata element since doing
102 | // so would cause the repertoire being deleted to also be loaded.
103 | e.stopPropagation();
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/client/js/preferences/preferenceloader.ts:
--------------------------------------------------------------------------------
1 | import { BoardTheme } from '../../../protocol/boardtheme';
2 | import { Preference } from '../../../protocol/preference/preference';
3 | import { SoundValue } from '../../../protocol/soundvalue';
4 | import { ServerWrapper } from '../server/serverwrapper';
5 | import { SoundToggler } from '../sound/soundtoggler';
6 | import { BoardThemeSetter } from '../theme/boardthemesetter';
7 |
8 | export class PreferenceLoader {
9 | private server_: ServerWrapper;
10 | private boardThemeSetter_: BoardThemeSetter;
11 | private soundToggler_: SoundToggler;
12 |
13 | constructor(
14 | server: ServerWrapper,
15 | boardThemeSetter: BoardThemeSetter,
16 | soundToggler: SoundToggler) {
17 | this.server_ = server;
18 | this.boardThemeSetter_ = boardThemeSetter;
19 | this.soundToggler_ = soundToggler;
20 | }
21 |
22 | load(): void {
23 | this.server_.getPreference()
24 | .then(preference => {
25 | this.setBoardTheme_(preference);
26 | this.setSoundValue_(preference);
27 | });
28 | }
29 |
30 | private setBoardTheme_(preference: Preference) {
31 | this.boardThemeSetter_.set(preference.boardTheme || BoardTheme.BLUE);
32 | }
33 |
34 | private setSoundValue_(preference: Preference) {
35 | const enabled = preference.soundValue
36 | ? preference.soundValue == SoundValue.ON
37 | : true;
38 | this.soundToggler_.setSoundsEnabled(enabled);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/client/js/preferences/preferencesaver.ts:
--------------------------------------------------------------------------------
1 | import { Preference } from '../../../protocol/preference/preference';
2 | import { ServerWrapper } from '../server/serverwrapper';
3 |
4 | export class PreferenceSaver {
5 | private server_: ServerWrapper;
6 |
7 | constructor(server: ServerWrapper) {
8 | this.server_ = server;
9 | }
10 |
11 | save(preference: Preference): void {
12 | this.server_.setPreference(preference);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/client/js/priveleged/privelegedcopydialog.ts:
--------------------------------------------------------------------------------
1 | import { PickerController } from '../picker/pickercontroller';
2 | import { ServerWrapper } from '../server/serverwrapper';
3 |
4 | export class PrivelegedCopyDialog {
5 | private server_: ServerWrapper;
6 | private pickerController_: PickerController;
7 | private dialogEl_: HTMLElement;
8 | private inputEl_: HTMLInputElement;
9 |
10 | constructor(
11 | server: ServerWrapper,
12 | pickerController: PickerController,
13 | dialogEl: HTMLElement,
14 | inputEl: HTMLInputElement) {
15 | this.server_ = server;
16 | this.pickerController_ = pickerController;
17 | this.dialogEl_ = dialogEl;
18 | this.inputEl_ = inputEl;
19 | }
20 |
21 | bindButtons(
22 | okButton: HTMLElement,
23 | cancelButton: HTMLElement): void {
24 | okButton.onclick = () => this.onOkClick_();
25 | cancelButton.onclick = () => this.hide();
26 | }
27 |
28 | show(): void {
29 | this.dialogEl_.classList.remove('hidden');
30 | }
31 |
32 | hide(): void {
33 | this.dialogEl_.classList.add('hidden');
34 | }
35 |
36 | isVisible(): boolean {
37 | return !this.dialogEl_.classList.contains('hidden');
38 | }
39 |
40 | onKeyDown(e: KeyboardEvent): void {
41 | if (e.keyCode == 13) {
42 | this.onOkClick_(); // Enter
43 | } else if (e.keyCode == 27) {
44 | this.hide(); // Esc
45 | }
46 | }
47 |
48 | private onOkClick_(): void {
49 | this.server_.copyRepertoireAsPrivelegedUser(this.inputEl_.value).then(
50 | () => {
51 | this.hide();
52 | this.pickerController_.updatePicker();
53 | });
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/client/js/priveleged/privelegedfeature.ts:
--------------------------------------------------------------------------------
1 | import { assert } from '../../../util/assert';
2 | import { PrivelegedCopyDialog } from './privelegedcopydialog';
3 |
4 | export class PrivelegedFeature {
5 | static install(
6 | privelegedCopyDialog: PrivelegedCopyDialog) {
7 | privelegedCopyDialog.bindButtons(
8 | assert(document.getElementById('privelegedCopyOk')),
9 | assert(document.getElementById('privelegedCopyCancel')));
10 |
11 | const copyButton = assert(document.getElementById('privelegedCopyButton'));
12 | copyButton.classList.remove('hidden');
13 | copyButton.onclick = () => privelegedCopyDialog.show();
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/client/js/server/delegatingserverwrapper.ts:
--------------------------------------------------------------------------------
1 | import { Preference } from '../../../protocol/preference/preference';
2 | import { CumulatedStatistic } from '../../../protocol/statistic/cumulatedstatistic';
3 | import { Statistic } from '../../../protocol/statistic/statistic';
4 | import { Metadata, Repertoire } from '../../../protocol/storage';
5 | import { ServerWrapper } from './serverwrapper';
6 |
7 | export class DelegatingServerWrapper implements ServerWrapper {
8 | private delegate_: ServerWrapper;
9 |
10 | constructor(delegate: ServerWrapper) {
11 | this.delegate_ = delegate;
12 | }
13 |
14 | setDelegate(delegate: ServerWrapper) {
15 | this.delegate_ = delegate;
16 | }
17 |
18 | getAllRepertoireMetadata(): Promise {
19 | return this.delegate_.getAllRepertoireMetadata();
20 | }
21 |
22 | loadRepertoire(repertoireId: string): Promise {
23 | return this.delegate_.loadRepertoire(repertoireId);
24 | }
25 |
26 | updateRepertoire(
27 | repertoireId: string, repertoire: Repertoire): Promise {
28 | return this.delegate_.updateRepertoire(repertoireId, repertoire);
29 | }
30 |
31 | createRepertoire(): Promise {
32 | return this.delegate_.createRepertoire();
33 | }
34 |
35 | deleteRepertoire(repertoireId: string): Promise {
36 | return this.delegate_.deleteRepertoire(repertoireId);
37 | }
38 |
39 | setPreference(preference: Preference): Promise {
40 | return this.delegate_.setPreference(preference);
41 | }
42 |
43 | getPreference(): Promise {
44 | return this.delegate_.getPreference();
45 | }
46 |
47 | recordStatistics(statisticList: Statistic[]): Promise {
48 | return this.delegate_.recordStatistics(statisticList);
49 | }
50 |
51 | loadCumulatedStatistics(repertoireId: string): Promise {
52 | return this.delegate_.loadCumulatedStatistics(repertoireId);
53 | }
54 |
55 | copyRepertoireAsPrivelegedUser(repertoireId: string): Promise {
56 | return this.delegate_.copyRepertoireAsPrivelegedUser(repertoireId);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/client/js/server/evaluatedflagfetcher.ts:
--------------------------------------------------------------------------------
1 | import { EvaluateFlagsResponse } from '../../../protocol/actions';
2 | import { EvaluatedFlags } from '../../../protocol/evaluatedflags';
3 | import { Toasts } from '../common/toasts';
4 |
5 | export class EvaluatedFlagFetcher {
6 | static fetchEvaluatedFlags(): Promise {
7 | const options = {
8 | method: 'POST',
9 | headers: {
10 | 'Content-Type': 'application/json',
11 | }
12 | };
13 | return fetch('/flags', options)
14 | .then(res => (res.json() as unknown) as EvaluateFlagsResponse)
15 | .then(evaluatedFlagsResponse => evaluatedFlagsResponse.evaluatedFlags)
16 | .catch(err => {
17 | EvaluatedFlagFetcher.showError_();
18 | throw err;
19 | });
20 | }
21 |
22 | private static showError_(): void {
23 | Toasts.error(
24 | 'Something went wrong.',
25 | 'There was a problem reaching the server. Please refresh the page and '
26 | + 'try again.');
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/client/js/server/serverwrapper.ts:
--------------------------------------------------------------------------------
1 | import { Preference } from '../../../protocol/preference/preference';
2 | import { CumulatedStatistic } from '../../../protocol/statistic/cumulatedstatistic';
3 | import { Statistic } from '../../../protocol/statistic/statistic';
4 | import { Metadata, Repertoire } from '../../../protocol/storage';
5 |
6 | /**
7 | * An interface for classes that send requests to the server and receive
8 | * responses on behalf of the rest of the application.
9 | *
10 | * Implementations may not actually communicate with the server (e.g. in the
11 | * case of users that are not logged in).
12 | */
13 | export interface ServerWrapper {
14 | /**
15 | * Returns a promise of the list of the metadata for all repertoires owned by
16 | * the current user.
17 | */
18 | getAllRepertoireMetadata(): Promise;
19 |
20 | /**
21 | * Loads the repertoire with the given ID, returning a promise of the loaded
22 | * repertoire.
23 | */
24 | loadRepertoire(repertoireId: string): Promise;
25 |
26 | /** Updates the existing repertoire with the given ID. */
27 | updateRepertoire(
28 | repertoireId: string,
29 | repertoire: Repertoire): Promise;
30 |
31 | /**
32 | * Creates a new repertoire and returns a promise of the ID of the newly
33 | * created repertoire.
34 | */
35 | createRepertoire(): Promise;
36 |
37 | /** Deletes the repertoire with the given ID. */
38 | deleteRepertoire(repertoireId: string): Promise;
39 |
40 | /** Sets the given preference. */
41 | setPreference(preference: Preference): Promise;
42 |
43 | /** Gets the preference. */
44 | getPreference(): Promise;
45 |
46 | /** Records the given statistics. */
47 | recordStatistics(statisticList: Statistic[]): Promise;
48 |
49 | /** Loads the cumulated statistics for the given repertoire. */
50 | loadCumulatedStatistics(repertoireId: string): Promise;
51 |
52 | /** Copies the given repertoire as a priveleged user. */
53 | copyRepertoireAsPrivelegedUser(repertoireId: string): Promise;
54 | }
55 |
--------------------------------------------------------------------------------
/src/client/js/sound/soundplayer.ts:
--------------------------------------------------------------------------------
1 | import { Howl } from 'howler';
2 | import { SoundToggler } from './soundtoggler';
3 |
4 | export class SoundPlayer {
5 | private soundToggler_: SoundToggler;
6 |
7 | constructor(soundToggler: SoundToggler) {
8 | this.soundToggler_ = soundToggler;
9 | }
10 |
11 | playMove(): void {
12 | this.play_('move', 0.4);
13 | }
14 |
15 | playCapture(): void {
16 | this.play_('capture', 0.4);
17 | }
18 |
19 | playWrongMove(): void {
20 | this.play_('wrongmove', 1);
21 | }
22 |
23 | playFinishLine(): void {
24 | this.play_('finishline', 1);
25 | }
26 |
27 | private play_(soundName: string, volume: number): void {
28 | if (!this.soundToggler_.areSoundsEnabled()) {
29 | return;
30 | }
31 |
32 | new Howl({
33 | src: [`ogg/${soundName}.ogg`],
34 | volume: volume,
35 | autoplay: true
36 | });
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/client/js/sound/soundtoggler.ts:
--------------------------------------------------------------------------------
1 | import { ImpressionCode } from '../../../protocol/impression/impressioncode';
2 | import { SoundValue } from '../../../protocol/soundvalue';
3 | import { ImpressionSender } from '../impressions/impressionsender';
4 | import { PreferenceSaver } from '../preferences/preferencesaver';
5 |
6 | export class SoundToggler {
7 | private impressionSender_: ImpressionSender;
8 | private preferenceSaver_: PreferenceSaver;
9 | private soundOnEl_: HTMLElement;
10 | private soundOffEl_: HTMLElement;
11 | private soundsEnabled_: boolean;
12 |
13 | constructor(
14 | impressionSender: ImpressionSender,
15 | preferenceSaver: PreferenceSaver,
16 | soundTogglerEl: HTMLElement,
17 | soundOnEl: HTMLElement,
18 | soundOffEl: HTMLElement) {
19 | this.impressionSender_ = impressionSender;
20 | this.preferenceSaver_ = preferenceSaver;
21 | this.soundOnEl_ = soundOnEl;
22 | this.soundOffEl_ = soundOffEl;
23 | this.soundsEnabled_ = true;
24 |
25 | this.refreshEls_();
26 |
27 | soundTogglerEl.onclick = () => this.toggle();
28 | }
29 |
30 | areSoundsEnabled(): boolean {
31 | return this.soundsEnabled_;
32 | }
33 |
34 | setSoundsEnabled(soundsEnabled: boolean): void {
35 | this.setSoundsEnabledAndMaybeSetPreference_(
36 | soundsEnabled, false /* setPreference */);
37 | }
38 |
39 | toggle(): void {
40 | this.setSoundsEnabledAndMaybeSetPreference_(
41 | !this.soundsEnabled_, true /* setPreference */);
42 | }
43 |
44 | private setSoundsEnabledAndMaybeSetPreference_(
45 | soundsEnabled: boolean, setPreference: boolean): void {
46 | this.soundsEnabled_ = soundsEnabled;
47 | this.refreshEls_();
48 |
49 | if (setPreference) {
50 | const soundValue = soundsEnabled ? SoundValue.ON : SoundValue.OFF;
51 | this.impressionSender_.sendImpression(
52 | ImpressionCode.TOGGLED_SOUNDS, {soundValue});
53 | this.preferenceSaver_.save({soundValue});
54 | }
55 | }
56 |
57 | private refreshEls_(): void {
58 | this.soundOnEl_.classList.toggle('hidden', !this.soundsEnabled_);
59 | this.soundOffEl_.classList.toggle('hidden', this.soundsEnabled_);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/client/js/statistics/debouncingstatisticsrecorder.ts:
--------------------------------------------------------------------------------
1 | import { Statistic } from '../../../protocol/statistic/statistic';
2 | import { StatisticType } from '../../../protocol/statistic/statistictype';
3 | import { Debouncer } from '../common/debouncer';
4 | import { PickerController } from '../picker/pickercontroller';
5 | import { ServerWrapper } from '../server/serverwrapper';
6 | import { StatisticRecorder } from './statisticrecorder';
7 |
8 | export class DebouncingStatisticRecorder implements StatisticRecorder {
9 | private pickerController_: PickerController;
10 | private server_: ServerWrapper;
11 | private debouncer_: Debouncer;
12 | private statisticsToSend_: Statistic[];
13 |
14 | constructor(
15 | pickerController: PickerController,
16 | server: ServerWrapper,
17 | debounceIntervalMs: number) {
18 | this.pickerController_ = pickerController;
19 | this.server_ = server;
20 | this.debouncer_ = new Debouncer(
21 | () => this.sendToServer_(), debounceIntervalMs);
22 |
23 | this.statisticsToSend_ = [];
24 | }
25 |
26 | recordRightMove(pgn: string): void {
27 | this.recordStatistic_(pgn, StatisticType.RIGHT_MOVE);
28 | }
29 |
30 | recordWrongMove(pgn: string): void {
31 | this.recordStatistic_(pgn, StatisticType.WRONG_MOVE);
32 | }
33 |
34 | recordFinishLine(pgn: string): void {
35 | this.recordStatistic_(pgn, StatisticType.FINISH_LINE);
36 | }
37 |
38 | private recordStatistic_(pgn: string, statisticType: StatisticType): void {
39 | const repertoireId = this.pickerController_.getSelectedMetadataId();
40 | this.statisticsToSend_.push({repertoireId, pgn, statisticType});
41 | this.debouncer_.fire();
42 | }
43 |
44 | private sendToServer_(): void {
45 | this.server_.recordStatistics(this.statisticsToSend_);
46 | this.statisticsToSend_ = [];
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/client/js/statistics/delegatingstatisticsmodel.ts:
--------------------------------------------------------------------------------
1 | import { StatisticsModel } from './statisticsmodel';
2 | import { ZeroStatisticsModel } from './zerostatisticsmodel';
3 |
4 | export class DelegatingStatisticsModel implements StatisticsModel {
5 | private delegate_: StatisticsModel;
6 |
7 | constructor() {
8 | this.delegate_ = new ZeroStatisticsModel();
9 | }
10 |
11 | setDelegate(delegate: StatisticsModel): void {
12 | this.delegate_ = delegate;
13 | }
14 |
15 | getRepertoireFinishLineCount(): Promise {
16 | return this.delegate_.getRepertoireFinishLineCount();
17 | }
18 |
19 | getRepertoireRightMoveCount(): Promise {
20 | return this.delegate_.getRepertoireRightMoveCount();
21 | }
22 |
23 | getRepertoireWrongMoveCount(): Promise {
24 | return this.delegate_.getRepertoireWrongMoveCount();
25 | }
26 |
27 | getRightMoveCount(pgn: string): Promise {
28 | return this.delegate_.getRightMoveCount(pgn);
29 | }
30 |
31 | getWrongMoveCount(pgn: string): Promise {
32 | return this.delegate_.getWrongMoveCount(pgn);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/client/js/statistics/serverstatisticsmodel.ts:
--------------------------------------------------------------------------------
1 | import { CumulatedStatistic } from '../../../protocol/statistic/cumulatedstatistic';
2 | import { ServerWrapper } from '../server/serverwrapper';
3 | import { StatisticsModel } from './statisticsmodel';
4 |
5 | export class ServerStatisticsModel implements StatisticsModel {
6 | private repertoireFinishLineCount_: Promise;
7 | private repertoireRightMoveCount_: Promise;
8 | private repertoireWrongMoveCount_: Promise;
9 | private rightMoveCounts_: Promise