playball 3.1.3 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -1
- package/dist/cli.js +1 -1
- package/dist/components/AllPlays.js +66 -40
- package/dist/components/App.js +16 -5
- package/dist/components/BoxScore.js +69 -0
- package/dist/components/FinishedGame.js +85 -34
- package/dist/components/Game.js +5 -3
- package/dist/components/GameList.js +1 -1
- package/dist/components/LiveGame.js +64 -3
- package/dist/components/PreviewGame.js +22 -2
- package/dist/components/Table.js +44 -0
- package/dist/config.js +14 -1
- package/dist/features/games.js +46 -4
- package/dist/main.js +4 -2
- package/dist/screen.js +8 -1
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -24,6 +24,29 @@ Then run it
|
|
|
24
24
|
$ playball
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
### Docker
|
|
28
|
+
Don't have Node.js installed? You can run via Docker instead.
|
|
29
|
+
```
|
|
30
|
+
$ docker run -it --rm paaatrick0/playball
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
> [!TIP]
|
|
34
|
+
> When running via Docker, times will be shown by default in Eastern Time. To change this, set the `TZ` environment variable to your desired timezone.
|
|
35
|
+
>
|
|
36
|
+
> For Central Time use:
|
|
37
|
+
> ```
|
|
38
|
+
> $ docker run -it --rm -e TZ=America/Chicago paaatrick0/playball
|
|
39
|
+
> ```
|
|
40
|
+
> For Mountain Time use:
|
|
41
|
+
> ```
|
|
42
|
+
> $ docker run -it --rm -e TZ=America/Denver paaatrick0/playball
|
|
43
|
+
> ```
|
|
44
|
+
> For Pacific Time use:
|
|
45
|
+
> ```
|
|
46
|
+
> $ docker run -it --rm -e TZ=America/Los_Angeles paaatrick0/playball
|
|
47
|
+
> ```
|
|
48
|
+
> For other timezones see the list of [TZ database time zones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones).
|
|
49
|
+
|
|
27
50
|
### Keys
|
|
28
51
|
#### Global
|
|
29
52
|
key | action
|
|
@@ -106,7 +129,9 @@ key | description | default | allowed values
|
|
|
106
129
|
`color.strike` | Color of dots representing strikes in top row of game view | red | _See above_
|
|
107
130
|
`color.strike-out` | Color of result where play ends on a strike (strike out) in list of plays in game view | red | _See above_
|
|
108
131
|
`color.walk` | Color of result where play ends on a ball (walk, hit by pitch) in list of plays in game view | green | _See above_
|
|
109
|
-
`favorites` | Teams to highlight in schedule and standings views | | Any one of the following: `ATL`, `AZ`, `BAL`, `BOS`, `CHC`, `CIN`, `CLE`, `COL`, `CWS`, `DET`, `HOU`, `KC`, `LAA`, `LAD`, `MIA`, `MIL`, `MIN`, `NYM`, `NYY`, `OAK`, `PHI`, `PIT`, `SD`, `SEA`, `SF`, `STL`, `TB`, `TEX`, `TOR`, `WSH`. Or a comma-separated list of multiple (e.g. `SEA,MIL`)
|
|
132
|
+
`favorites` | Teams to highlight in schedule and standings views | | Any one of the following: `ATL`, `AZ`, `BAL`, `BOS`, `CHC`, `CIN`, `CLE`, `COL`, `CWS`, `DET`, `HOU`, `KC`, `LAA`, `LAD`, `MIA`, `MIL`, `MIN`, `NYM`, `NYY`, `OAK`, `PHI`, `PIT`, `SD`, `SEA`, `SF`, `STL`, `TB`, `TEX`, `TOR`, `WSH`. Or a comma-separated list of multiple (e.g. `SEA,MIL`).<br/><br />Note: in some terminals the list must be quoted: `playball config favorites "SEA,MIL"`
|
|
133
|
+
`title` | If enabled, the terminal title will be set to the score of the current game | `false` | `false`, `true`
|
|
134
|
+
`live-delay` | Number of seconds to delay the live game stream. Useful when watching with delayed broadcast streams. | `0` (no delay) | Any positive number
|
|
110
135
|
|
|
111
136
|
### Development
|
|
112
137
|
```
|
package/dist/cli.js
CHANGED
|
@@ -10,7 +10,7 @@ const notifier = updateNotifier({
|
|
|
10
10
|
updateCheckInterval: 1000 * 60 * 60 * 24 * 7 // 1 week
|
|
11
11
|
});
|
|
12
12
|
|
|
13
|
-
program.name(pkg.name).description(pkg.description).version(pkg.version).action(main).hook('postAction', () => notifier.notify({
|
|
13
|
+
program.name(pkg.name).description(pkg.description).version(pkg.version).option('--replay <gameId>', 'Replay a game by game ID').action(main).hook('postAction', () => notifier.notify({
|
|
14
14
|
isGlobal: true
|
|
15
15
|
}));
|
|
16
16
|
program.command('config').description('Set or get configration values').argument('[key]').argument('[value]').option('--unset', 'Unset configuration value').action((key, value, options) => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { useSelector } from "react-redux/lib/alternate-renderers.js";
|
|
3
|
+
import PropTypes from 'prop-types';
|
|
3
4
|
import { selectAllPlays, selectTeams } from "../features/games.js";
|
|
4
5
|
import { get } from "../config.js";
|
|
5
6
|
import style from "../style/index.js";
|
|
@@ -19,28 +20,46 @@ function getPlayResultColor(play) {
|
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
22
|
function formatOut(out) {
|
|
22
|
-
return ` {bold}${out} out{/}`;
|
|
23
|
+
return ` {bold}${out} out${out > 1 ? 's' : ''}{/}`;
|
|
23
24
|
}
|
|
24
|
-
function AllPlays(
|
|
25
|
+
function AllPlays({
|
|
26
|
+
reverse,
|
|
27
|
+
scoringOnly
|
|
28
|
+
}) {
|
|
25
29
|
const plays = useSelector(selectAllPlays);
|
|
26
30
|
const teams = useSelector(selectTeams);
|
|
27
31
|
const formatScoreDetail = scoreObj => ` {bold}{${get('color.in-play-runs-bg')}-bg}{${get('color.in-play-runs-fg')}-fg} ` + `${teams.away.abbreviation} ${scoreObj.awayScore} - ` + `${teams.home.abbreviation} ${scoreObj.homeScore}` + ' {/}';
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
let lastPlay;
|
|
32
|
-
if (playIdx < plays.length - 1) {
|
|
33
|
-
lastPlay = plays[playIdx + 1];
|
|
34
|
-
}
|
|
32
|
+
const playsByInning = {};
|
|
33
|
+
plays === null || plays === void 0 ? void 0 : plays.forEach(play => {
|
|
34
|
+
var _play$playEvents2;
|
|
35
35
|
const playInning = play.about.halfInning + ' ' + play.about.inning;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
(_play$playEvents2 = play.playEvents) === null || _play$playEvents2 === void 0 ? void 0 : _play$playEvents2.forEach(event => {
|
|
37
|
+
if (event.type === 'action') {
|
|
38
|
+
if (scoringOnly && !event.details.isScoringPlay) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (event.details.eventType === 'batter_timeout' || event.details.eventType === 'mound_visit') {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
let line = '';
|
|
45
|
+
if (event.details.event) {
|
|
46
|
+
line += `{${get('color.other-event')}-fg}[${event.details.event}]{/} `;
|
|
47
|
+
}
|
|
48
|
+
line += event.details.description;
|
|
49
|
+
if (event.details.isOut) {
|
|
50
|
+
var _event$count;
|
|
51
|
+
line += formatOut((_event$count = event.count) === null || _event$count === void 0 ? void 0 : _event$count.outs);
|
|
52
|
+
}
|
|
53
|
+
if (event.isScoringPlay || event.details.isScoringPlay) {
|
|
54
|
+
line += formatScoreDetail(event.details);
|
|
55
|
+
}
|
|
56
|
+
if (!(playInning in playsByInning)) {
|
|
57
|
+
playsByInning[playInning] = [];
|
|
58
|
+
}
|
|
59
|
+
playsByInning[playInning].push(line);
|
|
40
60
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (play.about.isComplete) {
|
|
61
|
+
});
|
|
62
|
+
if (play.about.isComplete && (!scoringOnly || play.about.isScoringPlay)) {
|
|
44
63
|
const color = getPlayResultColor(play);
|
|
45
64
|
let line = `{${color}-fg}[${play.result.event}]{/} ${play.result.description}`;
|
|
46
65
|
if (play.about.hasOut) {
|
|
@@ -52,32 +71,35 @@ function AllPlays() {
|
|
|
52
71
|
if (play.about.isScoringPlay) {
|
|
53
72
|
line += formatScoreDetail(play.result);
|
|
54
73
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
play.playEvents && play.playEvents.slice().reverse().forEach((event, eventIdx, events) => {
|
|
58
|
-
if (event.type === 'action') {
|
|
59
|
-
var _event$count;
|
|
60
|
-
let line = '';
|
|
61
|
-
if (event.details.event) {
|
|
62
|
-
line += `{${get('color.other-event')}-fg}[${event.details.event}]{/} `;
|
|
63
|
-
}
|
|
64
|
-
line += event.details.description;
|
|
65
|
-
if (event.isScoringPlay || event.details.isScoringPlay) {
|
|
66
|
-
line += formatScoreDetail(event.details);
|
|
67
|
-
}
|
|
68
|
-
const currentOut = (_event$count = event.count) === null || _event$count === void 0 ? void 0 : _event$count.outs;
|
|
69
|
-
let prevOut = lastPlay ? lastPlay.count.outs : 0;
|
|
70
|
-
if (eventIdx < events.length - 1) {
|
|
71
|
-
var _events$count;
|
|
72
|
-
prevOut = (_events$count = events[eventIdx + 1].count) === null || _events$count === void 0 ? void 0 : _events$count.outs;
|
|
73
|
-
}
|
|
74
|
-
if (currentOut > prevOut) {
|
|
75
|
-
line += formatOut(currentOut);
|
|
76
|
-
}
|
|
77
|
-
lines.push(line);
|
|
74
|
+
if (!(playInning in playsByInning)) {
|
|
75
|
+
playsByInning[playInning] = [];
|
|
78
76
|
}
|
|
79
|
-
|
|
77
|
+
playsByInning[playInning].push(line);
|
|
78
|
+
}
|
|
80
79
|
});
|
|
80
|
+
const lines = [];
|
|
81
|
+
const inningKeys = Object.keys(playsByInning);
|
|
82
|
+
if (reverse) {
|
|
83
|
+
inningKeys.reverse();
|
|
84
|
+
}
|
|
85
|
+
inningKeys.forEach(inning => {
|
|
86
|
+
if (lines.length > 0) {
|
|
87
|
+
lines.push('');
|
|
88
|
+
}
|
|
89
|
+
lines.push(`{bold}[${inning.toUpperCase()}]{/}`);
|
|
90
|
+
if (reverse) {
|
|
91
|
+
lines.push(...playsByInning[inning].slice().reverse());
|
|
92
|
+
} else {
|
|
93
|
+
lines.push(...playsByInning[inning]);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
if (lines.length === 0) {
|
|
97
|
+
if (scoringOnly) {
|
|
98
|
+
lines.push('No scoring plays yet');
|
|
99
|
+
} else {
|
|
100
|
+
lines.push('No plays yet');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
81
103
|
return /*#__PURE__*/React.createElement("box", {
|
|
82
104
|
content: lines.join('\n'),
|
|
83
105
|
focused: true,
|
|
@@ -90,4 +112,8 @@ function AllPlays() {
|
|
|
90
112
|
tags: true
|
|
91
113
|
});
|
|
92
114
|
}
|
|
115
|
+
AllPlays.propTypes = {
|
|
116
|
+
reverse: PropTypes.bool,
|
|
117
|
+
scoringOnly: PropTypes.bool
|
|
118
|
+
};
|
|
93
119
|
export default AllPlays;
|
package/dist/components/App.js
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
2
|
import { useDispatch } from "react-redux/lib/alternate-renderers.js";
|
|
3
|
+
import PropTypes from 'prop-types';
|
|
3
4
|
import GameList from "./GameList.js";
|
|
4
5
|
import HelpBar from "./HelpBar.js";
|
|
5
|
-
import {
|
|
6
|
+
import { setReplayGame, setLiveGame } from "../features/games.js";
|
|
6
7
|
import Game from "./Game.js";
|
|
7
8
|
import useKey from "../hooks/useKey.js";
|
|
8
9
|
import Standings from "./Standings.js";
|
|
9
10
|
const SCHEDULE = 'schedule';
|
|
10
11
|
const STANDINGS = 'standings';
|
|
11
12
|
const GAME = 'game';
|
|
12
|
-
function App(
|
|
13
|
+
function App({
|
|
14
|
+
replayId
|
|
15
|
+
}) {
|
|
13
16
|
const [view, setView] = useState(SCHEDULE);
|
|
14
17
|
const dispatch = useDispatch();
|
|
15
18
|
useKey('c', () => {
|
|
16
19
|
setView(SCHEDULE);
|
|
17
|
-
dispatch(
|
|
20
|
+
dispatch(setLiveGame(null));
|
|
18
21
|
}, {
|
|
19
22
|
key: 'C',
|
|
20
23
|
label: 'Schedule'
|
|
@@ -24,9 +27,14 @@ function App() {
|
|
|
24
27
|
label: 'Standings'
|
|
25
28
|
});
|
|
26
29
|
const handleGameSelect = game => {
|
|
27
|
-
dispatch(
|
|
30
|
+
dispatch(setLiveGame(game.gamePk));
|
|
28
31
|
setView(GAME);
|
|
29
32
|
};
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (replayId) {
|
|
35
|
+
dispatch(setReplayGame(replayId)).unwrap().then(() => setView(GAME)).catch(() => setView(SCHEDULE));
|
|
36
|
+
}
|
|
37
|
+
}, [replayId]);
|
|
30
38
|
return /*#__PURE__*/React.createElement("element", null, /*#__PURE__*/React.createElement("element", {
|
|
31
39
|
top: 0,
|
|
32
40
|
left: 0,
|
|
@@ -39,4 +47,7 @@ function App() {
|
|
|
39
47
|
height: 1
|
|
40
48
|
}, /*#__PURE__*/React.createElement(HelpBar, null)));
|
|
41
49
|
}
|
|
50
|
+
App.propTypes = {
|
|
51
|
+
replayId: PropTypes.string
|
|
52
|
+
};
|
|
42
53
|
export default App;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useSelector } from "react-redux/lib/alternate-renderers.js";
|
|
3
|
+
import { selectBoxscore, selectPlayers } from "../features/games.js";
|
|
4
|
+
import Table from "./Table.js";
|
|
5
|
+
function getBatterRows(boxscoreTeam, players) {
|
|
6
|
+
const batters = Object.values(boxscoreTeam.players).filter(player => player.battingOrder !== undefined).sort((a, b) => parseInt(a.battingOrder) - parseInt(b.battingOrder));
|
|
7
|
+
const batterNames = batters.map(batter => {
|
|
8
|
+
var _batter$allPositions, _batter$gameStatus;
|
|
9
|
+
const name = players[`ID${batter.person.id}`].boxscoreName;
|
|
10
|
+
const positions = (_batter$allPositions = batter.allPositions) === null || _batter$allPositions === void 0 ? void 0 : _batter$allPositions.map(pos => pos.abbreviation).join('-');
|
|
11
|
+
const prefix = (_batter$gameStatus = batter.gameStatus) !== null && _batter$gameStatus !== void 0 && _batter$gameStatus.isSubstitute ? ' ' : '';
|
|
12
|
+
return `${prefix}${name} (${positions})`;
|
|
13
|
+
});
|
|
14
|
+
return [...batters.map((batter, idx) => [batterNames[idx], batter.stats.batting.atBats.toString(), batter.stats.batting.runs.toString(), batter.stats.batting.hits.toString(), batter.stats.batting.rbi.toString(), batter.stats.batting.baseOnBalls.toString(), batter.stats.batting.strikeOuts.toString(), batter.seasonStats.batting.avg, batter.seasonStats.batting.ops]), ['Totals', boxscoreTeam.teamStats.batting.atBats.toString(), boxscoreTeam.teamStats.batting.runs.toString(), boxscoreTeam.teamStats.batting.hits.toString(), boxscoreTeam.teamStats.batting.rbi.toString(), boxscoreTeam.teamStats.batting.baseOnBalls.toString(), boxscoreTeam.teamStats.batting.strikeOuts.toString(), '', '']];
|
|
15
|
+
}
|
|
16
|
+
function getPitcherRows(boxscoreTeam, players) {
|
|
17
|
+
const pitchers = boxscoreTeam.pitchers.map(pitcherId => boxscoreTeam.players['ID' + pitcherId]);
|
|
18
|
+
const pitcherNames = pitchers.map(pitcher => {
|
|
19
|
+
const name = players[`ID${pitcher.person.id}`].boxscoreName;
|
|
20
|
+
const note = pitcher.stats.pitching.note ? ` ${pitcher.stats.pitching.note}` : '';
|
|
21
|
+
return `${name}${note}`;
|
|
22
|
+
});
|
|
23
|
+
return [...pitchers.map((pitcher, idx) => [pitcherNames[idx], pitcher.stats.pitching.inningsPitched, pitcher.stats.pitching.hits.toString(), pitcher.stats.pitching.runs.toString(), pitcher.stats.pitching.earnedRuns.toString(), pitcher.stats.pitching.baseOnBalls.toString(), pitcher.stats.pitching.strikeOuts.toString(), pitcher.stats.pitching.homeRuns.toString(), pitcher.seasonStats.pitching.era]), ['Totals', boxscoreTeam.teamStats.pitching.inningsPitched, boxscoreTeam.teamStats.pitching.hits.toString(), boxscoreTeam.teamStats.pitching.runs.toString(), boxscoreTeam.teamStats.pitching.earnedRuns.toString(), boxscoreTeam.teamStats.pitching.baseOnBalls.toString(), boxscoreTeam.teamStats.pitching.strikeOuts.toString(), boxscoreTeam.teamStats.pitching.homeRuns.toString(), '']];
|
|
24
|
+
}
|
|
25
|
+
function BoxScore({
|
|
26
|
+
...props
|
|
27
|
+
}) {
|
|
28
|
+
const boxscore = useSelector(selectBoxscore);
|
|
29
|
+
const players = useSelector(selectPlayers);
|
|
30
|
+
const batterHeader = ['Batters', 'AB', 'R', 'H', 'RBI', 'BB', 'K', 'AVG', 'OPS'];
|
|
31
|
+
const batterWidths = ['auto', 4, 4, 4, 4, 4, 4, 6, 6];
|
|
32
|
+
const awayBatterRows = getBatterRows(boxscore.away, players);
|
|
33
|
+
const homeBatterRows = getBatterRows(boxscore.home, players);
|
|
34
|
+
const pitcherHeader = ['Pitchers', 'IP', 'H', 'R', 'ER', 'BB', 'K', 'HR', 'ERA'];
|
|
35
|
+
const pitcherWidths = ['auto', 5, 4, 4, 4, 4, 4, 4, 6];
|
|
36
|
+
const pitcherStart = Math.max(awayBatterRows.length, homeBatterRows.length) + 2;
|
|
37
|
+
const awayPitcherRows = getPitcherRows(boxscore.away, players);
|
|
38
|
+
const homePitcherRows = getPitcherRows(boxscore.home, players);
|
|
39
|
+
return /*#__PURE__*/React.createElement("element", props, /*#__PURE__*/React.createElement(Table, {
|
|
40
|
+
top: 0,
|
|
41
|
+
left: 0,
|
|
42
|
+
width: "50%-1",
|
|
43
|
+
headers: batterHeader,
|
|
44
|
+
widths: batterWidths,
|
|
45
|
+
rows: awayBatterRows
|
|
46
|
+
}), /*#__PURE__*/React.createElement(Table, {
|
|
47
|
+
top: 0,
|
|
48
|
+
left: "50%+1",
|
|
49
|
+
width: "50%-1",
|
|
50
|
+
headers: batterHeader,
|
|
51
|
+
widths: batterWidths,
|
|
52
|
+
rows: homeBatterRows
|
|
53
|
+
}), /*#__PURE__*/React.createElement(Table, {
|
|
54
|
+
top: pitcherStart,
|
|
55
|
+
left: 0,
|
|
56
|
+
width: "50%-1",
|
|
57
|
+
headers: pitcherHeader,
|
|
58
|
+
widths: pitcherWidths,
|
|
59
|
+
rows: awayPitcherRows
|
|
60
|
+
}), /*#__PURE__*/React.createElement(Table, {
|
|
61
|
+
top: pitcherStart,
|
|
62
|
+
left: "50%+1",
|
|
63
|
+
width: "50%-1",
|
|
64
|
+
headers: pitcherHeader,
|
|
65
|
+
widths: pitcherWidths,
|
|
66
|
+
rows: homePitcherRows
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
export default BoxScore;
|
|
@@ -1,7 +1,16 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { useSelector } from "react-redux/lib/alternate-renderers.js";
|
|
3
|
-
import
|
|
1
|
+
import React, { useEffect, useMemo } from 'react';
|
|
2
|
+
import { useDispatch, useSelector } from "react-redux/lib/alternate-renderers.js";
|
|
3
|
+
import figlet from 'figlet';
|
|
4
4
|
import LineScore from "./LineScore.js";
|
|
5
|
+
import { get } from "../config.js";
|
|
6
|
+
import { selectBoxscore, selectDecisions, selectGameStatus, selectLineScore, selectSelectedId, selectTeams, setReplayGame } from "../features/games.js";
|
|
7
|
+
import { resetTitle, setTitle } from "../screen.js";
|
|
8
|
+
import useKey from "../hooks/useKey.js";
|
|
9
|
+
import BoxScore from "./BoxScore.js";
|
|
10
|
+
import AllPlays from "./AllPlays.js";
|
|
11
|
+
const BOX_SCORE = 'BOX_SCORE';
|
|
12
|
+
const ALL_PLAYS = 'ALL_PLAYS';
|
|
13
|
+
const SCORING_PLAYS = 'SCORING_PLAYS';
|
|
5
14
|
const getPlayer = (id, boxscore) => {
|
|
6
15
|
var _boxscore$home, _boxscore$away;
|
|
7
16
|
const homePlayer = (_boxscore$home = boxscore.home) === null || _boxscore$home === void 0 || (_boxscore$home = _boxscore$home.players) === null || _boxscore$home === void 0 ? void 0 : _boxscore$home['ID' + id];
|
|
@@ -35,56 +44,98 @@ const formatDecisions = (decisions, boxscore) => {
|
|
|
35
44
|
}
|
|
36
45
|
return content.join('\n');
|
|
37
46
|
};
|
|
38
|
-
const formatScore = (status, linescore) => {
|
|
39
|
-
let display = '';
|
|
40
|
-
if (status.detailedState === 'Postponed') {
|
|
41
|
-
display = status.detailedState;
|
|
42
|
-
if (status.reason) {
|
|
43
|
-
display += '\n' + status.reason;
|
|
44
|
-
}
|
|
45
|
-
} else {
|
|
46
|
-
display = `\n${linescore.teams.away.runs} - ${linescore.teams.home.runs}`;
|
|
47
|
-
}
|
|
48
|
-
return display;
|
|
49
|
-
};
|
|
50
47
|
function FinishedGame() {
|
|
48
|
+
const dispatch = useDispatch();
|
|
49
|
+
const id = useSelector(selectSelectedId);
|
|
51
50
|
const boxscore = useSelector(selectBoxscore);
|
|
52
51
|
const decisions = useSelector(selectDecisions);
|
|
53
52
|
const linescore = useSelector(selectLineScore);
|
|
54
53
|
const status = useSelector(selectGameStatus);
|
|
55
54
|
const teams = useSelector(selectTeams);
|
|
55
|
+
const [view, setView] = React.useState(BOX_SCORE);
|
|
56
|
+
useKey('r', () => dispatch(setReplayGame(id)), {
|
|
57
|
+
key: 'R',
|
|
58
|
+
label: 'Replay'
|
|
59
|
+
});
|
|
60
|
+
useKey('b', () => setView(BOX_SCORE), {
|
|
61
|
+
key: 'B',
|
|
62
|
+
label: 'Box Score'
|
|
63
|
+
});
|
|
64
|
+
useKey('a', () => setView(ALL_PLAYS), {
|
|
65
|
+
key: 'A',
|
|
66
|
+
label: 'All Plays'
|
|
67
|
+
});
|
|
68
|
+
useKey('p', () => setView(SCORING_PLAYS), {
|
|
69
|
+
key: 'P',
|
|
70
|
+
label: 'Scoring Plays'
|
|
71
|
+
});
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (get('title')) {
|
|
74
|
+
const homeRuns = linescore.teams['home'].runs;
|
|
75
|
+
const awayRuns = linescore.teams['away'].runs;
|
|
76
|
+
const home = teams.home.abbreviation;
|
|
77
|
+
const away = teams.away.abbreviation;
|
|
78
|
+
setTitle(`${away} ${awayRuns} - ${home} ${homeRuns} F`);
|
|
79
|
+
return () => {
|
|
80
|
+
resetTitle();
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}, [get, linescore, resetTitle, setTitle, teams]);
|
|
84
|
+
const bigTextOptions = {
|
|
85
|
+
font: 'Small Block'
|
|
86
|
+
};
|
|
56
87
|
const awayTeam = `${teams.away.teamName}\n(${teams.away.record.wins}-${teams.away.record.losses})`;
|
|
88
|
+
const awayRuns = useMemo(() => figlet.textSync(linescore.teams.away.runs, bigTextOptions), [linescore.teams.away.runs]);
|
|
57
89
|
const homeTeam = `${teams.home.teamName}\n(${teams.home.record.wins}-${teams.home.record.losses})`;
|
|
58
|
-
|
|
59
|
-
|
|
90
|
+
const homeRuns = useMemo(() => figlet.textSync(linescore.teams.home.runs, bigTextOptions), [linescore.teams.home.runs]);
|
|
91
|
+
return /*#__PURE__*/React.createElement("element", null, status.detailedState === 'Postponed' ? /*#__PURE__*/React.createElement("element", null, /*#__PURE__*/React.createElement("box", {
|
|
92
|
+
top: 1
|
|
93
|
+
}, "status.detailedState"), /*#__PURE__*/React.createElement("box", {
|
|
94
|
+
top: 2
|
|
95
|
+
}, status.reason)) : /*#__PURE__*/React.createElement("element", {
|
|
96
|
+
top: 1
|
|
60
97
|
}, /*#__PURE__*/React.createElement("box", {
|
|
98
|
+
top: 1,
|
|
99
|
+
left: 0,
|
|
100
|
+
width: "25%",
|
|
61
101
|
content: awayTeam,
|
|
62
|
-
|
|
63
|
-
|
|
102
|
+
align: "right"
|
|
103
|
+
}), /*#__PURE__*/React.createElement("box", {
|
|
104
|
+
top: 0,
|
|
105
|
+
left: "25%",
|
|
106
|
+
width: "25%",
|
|
107
|
+
content: awayRuns,
|
|
64
108
|
align: "center"
|
|
65
109
|
}), /*#__PURE__*/React.createElement("box", {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
110
|
+
top: 0,
|
|
111
|
+
left: "50%",
|
|
112
|
+
width: "25%",
|
|
113
|
+
content: homeRuns,
|
|
70
114
|
align: "center"
|
|
71
115
|
}), /*#__PURE__*/React.createElement("box", {
|
|
116
|
+
top: 1,
|
|
117
|
+
left: "75%",
|
|
118
|
+
width: "25%",
|
|
72
119
|
content: homeTeam,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
top: "60%+1",
|
|
79
|
-
height: 3
|
|
120
|
+
align: "left"
|
|
121
|
+
}), /*#__PURE__*/React.createElement("element", {
|
|
122
|
+
top: 5,
|
|
123
|
+
left: 0,
|
|
124
|
+
width: "50%-2"
|
|
80
125
|
}, /*#__PURE__*/React.createElement(LineScore, {
|
|
81
|
-
align: "
|
|
126
|
+
align: "right",
|
|
82
127
|
final: true
|
|
83
128
|
})), /*#__PURE__*/React.createElement("element", {
|
|
84
|
-
top:
|
|
85
|
-
left: "50
|
|
129
|
+
top: 5,
|
|
130
|
+
left: "50%+2",
|
|
131
|
+
width: "50%-2",
|
|
132
|
+
align: "right"
|
|
86
133
|
}, /*#__PURE__*/React.createElement("box", {
|
|
87
134
|
content: formatDecisions(decisions, boxscore)
|
|
88
|
-
}))
|
|
135
|
+
})), /*#__PURE__*/React.createElement("element", {
|
|
136
|
+
top: 9
|
|
137
|
+
}, view === BOX_SCORE && /*#__PURE__*/React.createElement(BoxScore, null), view === ALL_PLAYS && /*#__PURE__*/React.createElement(AllPlays, null), view === SCORING_PLAYS && /*#__PURE__*/React.createElement(AllPlays, {
|
|
138
|
+
scoringOnly: true
|
|
139
|
+
}))));
|
|
89
140
|
}
|
|
90
141
|
export default FinishedGame;
|
package/dist/components/Game.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useEffect, useRef } from 'react';
|
|
2
2
|
import { useDispatch, useSelector } from "react-redux/lib/alternate-renderers.js";
|
|
3
|
-
import { fetchGame, selectGame, selectSelectedId, selectFullUpdateRequired } from "../features/games.js";
|
|
3
|
+
import { fetchGame, selectGame, selectSelectedId, selectFullUpdateRequired, selectDelay } from "../features/games.js";
|
|
4
4
|
import PreviewGame from "./PreviewGame.js";
|
|
5
5
|
import LiveGame from "./LiveGame.js";
|
|
6
6
|
import FinishedGame from "./FinishedGame.js";
|
|
@@ -11,13 +11,15 @@ function Game() {
|
|
|
11
11
|
const game = useSelector(selectGame);
|
|
12
12
|
const fullUpdateRequired = useSelector(selectFullUpdateRequired);
|
|
13
13
|
const id = useSelector(selectSelectedId);
|
|
14
|
+
const delay = useSelector(selectDelay);
|
|
14
15
|
const timerRef = useRef(null);
|
|
15
16
|
const timestampRef = useRef();
|
|
16
17
|
timestampRef.current = fullUpdateRequired ? null : game === null || game === void 0 || (_game$metaData = game.metaData) === null || _game$metaData === void 0 ? void 0 : _game$metaData.timeStamp;
|
|
17
18
|
const updateGameData = () => {
|
|
18
19
|
dispatch(fetchGame({
|
|
19
20
|
id,
|
|
20
|
-
start: timestampRef.current
|
|
21
|
+
start: timestampRef.current,
|
|
22
|
+
delay
|
|
21
23
|
})).unwrap().then(result => {
|
|
22
24
|
var _result$metaData;
|
|
23
25
|
const wait = (result && ((_result$metaData = result.metaData) === null || _result$metaData === void 0 ? void 0 : _result$metaData.wait) || 10) * 1000;
|
|
@@ -29,7 +31,7 @@ function Game() {
|
|
|
29
31
|
return () => {
|
|
30
32
|
clearTimeout(timerRef.current);
|
|
31
33
|
};
|
|
32
|
-
}, [id]);
|
|
34
|
+
}, [id, delay]);
|
|
33
35
|
if (!game) {
|
|
34
36
|
return /*#__PURE__*/React.createElement("element", null);
|
|
35
37
|
}
|
|
@@ -8,7 +8,7 @@ import { teamFavoriteStar } from "../utils.js";
|
|
|
8
8
|
import Grid from "./Grid.js";
|
|
9
9
|
import useKey from "../hooks/useKey.js";
|
|
10
10
|
const formatGame = game => {
|
|
11
|
-
const startTime = format(new Date(game.gameDate), 'p');
|
|
11
|
+
const startTime = game.status.startTimeTBD ? 'TBD' : format(new Date(game.gameDate), 'p');
|
|
12
12
|
const start = game.doubleHeader === 'Y' && game.gameNumber > 1 ? 'Game ' + game.gameNumber : startTime;
|
|
13
13
|
const teamName = team => {
|
|
14
14
|
const star = teamFavoriteStar(team.team);
|
|
@@ -1,12 +1,63 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { useSelector } from "react-redux/lib/alternate-renderers.js";
|
|
2
3
|
import Count from "./Count.js";
|
|
3
4
|
import Bases from "./Bases.js";
|
|
5
|
+
import BoxScore from "./BoxScore.js";
|
|
4
6
|
import LineScore from "./LineScore.js";
|
|
5
7
|
import Matchup from "./Matchup.js";
|
|
6
8
|
import AtBat from "./AtBat.js";
|
|
7
9
|
import AllPlays from "./AllPlays.js";
|
|
8
10
|
import InningDisplay from "./InningDisplay.js";
|
|
11
|
+
import { get } from "../config.js";
|
|
12
|
+
import { selectGameStatus, selectLineScore, selectTeams } from "../features/games.js";
|
|
13
|
+
import { resetTitle, setTitle } from "../screen.js";
|
|
14
|
+
import useKey from "../hooks/useKey.js";
|
|
15
|
+
const GAME_STATUS = 'GAME_STATUS';
|
|
16
|
+
const BOX_SCORE = 'BOX_SCORE';
|
|
17
|
+
const SCORING_PLAYS = 'SCORING_PLAYS';
|
|
9
18
|
function LiveGame() {
|
|
19
|
+
const gameStatus = useSelector(selectGameStatus);
|
|
20
|
+
const linescore = useSelector(selectLineScore);
|
|
21
|
+
const teams = useSelector(selectTeams);
|
|
22
|
+
const [view, setView] = React.useState(GAME_STATUS);
|
|
23
|
+
useKey('g', () => setView(GAME_STATUS), {
|
|
24
|
+
key: 'G',
|
|
25
|
+
label: 'Game Status'
|
|
26
|
+
});
|
|
27
|
+
useKey('b', () => setView(BOX_SCORE), {
|
|
28
|
+
key: 'B',
|
|
29
|
+
label: 'Box Score'
|
|
30
|
+
});
|
|
31
|
+
useKey('p', () => setView(SCORING_PLAYS), {
|
|
32
|
+
key: 'P',
|
|
33
|
+
label: 'Scoring Plays'
|
|
34
|
+
});
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (get('title')) {
|
|
37
|
+
const homeRuns = linescore.teams['home'].runs;
|
|
38
|
+
const awayRuns = linescore.teams['away'].runs;
|
|
39
|
+
const home = teams.home.abbreviation;
|
|
40
|
+
const away = teams.away.abbreviation;
|
|
41
|
+
let inning = '';
|
|
42
|
+
if (gameStatus.detailedState === 'Postponed') {
|
|
43
|
+
inning = 'PPD';
|
|
44
|
+
} else if (gameStatus.detailedState === 'Cancelled') {
|
|
45
|
+
inning = 'C';
|
|
46
|
+
} else if (gameStatus.detailedState === 'Final') {
|
|
47
|
+
inning = 'F';
|
|
48
|
+
} else if (gameStatus.detailedState !== 'Pre-Game' && gameStatus.detailedState !== 'Warmup') {
|
|
49
|
+
const currentInning = linescore.currentInning;
|
|
50
|
+
if (currentInning) {
|
|
51
|
+
const upDown = linescore.isTopInning ? '▲' : '▼';
|
|
52
|
+
inning = ` ${upDown} ${currentInning}`;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
setTitle(`${away} ${awayRuns} - ${home} ${homeRuns}${inning}`);
|
|
56
|
+
return () => {
|
|
57
|
+
resetTitle();
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}, [gameStatus, get, linescore, resetTitle, setTitle, teams]);
|
|
10
61
|
return /*#__PURE__*/React.createElement("element", null, /*#__PURE__*/React.createElement("element", {
|
|
11
62
|
top: 0,
|
|
12
63
|
left: 1,
|
|
@@ -29,7 +80,7 @@ function LiveGame() {
|
|
|
29
80
|
type: "line",
|
|
30
81
|
top: 3,
|
|
31
82
|
width: "100%"
|
|
32
|
-
}), /*#__PURE__*/React.createElement("element", {
|
|
83
|
+
}), view === GAME_STATUS && /*#__PURE__*/React.createElement("element", {
|
|
33
84
|
top: 4,
|
|
34
85
|
left: 1
|
|
35
86
|
}, /*#__PURE__*/React.createElement("element", {
|
|
@@ -46,6 +97,16 @@ function LiveGame() {
|
|
|
46
97
|
}), /*#__PURE__*/React.createElement("element", {
|
|
47
98
|
left: "50%+2",
|
|
48
99
|
width: "50%-2"
|
|
49
|
-
}, /*#__PURE__*/React.createElement(AllPlays,
|
|
100
|
+
}, /*#__PURE__*/React.createElement(AllPlays, {
|
|
101
|
+
reverse: true
|
|
102
|
+
}))), view === BOX_SCORE && /*#__PURE__*/React.createElement("element", {
|
|
103
|
+
top: 4,
|
|
104
|
+
left: 1
|
|
105
|
+
}, /*#__PURE__*/React.createElement(BoxScore, null)), view === SCORING_PLAYS && /*#__PURE__*/React.createElement("element", {
|
|
106
|
+
top: 4,
|
|
107
|
+
left: 1
|
|
108
|
+
}, /*#__PURE__*/React.createElement(AllPlays, {
|
|
109
|
+
scoringOnly: true
|
|
110
|
+
})));
|
|
50
111
|
}
|
|
51
112
|
export default LiveGame;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
2
|
import { useSelector } from "react-redux/lib/alternate-renderers.js";
|
|
3
|
-
import { format } from 'date-fns';
|
|
3
|
+
import { format, isSameDay } from 'date-fns';
|
|
4
|
+
import { get } from "../config.js";
|
|
4
5
|
import { selectTeams, selectVenue, selectStartTime, selectBoxscore, selectProbablePitchers, selectGameStatus } from "../features/games.js";
|
|
6
|
+
import { resetTitle, setTitle } from "../screen.js";
|
|
5
7
|
const formatPitcherName = pitcher => {
|
|
6
8
|
let display = pitcher.person.fullName;
|
|
7
9
|
const number = pitcher.jerseyNumber;
|
|
@@ -33,6 +35,24 @@ function PreviewGame() {
|
|
|
33
35
|
const away = formatTeam(teams, probables, boxscore, 'away');
|
|
34
36
|
const home = formatTeam(teams, probables, boxscore, 'home');
|
|
35
37
|
const formattedStart = status.startTimeTBD ? 'Start time TBD' : format(new Date(startTime), 'MMMM d, yyy p');
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (get('title')) {
|
|
40
|
+
const home = teams.home.abbreviation;
|
|
41
|
+
const away = teams.away.abbreviation;
|
|
42
|
+
|
|
43
|
+
// Only show the date if it's not today.
|
|
44
|
+
const startDate = new Date(startTime);
|
|
45
|
+
const today = new Date();
|
|
46
|
+
let start = format(startDate, 'p');
|
|
47
|
+
if (!isSameDay(startDate, today)) {
|
|
48
|
+
start = `${format(startDate, 'MMMM d, yyy')} ${start}`;
|
|
49
|
+
}
|
|
50
|
+
setTitle(`${away} - ${home} @ ${start}`);
|
|
51
|
+
return () => {
|
|
52
|
+
resetTitle();
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}, [get, resetTitle, setTitle, startTime, teams]);
|
|
36
56
|
return /*#__PURE__*/React.createElement("element", null, /*#__PURE__*/React.createElement("element", {
|
|
37
57
|
height: "60%"
|
|
38
58
|
}, /*#__PURE__*/React.createElement("box", {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
function formatRow(row, widths) {
|
|
4
|
+
return row.map((cell, idx) => idx === 0 ? cell.padEnd(widths[idx]) : cell.padStart(widths[idx])).join('');
|
|
5
|
+
}
|
|
6
|
+
function Table({
|
|
7
|
+
headers,
|
|
8
|
+
widths,
|
|
9
|
+
rows,
|
|
10
|
+
...rest
|
|
11
|
+
}) {
|
|
12
|
+
const resolvedWidths = widths.map((width, idx) => {
|
|
13
|
+
if (width !== 'auto') {
|
|
14
|
+
return width;
|
|
15
|
+
}
|
|
16
|
+
return Math.max((headers[idx] || '').length, ...rows.map(row => (row[idx] || '').length));
|
|
17
|
+
});
|
|
18
|
+
const headerRow = formatRow(headers, resolvedWidths);
|
|
19
|
+
const contentRows = rows.map(row => formatRow(row, resolvedWidths));
|
|
20
|
+
return /*#__PURE__*/React.createElement("element", rest, /*#__PURE__*/React.createElement("box", {
|
|
21
|
+
top: 0,
|
|
22
|
+
left: 0,
|
|
23
|
+
width: "100%",
|
|
24
|
+
height: 1,
|
|
25
|
+
fg: "black",
|
|
26
|
+
bg: "white",
|
|
27
|
+
wrap: false,
|
|
28
|
+
content: headerRow
|
|
29
|
+
}), /*#__PURE__*/React.createElement("box", {
|
|
30
|
+
top: 1,
|
|
31
|
+
left: 0,
|
|
32
|
+
width: "100%",
|
|
33
|
+
height: rows.length,
|
|
34
|
+
wrap: false,
|
|
35
|
+
content: contentRows.join('\n')
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
Table.propTypes = {
|
|
39
|
+
headers: PropTypes.arrayOf(PropTypes.string),
|
|
40
|
+
widths: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.oneOf(['auto'])])),
|
|
41
|
+
rows: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)),
|
|
42
|
+
top: PropTypes.number
|
|
43
|
+
};
|
|
44
|
+
export default Table;
|
package/dist/config.js
CHANGED
|
@@ -75,6 +75,15 @@ const schema = {
|
|
|
75
75
|
enum: ['ATL', 'AZ', 'BAL', 'BOS', 'CHC', 'CIN', 'CLE', 'COL', 'CWS', 'DET', 'HOU', 'KC', 'LAA', 'LAD', 'MIA', 'MIL', 'MIN', 'NYM', 'NYY', 'OAK', 'PHI', 'PIT', 'SD', 'SEA', 'SF', 'STL', 'TB', 'TEX', 'TOR', 'WSH']
|
|
76
76
|
},
|
|
77
77
|
default: []
|
|
78
|
+
},
|
|
79
|
+
'title': {
|
|
80
|
+
type: 'boolean',
|
|
81
|
+
default: false
|
|
82
|
+
},
|
|
83
|
+
'live-delay': {
|
|
84
|
+
type: 'number',
|
|
85
|
+
default: 0,
|
|
86
|
+
minimum: 0
|
|
78
87
|
}
|
|
79
88
|
};
|
|
80
89
|
const config = new Conf({
|
|
@@ -88,9 +97,13 @@ function serialize(value) {
|
|
|
88
97
|
return value;
|
|
89
98
|
}
|
|
90
99
|
function deserialize(key, value) {
|
|
91
|
-
var _schema$key;
|
|
100
|
+
var _schema$key, _schema$key2, _schema$key3;
|
|
92
101
|
if (value && ((_schema$key = schema[key]) === null || _schema$key === void 0 ? void 0 : _schema$key.type) === 'array') {
|
|
93
102
|
return value.split(/\s*,\s*/);
|
|
103
|
+
} else if (((_schema$key2 = schema[key]) === null || _schema$key2 === void 0 ? void 0 : _schema$key2.type) === 'boolean') {
|
|
104
|
+
return value === 'true';
|
|
105
|
+
} else if (((_schema$key3 = schema[key]) === null || _schema$key3 === void 0 ? void 0 : _schema$key3.type) === 'number') {
|
|
106
|
+
return parseInt(value);
|
|
94
107
|
}
|
|
95
108
|
return value;
|
|
96
109
|
}
|
package/dist/features/games.js
CHANGED
|
@@ -6,19 +6,42 @@ const {
|
|
|
6
6
|
createSelector
|
|
7
7
|
} = reduxjsToolkit;
|
|
8
8
|
import jsonpatch from 'json-patch';
|
|
9
|
+
import { UTCDate } from '@date-fns/utc';
|
|
10
|
+
import { addSeconds, differenceInSeconds, format } from 'date-fns';
|
|
11
|
+
import logger from "../logger.js";
|
|
12
|
+
import { get } from "../config.js";
|
|
9
13
|
const initialState = {
|
|
10
14
|
loading: false,
|
|
11
15
|
fullUpdateRequired: false,
|
|
12
16
|
error: null,
|
|
13
17
|
selectedId: null,
|
|
18
|
+
delay: 0,
|
|
14
19
|
games: {}
|
|
15
20
|
};
|
|
21
|
+
function makeDiffParams(start, end) {
|
|
22
|
+
const endParam = start ? '&endTimecode=' : '?timecode=';
|
|
23
|
+
const startParam = start ? `/diffPatch?startTimecode=${start}` : '';
|
|
24
|
+
if (end) {
|
|
25
|
+
return `${startParam}${endParam}${end}`;
|
|
26
|
+
} else {
|
|
27
|
+
return startParam;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
16
30
|
export const fetchGame = createAsyncThunk('games/fetch', async ({
|
|
17
31
|
id,
|
|
18
|
-
start
|
|
32
|
+
start,
|
|
33
|
+
delay
|
|
19
34
|
}) => {
|
|
20
|
-
const
|
|
35
|
+
const end = delay > 0 ? format(addSeconds(new UTCDate(Date.now()), -delay), 'yyyyMMdd_HHmmss') : null;
|
|
36
|
+
const diffParams = makeDiffParams(start, end);
|
|
21
37
|
const url = `https://statsapi.mlb.com/api/v1.1/game/${id}/feed/live${diffParams}`;
|
|
38
|
+
logger.info(`GET ${url}`);
|
|
39
|
+
const response = await axios.get(url);
|
|
40
|
+
return response.data;
|
|
41
|
+
});
|
|
42
|
+
export const setReplayGame = createAsyncThunk('games/setReplay', async id => {
|
|
43
|
+
const url = `https://statsapi.mlb.com/api/v1.1/game/${id}/feed/live?fields=gamePk,gameData,datetime,gameInfo,dateTime,firstPitch`;
|
|
44
|
+
logger.info(`GET ${url}`);
|
|
22
45
|
const response = await axios.get(url);
|
|
23
46
|
return response.data;
|
|
24
47
|
});
|
|
@@ -26,11 +49,28 @@ export const gamesSlice = createSlice({
|
|
|
26
49
|
name: 'games',
|
|
27
50
|
initialState,
|
|
28
51
|
reducers: {
|
|
29
|
-
|
|
52
|
+
setLiveGame(state, action) {
|
|
30
53
|
state.selectedId = action.payload;
|
|
54
|
+
state.delay = get('live-delay') || 0;
|
|
31
55
|
}
|
|
32
56
|
},
|
|
33
57
|
extraReducers: builder => {
|
|
58
|
+
builder.addCase(setReplayGame.pending, state => {
|
|
59
|
+
state.loading = true;
|
|
60
|
+
});
|
|
61
|
+
builder.addCase(setReplayGame.fulfilled, (state, action) => {
|
|
62
|
+
state.loading = false;
|
|
63
|
+
state.error = null;
|
|
64
|
+
state.selectedId = action.payload.gamePk;
|
|
65
|
+
state.fullUpdateRequired = true;
|
|
66
|
+
const start = action.payload.gameData.gameInfo.firstPitch || action.payload.gameData.datetime.dateTime;
|
|
67
|
+
state.delay = differenceInSeconds(new Date(), start);
|
|
68
|
+
});
|
|
69
|
+
builder.addCase(setReplayGame.rejected, (state, action) => {
|
|
70
|
+
state.loading = false;
|
|
71
|
+
state.fullUpdateRequired = true;
|
|
72
|
+
state.error = action.error;
|
|
73
|
+
});
|
|
34
74
|
builder.addCase(fetchGame.pending, state => {
|
|
35
75
|
state.loading = true;
|
|
36
76
|
});
|
|
@@ -70,13 +110,14 @@ export const gamesSlice = createSlice({
|
|
|
70
110
|
}
|
|
71
111
|
});
|
|
72
112
|
export const {
|
|
73
|
-
|
|
113
|
+
setLiveGame
|
|
74
114
|
} = gamesSlice.actions;
|
|
75
115
|
const gamesRoot = state => state.games;
|
|
76
116
|
export const selectLoading = createSelector(gamesRoot, root => root.loading);
|
|
77
117
|
export const selectError = createSelector(gamesRoot, root => root.error);
|
|
78
118
|
export const selectFullUpdateRequired = createSelector(gamesRoot, root => root.fullUpdateRequired);
|
|
79
119
|
export const selectSelectedId = createSelector(gamesRoot, root => root.selectedId);
|
|
120
|
+
export const selectDelay = createSelector(gamesRoot, root => root.delay);
|
|
80
121
|
export const selectGame = createSelector([gamesRoot, selectSelectedId], (root, id) => root.games[id]);
|
|
81
122
|
const selectLiveData = createSelector(selectGame, game => game.liveData);
|
|
82
123
|
const selectPlays = createSelector(selectLiveData, data => data.plays);
|
|
@@ -90,6 +131,7 @@ export const selectLineScore = createSelector(selectLiveData, data => data.lines
|
|
|
90
131
|
export const selectDecisions = createSelector(selectLiveData, data => data.decisions);
|
|
91
132
|
const selectGameData = createSelector(selectGame, game => game.gameData);
|
|
92
133
|
export const selectGameStatus = createSelector(selectGameData, game => game.status);
|
|
134
|
+
export const selectPlayers = createSelector(selectGameData, gameData => gameData.players);
|
|
93
135
|
export const selectTeams = createSelector(selectGameData, gameData => gameData.teams);
|
|
94
136
|
export const selectVenue = createSelector(selectGameData, gameData => gameData.venue);
|
|
95
137
|
export const selectStartTime = createSelector(selectGameData, gameData => {
|
package/dist/main.js
CHANGED
|
@@ -5,7 +5,7 @@ import screen from "./screen.js";
|
|
|
5
5
|
import store from "./store/index.js";
|
|
6
6
|
import log from "./logger.js";
|
|
7
7
|
import App from "./components/App.js";
|
|
8
|
-
export default async function startInterface() {
|
|
8
|
+
export default async function startInterface(options) {
|
|
9
9
|
raf.polyfill();
|
|
10
10
|
process.on('uncaughtException', function (error) {
|
|
11
11
|
log.error('UNCAUGHT EXCEPTION\n' + JSON.stringify(error) + '\n' + error.stack);
|
|
@@ -16,5 +16,7 @@ export default async function startInterface() {
|
|
|
16
16
|
const reactBlessed = await import('react-blessed');
|
|
17
17
|
reactBlessed.render( /*#__PURE__*/React.createElement(Provider, {
|
|
18
18
|
store: store
|
|
19
|
-
}, /*#__PURE__*/React.createElement(App,
|
|
19
|
+
}, /*#__PURE__*/React.createElement(App, {
|
|
20
|
+
replayId: options.replay
|
|
21
|
+
})), screen());
|
|
20
22
|
}
|
package/dist/screen.js
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
import blessed from 'blessed';
|
|
2
2
|
let screen;
|
|
3
|
+
const DEFAULT_TITLE = 'Playball!';
|
|
3
4
|
function getScreen() {
|
|
4
5
|
if (screen === undefined) {
|
|
5
6
|
screen = blessed.screen({
|
|
6
7
|
autoPadding: true,
|
|
7
8
|
debug: true,
|
|
8
9
|
smartCSR: true,
|
|
9
|
-
title:
|
|
10
|
+
title: DEFAULT_TITLE,
|
|
10
11
|
handleUncaughtExceptions: false
|
|
11
12
|
});
|
|
12
13
|
screen.key(['escape', 'q', 'C-c'], () => process.exit(0));
|
|
13
14
|
}
|
|
14
15
|
return screen;
|
|
15
16
|
}
|
|
17
|
+
export function resetTitle() {
|
|
18
|
+
getScreen().title = DEFAULT_TITLE;
|
|
19
|
+
}
|
|
20
|
+
export function setTitle(title) {
|
|
21
|
+
getScreen().title = title;
|
|
22
|
+
}
|
|
16
23
|
export default getScreen;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "playball",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "Watch MLB games from the comfort of your terminal",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"MLB",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"lint": "eslint --ext .jsx,.js src",
|
|
17
17
|
"start": "babel src --out-dir dist --watch",
|
|
18
18
|
"prepublishOnly": "npm run build",
|
|
19
|
-
"react-devtools": "react-devtools"
|
|
19
|
+
"react-devtools": "react-devtools",
|
|
20
|
+
"test": "echo \"No tests yet\""
|
|
20
21
|
},
|
|
21
22
|
"bin": {
|
|
22
23
|
"playball": "./bin/playball.js"
|
|
@@ -37,12 +38,14 @@
|
|
|
37
38
|
"preferGlobal": true,
|
|
38
39
|
"license": "MIT",
|
|
39
40
|
"dependencies": {
|
|
41
|
+
"@date-fns/utc": "^2.1.1",
|
|
40
42
|
"@reduxjs/toolkit": "^1.8.0",
|
|
41
43
|
"axios": "^0.26.1",
|
|
42
44
|
"blessed": "^0.1.81",
|
|
43
45
|
"commander": "^11.0.0",
|
|
44
46
|
"conf": "^11.0.1",
|
|
45
|
-
"date-fns": "^
|
|
47
|
+
"date-fns": "^4.1.0",
|
|
48
|
+
"figlet": "^1.10.0",
|
|
46
49
|
"json-patch": "^0.7.0",
|
|
47
50
|
"prop-types": "^15.8.1",
|
|
48
51
|
"raf": "^3.4.1",
|