diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/Chart.js | 1 | ||||
-rw-r--r-- | src/Dashboard.js | 3 | ||||
-rw-r--r-- | src/PatternTable.js | 40 | ||||
-rw-r--r-- | src/Settings.js | 29 | ||||
-rw-r--r-- | src/background.js | 159 | ||||
-rw-r--r-- | src/msg.js | 6 | ||||
-rw-r--r-- | src/popup.js | 146 |
7 files changed, 259 insertions, 125 deletions
diff --git a/src/Chart.js b/src/Chart.js index 88ab72c..983436a 100644 --- a/src/Chart.js +++ b/src/Chart.js @@ -11,6 +11,7 @@ export function getChartData(start, end, patterns, calendars, calEventsGetter) { let event_pms = []; for (let id in calendars) { + if (!calendars[id].enabled) continue; let filtered = patterns.filter(p => p.cal.regex.test(calendars[id].name)); if (filtered.length > 0) event_pms.push(calEventsGetter(id, start, end) diff --git a/src/Dashboard.js b/src/Dashboard.js index 748d117..6c17d2c 100644 --- a/src/Dashboard.js +++ b/src/Dashboard.js @@ -1,8 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import 'typeface-roboto'; -import { withStyles } from '@material-ui/core/styles'; -import { MuiThemeProvider } from '@material-ui/core/styles'; +import { withStyles, MuiThemeProvider } from '@material-ui/core/styles'; import CssBaseline from '@material-ui/core/CssBaseline'; import AppBar from '@material-ui/core/AppBar'; import Toolbar from '@material-ui/core/Toolbar'; diff --git a/src/PatternTable.js b/src/PatternTable.js index 3e16bbf..e054f9e 100644 --- a/src/PatternTable.js +++ b/src/PatternTable.js @@ -13,15 +13,19 @@ import { CalendarField, EventField } from './RegexField'; import theme from './theme'; const styles = theme => ({ - deleteButtonShow: { + deleteButton: { + width: '100%', position: 'absolute', + marginRight: '2em', right: 0, - height: 48 + height: 48, }, deleteButtonHide: { display: 'none' }, + deleteButtonShow: {}, deleteIcon: { + position: 'absolute', height: '100%', cursor: 'pointer' }, @@ -58,27 +62,35 @@ class PatternTable extends React.Component { const { classes, calendars, patterns } = this.props; const { rowsPerPage, page } = this.state; const nDummy = rowsPerPage - Math.min(rowsPerPage, patterns.length - page * rowsPerPage); - let rows = patterns.slice(page * rowsPerPage, (page + 1) * rowsPerPage).map(p => ( - <TableRow - onMouseOver={() => this.setState({ activePattern: p.idx })} - onMouseOut={() => this.setState({ activePattern: null })}> + let rows = patterns.slice(page * rowsPerPage, (page + 1) * rowsPerPage).map((p, i) => { + let setActive = () => this.setState({ activePattern: p.idx }); + let unsetActive = () => this.setState({ activePattern: null }); + return [<TableRow key={i * 2} + onMouseOver={setActive} onMouseOut={unsetActive} + className={classes.deleteButton}> + <td> + <span className={this.state.activePattern !== p.idx ? classes.deleteButtonHide : classes.deleteButtonShow}> + <DeleteOutlinedIcon + className={classes.deleteIcon} + onClick={() => this.props.onRemovePattern(p.idx)} /> + </span> + </td> + </TableRow>, + <TableRow key={i * 2 + 1} onMouseOver={setActive} onMouseOut={unsetActive}> { - patternHead.map(s => { + patternHead.map((s, i) => { const CustomText = s.elem; return ( - <TableCell> + <TableCell key={i}> <CustomText value={p[s.field]} calendars={calendars} onChange={event => this.props.onUpdatePattern(s.field, p.idx, event.target.value)}/> </TableCell>)}) } - <span className={this.state.activePattern === p.idx ? classes.deleteButtonShow : classes.deleteButtonHide}> - <DeleteOutlinedIcon - className={classes.deleteIcon} - onClick={() => this.props.onRemovePattern(p.idx)} /> - </span> - </TableRow>)); + </TableRow>] + }); + rows.flat(); return ( <div> diff --git a/src/Settings.js b/src/Settings.js index 0951e27..9358eee 100644 --- a/src/Settings.js +++ b/src/Settings.js @@ -78,6 +78,13 @@ class TrackedPeriod extends React.Component { } }; + static toValue(value) { + if (isNaN(value)) return null; + let v = parseInt(value, 10); + if (v < 0 || v > 999) return null; + return v; + } + render() { let { classes, fromDuration, toDuration, nameOnChange, fromOnChange, toOnChange, name } = this.props; let units = [ @@ -92,12 +99,14 @@ class TrackedPeriod extends React.Component { value={name} onChange={event => nameOnChange(event.target.value)}/>: from <TextField + error={TrackedPeriod.toValue(fromDuration.value) === null} inputProps={{style: TrackedPeriod.styles.periodValue}} value={fromDuration.value} onChange={this.valueOnChange(fromDuration, fromOnChange)} /> <Select value={fromDuration.unit} onChange={this.unitOnChange(fromDuration, fromOnChange)}>{units}</Select> ago to <TextField + error={TrackedPeriod.toValue(toDuration.value) === null} inputProps={{style: TrackedPeriod.styles.periodValue}} value={toDuration.value} onChange={this.valueOnChange(toDuration, toOnChange)} /> @@ -118,6 +127,7 @@ class Settings extends React.Component { snackBarMsg: 'unknown', dialogOpen: false, dialogMsg: {title: '', message: ''}, + calendarsLoading: false, }; constructor(props) { @@ -186,12 +196,14 @@ class Settings extends React.Component { }).then(() => this.setState({ calendars })); } - loadAll = loadDefaultPatterns => { + async loadAll(loadDefaultPatterns) { + await new Promise(resolver => (this.setState({ calendarsLoading: true }, resolver))); + let colors = gapi.getAuthToken().then(gapi.getColors).then(color => { return color.calendar; }); let cals = gapi.getAuthToken().then(gapi.getCalendars); - Promise.all([colors, cals]).then(([colors, items]) => { + await Promise.all([colors, cals]).then(([colors, items]) => { var cals = {}; items.forEach(item => { cals[item.id] = { @@ -210,6 +222,7 @@ class Settings extends React.Component { }), 'main'); } }); + this.setState({ calendarsLoading: false }); }; loadCalendars = calendars => { @@ -276,7 +289,11 @@ class Settings extends React.Component { updateTrackedPeriods = trackedPeriods => { this.msgClient.sendMsg({ type: msgType.updateConfig, - data: { trackedPeriods: trackedPeriods.map(p => p.deflate()) } + data: { trackedPeriods: trackedPeriods.map(p => ({ + name: p.name, + start: p.start.deflate(), + end: p.end.deflate() + })) } }).then(() => this.setState({...this.state.config, trackedPeriods })); } @@ -332,7 +349,9 @@ class Settings extends React.Component { <IconButton style={{marginBottom: '0.12em', marginRight: '0.5em'}} onClick={() => this.loadAll(false)} - disabled={!this.state.isLoggedIn}><RefreshIcon /></IconButton> + disabled={this.state.calendarsLoading || !this.state.isLoggedIn}> + <RefreshIcon /> + </IconButton> Calendars </STableCell> <STableCell className={classes.tableContent}> @@ -378,7 +397,7 @@ class Settings extends React.Component { <STableCell className={classes.tableContent}> {this.state.config.trackedPeriods && this.state.config.trackedPeriods.map((p, idx) => - <FormGroup> + <FormGroup key={idx}> <TrackedPeriod name={p.name} fromDuration={p.start} diff --git a/src/background.js b/src/background.js index 4bee605..1907a30 100644 --- a/src/background.js +++ b/src/background.js @@ -1,6 +1,9 @@ import * as gapi from './gapi'; import { msgType, Msg } from './msg'; import { Duration } from './duration'; +import moment from 'moment'; +import { getChartData } from './Chart'; +import { PatternEntry } from './pattern'; let mainPatterns = []; let analyzePatterns = []; @@ -13,6 +16,118 @@ let config = { {name: 'This Week', start: Duration.weeks(1), end: Duration.weeks(0)}, {name: 'This Month', start: Duration.months(1), end: Duration.months(0)}] }; +let mainGraphData = [{}, {}, {}, {}]; +let dirtyMetadata = false; + +function loadMetadata() { + return new Promise(resolver => chrome.storage.local.get([ + 'calendars', 'config', 'mainPatterns', 'analyzePatterns', + ], function(items) { + if (chrome.runtime.lastError) + { + console.error("error while loading saved metadata"); + return; + } + if (!items.hasOwnProperty('config')) + { + console.log("no saved metadata"); + return; + } + console.log('metadata loaded'); + config = { + trackedPeriods: items.config.trackedPeriods.map(p => ({ + name: p.name, + start: Duration.inflate(p.start), + end: Duration.inflate(p.end), + })) + }; + calendars = items.calendars; + mainPatterns = items.mainPatterns.map(p => PatternEntry.inflate(p)); + analyzePatterns = items.analyzePatterns.map(p => PatternEntry.inflate(p)); + resolver(); + })); +} + +function saveMetadata() { + return new Promise(resolver => chrome.storage.local.set({ + calendars, + config: { + trackedPeriods: config.trackedPeriods.map(p => ({ + name: p.name, + start: p.start.deflate(), + end: p.end.deflate() + })) + }, + mainPatterns: mainPatterns.map(p => p.deflate()), + analyzePatterns: analyzePatterns.map(p => p.deflate()) + }, function() { + console.log('metadata saved'); + resolver(); + })); +} + +function getCalEvents(id, start, end) { + if (!calData.hasOwnProperty(id)) + calData[id] = new gapi.GCalendar(id, calendars[id].summary); + return calData[id].getEvents(new Date(start), new Date(end)) + .catch(e => { + console.log(`cannot load calendar ${id}`, e); + calendars[id].enabled = false; + return []; + }); +} + +function updateMainGraphData() { + console.log('refreshing graph data'); + console.log(mainGraphData); + let pms = []; + for (let i = 0; i < config.trackedPeriods.length; i++) + { + let p = config.trackedPeriods[i]; + let startD = p.start.toMoment(); + let endD = p.end.toMoment(); + if (!(startD && endD)) return; + let start = moment().endOf('day'); + if (endD.valueOf() == 0) { + switch (p.start.unit) { + case 'days': start = moment().endOf('day'); break; + case 'weeks': start = moment().endOf('week'); break; + case 'months': start = moment().endOf('month'); break; + default: + } + } + let end = start.clone(); + start.subtract(startD); + end.subtract(endD); + pms.push(getChartData( + start.toDate(), + end.toDate(), + mainPatterns, + calendars, + (id, start,end) => getCalEvents(id, start, end).then(d => d.map(e => ({ + id: e.id, + start: e.start.getTime(), + end: e.end.getTime() + })))).then(results => { + mainGraphData[i] = { + name: p.name, start, end, + data: results.patternGraphData + }; + })); + } + return Promise.all(pms); +} + +async function pollSync() { + await updateMainGraphData(); + if (dirtyMetadata) + await saveMetadata().then(() => dirtyMetadata = false); + return new Promise(resolver => ( + window.setTimeout(() => { resolver(); pollSync();}, 10000) + )); +} + +loadMetadata().then(() => pollSync()); chrome.runtime.onConnect.addListener(function(port) { console.assert(port.name == 'main'); @@ -21,10 +136,12 @@ chrome.runtime.onConnect.addListener(function(port) { console.log(msg); switch (msg.type) { case msgType.updatePatterns: { + let patterns = msg.data.patterns.map(p => PatternEntry.inflate(p)); if (msg.data.id == 'analyze') - analyzePatterns = msg.data.patterns; + analyzePatterns = patterns; else - mainPatterns = msg.data.patterns; + mainPatterns = patterns; + dirtyMetadata = true; port.postMessage(msg.genResp(null)); break; } @@ -34,15 +151,12 @@ chrome.runtime.onConnect.addListener(function(port) { patterns = analyzePatterns; else patterns = mainPatterns; - port.postMessage(msg.genResp(patterns)); + port.postMessage(msg.genResp(patterns.map(p => p.deflate()))); break; } case msgType.updateCalendars: { calendars = msg.data; - for (let id in calendars) { - if (!calData.hasOwnProperty(id)) - calData[id] = new gapi.GCalendar(id, calendars[id].summary); - } + dirtyMetadata = true; port.postMessage(msg.genResp(null)); break; } @@ -58,12 +172,7 @@ chrome.runtime.onConnect.addListener(function(port) { break; } case msgType.getCalEvents: { - calData[msg.data.id].getEvents(new Date(msg.data.start), new Date(msg.data.end)) - .catch(e => { - console.log(`cannot load calendar ${msg.data.id}`, e); - return []; - }) - .then(data => { + getCalEvents(msg.data.id, msg.data.start, msg.data.end).then(data => { console.log(data); let resp = msg.genResp(data.map(e => { return { @@ -78,8 +187,12 @@ chrome.runtime.onConnect.addListener(function(port) { break; } case msgType.updateConfig: { - for (let prop in msg.data) - config[prop] = msg.data[prop]; + config.trackedPeriods = msg.data.trackedPeriods.map(p => ({ + name: p.name, + start: Duration.inflate(p.start), + end: Duration.inflate(p.end) + })); + dirtyMetadata = true; port.postMessage(msg.genResp(null)); break; } @@ -89,12 +202,18 @@ chrome.runtime.onConnect.addListener(function(port) { port.postMessage(msg.genResp(res)); break; } + case msgType.getGraphData: { + (msg.data.sync ? updateMainGraphData() : Promise.resolve()).then(() => ( + port.postMessage(msg.genResp(mainGraphData.map(d => ({ + name: d.name, + start: d.start.toISOString(), + end: d.end.toISOString(), + data: d.data + })))) + )); + break; + } default: console.error("unknown msg type"); } }); }); - -chrome.browserAction.onClicked.addListener(function() { - chrome.tabs.create({url: 'index.html'}); -}); - @@ -6,6 +6,7 @@ const _getCalendars = "getCalendars"; const _getCalEvents = "getCalEvents"; const _updateConfig = "updateConfig"; const _getConfig = "getConfig"; +const _getGraphData = "getGraphData"; export const msgType = Object.freeze({ updatePatterns: Symbol(_updatePatterns), @@ -14,7 +15,8 @@ export const msgType = Object.freeze({ getCalendars: Symbol(_getCalendars), getCalEvents: Symbol(_getCalEvents), updateConfig: Symbol(_updateConfig), - getConfig: Symbol(_getConfig) + getConfig: Symbol(_getConfig), + getGraphData: Symbol(_getGraphData), }); function stringifyMsgType(mt) { @@ -26,6 +28,7 @@ function stringifyMsgType(mt) { case msgType.getCalEvents: return _getCalEvents; case msgType.updateConfig: return _updateConfig; case msgType.getConfig: return _getConfig; + case msgType.getGraphData: return _getGraphData; default: console.error("unreachable"); } } @@ -39,6 +42,7 @@ function parseMsgType(s) { case _getCalEvents: return msgType.getCalEvents; case _updateConfig: return msgType.updateConfig; case _getConfig: return msgType.getConfig; + case _getGraphData: return msgType.getGraphData; default: console.error("unreachable"); } } diff --git a/src/popup.js b/src/popup.js index d8e045c..f13688c 100644 --- a/src/popup.js +++ b/src/popup.js @@ -1,135 +1,115 @@ import React from 'react'; import ReactDOM from 'react-dom'; import * as serviceWorker from './serviceWorker'; -import { MuiThemeProvider } from '@material-ui/core/styles'; +import { withStyles, MuiThemeProvider } from '@material-ui/core/styles'; import Button from '@material-ui/core/Button'; +import IconButton from '@material-ui/core/IconButton'; +import RefreshIcon from '@material-ui/icons/Refresh'; +import Logo from './Logo'; import Typography from '@material-ui/core/Typography'; import theme from './theme'; +import CssBaseline from '@material-ui/core/CssBaseline'; import { PatternEntry } from './pattern'; import { Duration } from './duration'; import { msgType, MsgClient } from './msg'; -import { getChartData, StyledPatternPieChart } from './Chart'; +import { StyledPatternPieChart } from './Chart'; +import Divider from '@material-ui/core/Divider'; import moment from 'moment'; function openOptions() { chrome.tabs.create({ url: "index.html" }); } +const styles = theme => ({ + content: { + padding: theme.spacing.unit * 1, + overflow: 'auto', + }, + buttons: { + height: 48, + lineHeight: '48px' + }, + buttonSpacer: { + marginBottom: theme.spacing.unit * 2, + }, +}); + class Popup extends React.Component { state = { patternGraphData: [], + loading: false, }; constructor(props) { super(props); this.msgClient = new MsgClient('main'); - - let pm1 = this.msgClient.sendMsg({ - type: msgType.getPatterns, - data: { id: 'main' } - }).then(msg => { - this.patterns = msg.data.map(p => PatternEntry.inflate(p)); - }); - - let pm2 = this.msgClient.sendMsg({ - type: msgType.getCalendars, - data: { enabledOnly: false } - }).then(msg => { - this.calendars = msg.data; - }); - - let pm3 = this.msgClient.sendMsg({ - type: msgType.getConfig, - data: ['trackedPeriods'] - }).then(msg => { - this.trackedPeriods = msg.data.trackedPeriods.map(p => { - return { - start: Duration.inflate(p.start), - end: Duration.inflate(p.end), - name: p.name - }; - }); - }); - - // initial update - Promise.all([pm1, pm2, pm3]).then(() => { - for (let i = 0; i < this.trackedPeriods.length; i++) - this.renderChartData(i); - }); - } - - getCalEvents = (id, start, end) => { - return this.msgClient.sendMsg({ type: msgType.getCalEvents, data: { id, - start: start.getTime(), - end: end.getTime() } }) - .then(({ data }) => data.map(e => { - return { - id: e.id, - start: new Date(e.start), - end: new Date(e.end) } - })); + this.loading = true; + this.loadGraphData(false).then(() => this.setState({ loading: false })); } - renderChartData(idx) { - let p = this.trackedPeriods[idx]; - console.log(this.trackedPeriods); - let startD = p.start.toMoment(); - let endD = p.end.toMoment(); - if (!(startD && endD)) return; - let start = moment().endOf('day'); - if (endD.valueOf() == 0) { - switch (p.start.unit) { - case 'days': start = moment().endOf('day'); break; - case 'weeks': start = moment().endOf('week'); break; - case 'months': start = moment().endOf('month'); break; - default: - } - } - let end = start.clone(); - start.subtract(startD); - end.subtract(endD); - console.log(start, end); - return getChartData(start.toDate(), - end.toDate(), - this.patterns, this.calendars, this.getCalEvents).then(results => { - let patternGraphData = this.state.patternGraphData; - patternGraphData[idx] = { - start: moment(results.start), - end: moment(results.end), - data: results.patternGraphData - }; - this.setState({ patternGraphData }); + loadGraphData(sync) { + return this.msgClient.sendMsg({ + type: msgType.getGraphData, + data: { sync } + }).then(msg => { + this.setState({ patternGraphData: msg.data.map(d => ({ + name: d.name, + data: d.data, + start: new Date(d.start), + end: new Date(d.end) + }))}); }); } render() { - console.log(this.state.patternGraphData); + let { classes } = this.props; + let data = this.state.patternGraphData; return ( <MuiThemeProvider theme={theme}> - <Button variant="contained" color="primary" onClick={openOptions}>Dashboard</Button> + <CssBaseline /> + <main className={classes.content}> + <div className={classes.buttons}> + <Logo style={{height: '100%', verticalAlign: 'bottom', marginRight: '1em'}}/> + <Button variant="contained" color="primary" onClick={openOptions}>Settings</Button> + <IconButton + disabled={this.state.loading} + style={{float: 'right'}} + onClick={() => ( + new Promise(resolver => ( + this.setState({ loading: true }, resolver))) + .then(() => this.loadGraphData(true)) + .then(() => this.setState({ loading: false })) + )}><RefreshIcon /> + </IconButton> + </div> + <div className={classes.buttonSpacer} /> { - this.state.patternGraphData.map((d, idx) => ( + data.map((d, idx) => ( <div key={idx}> <Typography variant="subtitle1" align="center" color="textPrimary"> - {this.trackedPeriods[idx].name} + {d.name} </Typography> <Typography variant="caption" align="center"> - {`${d.start.format('ddd, MMM Do, YYYY')} - - ${d.end.format('ddd, MMM Do, YYYY')}`} + {`${moment(d.start).format('ddd, MMM Do, YYYY')} - + ${moment(d.end).format('ddd, MMM Do, YYYY')}`} </Typography> {(d.data.some(dd => dd.value > 1e-3) && <StyledPatternPieChart data={d.data} />) || <Typography variant="subtitle1" align="center" color="textSecondary"> No data available </Typography>} + {idx + 1 < data.length && <Divider />} </div> )) } + </main> </MuiThemeProvider> ); } } -ReactDOM.render(<Popup />, document.getElementById('root')); +const StyledPopup = withStyles(styles)(Popup); + +ReactDOM.render(<StyledPopup />, document.getElementById('root')); // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. |