aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorDeterminant <ted.sybil@gmail.com>2019-02-04 02:06:59 -0500
committerDeterminant <ted.sybil@gmail.com>2019-02-04 02:06:59 -0500
commitd3d4345a7ab82e9715b1cbf398d43206d43737f6 (patch)
treeb2006c6dcb774e0e6799ba6c43713be68a695412 /src
parent0ea5a1140c52d15ebb7f37862c1fabce5b80547b (diff)
add settings page
Diffstat (limited to 'src')
-rw-r--r--src/App.js287
-rw-r--r--src/CustomAnalyzer.js255
-rw-r--r--src/Settings.js80
-rw-r--r--src/gapi.js66
-rw-r--r--src/msg.js2
5 files changed, 434 insertions, 256 deletions
diff --git a/src/App.js b/src/App.js
index e98e3d4..6988105 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,39 +1,22 @@
import React from 'react';
import PropTypes from 'prop-types';
import 'typeface-roboto';
-import 'react-dates/initialize';
-import 'react-dates/lib/css/_datepicker.css';
-import { DateRangePicker } from 'react-dates';
import { withStyles } from '@material-ui/core/styles';
import { MuiThemeProvider } from '@material-ui/core/styles';
-import cyan from '@material-ui/core/colors/cyan';
import CssBaseline from '@material-ui/core/CssBaseline';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
-import Button from '@material-ui/core/Button';
-import FormControl from '@material-ui/core/FormControl';
-import FormGroup from '@material-ui/core/FormGroup';
+import Paper from '@material-ui/core/Paper';
+import Tabs from '@material-ui/core/Tabs';
+import Tab from '@material-ui/core/Tab';
import Grid from '@material-ui/core/Grid';
-import AddCircleIcon from '@material-ui/icons/AddCircle';
-import IconButton from '@material-ui/core/IconButton';
+import { HashRouter as Router, Route, Link, Redirect, Switch } from "react-router-dom";
+import { hashHistory } from 'react-router';
import Logo from './Logo';
-import * as gapi from './gapi';
-import { msgType, Msg, MsgClient } from './msg';
-import { Pattern, PatternEntry } from './pattern';
-import PieChart from './Chart';
-import PatternTable from './PatternTable';
import theme from './theme';
-
-const default_chart_data = [
- {name: 'Work', value: 10, color: cyan[300]},
- {name: 'Wasted', value: 10, color: cyan[300]}];
-
-function filterPatterns(patterns, calName) {
- return patterns.filter(p => {
- return p.cal.regex.test(calName);
- });
-}
+import CustomAnalyzer from './CustomAnalyzer';
+import Settings from './Settings';
const styles = theme => ({
root: {
@@ -49,9 +32,7 @@ const styles = theme => ({
},
title: {
flexGrow: 1,
- },
- sectionTitle: {
- flex: '0 0 auto'
+ display: 'inline-block'
},
appBarSpacer: theme.mixins.toolbar,
content: {
@@ -59,241 +40,51 @@ const styles = theme => ({
padding: theme.spacing.unit * 3,
overflow: 'auto',
},
- buttonSpacer: {
- marginBottom: theme.spacing.unit * 4,
- },
- fab: {
- margin: theme.spacing.unit,
- },
+ indicator: {
+ backgroundColor: theme.palette.primary.contrastText
+ }
});
class Dashboard extends React.Component {
state = {
- patterns: [],
- calendars: [],
- timeRange: null,
- token: gapi.getAuthToken(),
- patternGraphData: default_chart_data,
- calendarGraphData: default_chart_data,
- activePattern: null
+ currentTab: 0
};
- constructor(props) {
- super(props);
- this.msgClient = new MsgClient('main');
- this.msgClient.sendMsg({ type: msgType.getPatterns }).then(msg => {
- this.setState({ patterns: msg.data.map(p => PatternEntry.revive(p)) });
- });
- this.msgClient.sendMsg({ type: msgType.getCalendars }).then(msg => {
- this.setState({ calendars: msg.data });
- });
+ handleChangeTab = (event, currentTab) => {
+ this.setState({ currentTab });
}
- updatePattern = (field, idx, value) => {
- let patterns = this.state.patterns;
- patterns[idx][field] = value;
- this.setState({ patterns });
- this.msgClient.sendMsg({ type: msgType.updatePatterns, data: patterns });
- };
-
- removePattern = idx => {
- let patterns = this.state.patterns;
- patterns.splice(idx, 1);
- for (let i = 0; i < patterns.length; i++)
- patterns[i].idx = i;
- this.setState({ patterns });
- this.msgClient.sendMsg({ type: msgType.updatePatterns, data: patterns });
- };
-
- newPattern = () => {
- let patterns = [PatternEntry.defaultPatternEntry(0), ...this.state.patterns];
- for (let i = 1; i < patterns.length; i++)
- patterns[i].idx = i;
- this.setState({ patterns });
- this.msgClient.sendMsg({ type: msgType.updatePatterns, data: patterns });
- };
-
- loadPatterns = patterns => {
- this.setState({ patterns });
- this.msgClient.sendMsg({ type: msgType.updatePatterns, data: patterns });
- };
-
- loadCalendars = calendars => {
- this.setState({ calendars });
- this.msgClient.sendMsg({ type: msgType.updateCalendars, data: calendars });
- };
-
- 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) }
- }));
- }
-
- analyze = () => {
- if (!(this.state.startDate && this.state.endDate)) {
- alert("Please choose a valid time range.");
- return;
- }
- let start = this.state.startDate.startOf('day').toDate();
- let end = this.state.endDate.startOf('day').toDate();
- let event_pms = [];
- let cals = this.state.calendars;
- for (let id in cals)
- {
- let patterns = filterPatterns(this.state.patterns, cals[id].name);
- if (patterns.length > 0)
- event_pms.push(this.getCalEvents(id, start, end)
- .then(r => { return { id, events: r, patterns }; }));
- }
- Promise.all(event_pms).then(all_events => {
- console.log(all_events);
- let events = {};
- let patterns = {};
- let results = {}; // pattern idx => time
- let cal_results = {}; // cal id => time
- all_events.forEach(e => {
- events[e.id] = e.events;
- patterns[e.id] = e.patterns;
- });
- for (let i = 0; i < this.state.patterns.length; i++)
- results[i] = 0;
- for (let id in cals) {
- if (!events[id]) continue;
- events[id].forEach(event => {
- patterns[id].forEach(p => {
- if (!p.event.regex.test(event.summary)) return;
- if (!cal_results.hasOwnProperty(id)) {
- cal_results[id] = 0;
- }
- let duration = (event.end - event.start) / 60000;
- results[p.idx] += duration;
- cal_results[id] += duration;
- });
- });
- }
- let patternGraphData = [];
- let calendarGraphData = [];
- for (let i = 0; i < this.state.patterns.length; i++) {
- patternGraphData.push({ name: this.state.patterns[i].name, value: results[i] / 60.0 });
- }
- for (let id in cal_results) {
- calendarGraphData.push({
- name: cals[id].name,
- value: (cal_results[id] / 60.0),
- color: cals[id].color.background});
- }
- console.log(patternGraphData, calendarGraphData);
- this.setState({ patternGraphData, calendarGraphData });
- });
- };
-
- load = () => {
- let token = this.state.token;
- let colors = token.then(gapi.getColors).then(color => {
- return color.calendar;
- });
- let cals = token.then(gapi.getCalendars);
- Promise.all([colors, cals]).then(([colors, items]) => {
- var cals = {};
- items.forEach(item => {
- cals[item.id] = {
- name: item.summary,
- color: colors[item.colorId],
- //cal: new gapi.GCalendar(item.id, item.summary)
- }});
- this.loadCalendars(cals);
- this.loadPatterns(items.map((item, idx) => {
- return new PatternEntry(item.summary, idx,
- new Pattern(item.id, false, item.summary, item.summary),
- Pattern.anyPattern());
- }));
- });
- };
-
render() {
const { classes } = this.props;
return (
<MuiThemeProvider theme={theme}>
- <div className={classes.root}>
- <AppBar
- position="absolute"
- className={classes.appBar}>
- <Toolbar className={classes.toolbar}>
- <Typography component="h1" variant="h6" color="inherit" noWrap className={classes.title}>
- <Logo style={{width: '2em', verticalAlign: 'bottom', marginRight: '0.2em'}}/>Chromicle
- </Typography>
- </Toolbar>
- </AppBar>
- <main className={classes.content}>
- <div className={classes.appBarSpacer} />
- <Grid container spacing={16}>
- <CssBaseline />
- <Grid item md={6} xs={12}>
- <FormControl fullWidth={true}>
- <FormGroup>
- <Typography variant="h6" component="h1" gutterBottom>
- Event Patterns
- <IconButton
- style={{marginBottom: '0.12em', marginLeft: '0.5em'}}
- onClick={() => this.newPattern()}><AddCircleIcon /></IconButton>
- </Typography>
- <PatternTable
- patterns={this.state.patterns}
- calendars={this.state.calendars}
- onRemovePattern={this.removePattern}
- onUpdatePattern={this.updatePattern} />
- </FormGroup>
- <FormGroup>
- <Typography variant="h6" component="h1" gutterBottom>
- Time Range
- </Typography>
- <div style={{textAlign: 'center'}}>
- <DateRangePicker
- startDate={this.state.startDate}
- startDateId="start_date_id"
- endDate={this.state.endDate}
- endDateId="end_date_id"
- onDatesChange={({ startDate, endDate }) => {
- this.setState({ startDate, endDate });
- }}
- focusedInput={this.state.focusedInput}
- onFocusChange={focusedInput => this.setState({ focusedInput })}
- isOutsideRange={() => false}/>
- </div>
- </FormGroup>
- <div className={classes.buttonSpacer} />
- <Grid container spacing={16}>
- <Grid item md={6} xs={12}>
- <FormGroup>
- <Button variant="contained" color="primary" onClick={this.load}>Load</Button>
- </FormGroup>
- </Grid>
- <Grid item md={6} xs={12}>
- <FormGroup>
- <Button variant="contained" color="primary" onClick={this.analyze}>Analyze</Button>
- </FormGroup>
- </Grid>
- </Grid>
- </FormControl>
- </Grid>
- <Grid item md={6} xs={12}>
- <Typography variant="h6" component="h1" gutterBottom>
- Graph
+ <Router>
+ <div className={classes.root}>
+ <AppBar
+ position="absolute"
+ className={classes.appBar}>
+ <Toolbar className={classes.toolbar}>
+ <Typography component="h1" variant="h6" color="inherit" noWrap className={classes.title}>
+ <Logo style={{width: '2em', verticalAlign: 'bottom', marginRight: '0.2em'}}/>Chromicle
</Typography>
- <PieChart
- patternGraphData={this.state.patternGraphData}
- calendarGraphData={this.state.calendarGraphData}/>
- </Grid>
- </Grid>
- </main>
- </div>
+ <Tabs styles={{ display: 'inline-block '}}
+ classes={{ indicator: classes.indicator }}
+ value={this.state.currentTab}
+ onChange={this.handleChangeTab}>
+ <Tab label="Settings" component={Link} to="/settings" />
+ <Tab label="Analyze" component={Link} to="/analyze" />
+ </Tabs>
+ </Toolbar>
+ </AppBar>
+ <CssBaseline />
+ <main className={classes.content}>
+ <div className={classes.appBarSpacer} />
+ <Route exact path="/settings" component={Settings} />
+ <Route exact path="/analyze" component={CustomAnalyzer} />
+ </main>
+ </div>
+ </Router>
</MuiThemeProvider>);
}
}
diff --git a/src/CustomAnalyzer.js b/src/CustomAnalyzer.js
new file mode 100644
index 0000000..f76fa5e
--- /dev/null
+++ b/src/CustomAnalyzer.js
@@ -0,0 +1,255 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import 'react-dates/initialize';
+import 'react-dates/lib/css/_datepicker.css';
+import { DateRangePicker } from 'react-dates';
+import { withStyles } from '@material-ui/core/styles';
+import cyan from '@material-ui/core/colors/cyan';
+import CssBaseline from '@material-ui/core/CssBaseline';
+import Typography from '@material-ui/core/Typography';
+import Button from '@material-ui/core/Button';
+import FormControl from '@material-ui/core/FormControl';
+import FormGroup from '@material-ui/core/FormGroup';
+import Grid from '@material-ui/core/Grid';
+import AddCircleIcon from '@material-ui/icons/AddCircle';
+import IconButton from '@material-ui/core/IconButton';
+import * as gapi from './gapi';
+import { msgType, MsgClient } from './msg';
+import { Pattern, PatternEntry } from './pattern';
+import PieChart from './Chart';
+import PatternTable from './PatternTable';
+
+const default_chart_data = [
+ {name: 'Work', value: 10, color: cyan[300]},
+ {name: 'Wasted', value: 10, color: cyan[300]}];
+
+function filterPatterns(patterns, calName) {
+ return patterns.filter(p => {
+ return p.cal.regex.test(calName);
+ });
+}
+
+const styles = theme => ({
+ buttonSpacer: {
+ marginBottom: theme.spacing.unit * 4,
+ },
+});
+
+class CustomAnalyzer extends React.Component {
+ state = {
+ patterns: [],
+ calendars: [],
+ timeRange: null,
+ patternGraphData: default_chart_data,
+ calendarGraphData: default_chart_data,
+ activePattern: null
+ };
+
+ constructor(props) {
+ super(props);
+ this.msgClient = new MsgClient('main');
+ this.msgClient.sendMsg({ type: msgType.getPatterns }).then(msg => {
+ this.setState({ patterns: msg.data.map(p => PatternEntry.revive(p)) });
+ });
+ this.msgClient.sendMsg({ type: msgType.getCalendars }).then(msg => {
+ this.setState({ calendars: msg.data });
+ });
+ }
+
+ updatePattern = (field, idx, value) => {
+ let patterns = this.state.patterns;
+ patterns[idx][field] = value;
+ this.setState({ patterns });
+ this.msgClient.sendMsg({ type: msgType.updatePatterns, data: patterns });
+ };
+
+ removePattern = idx => {
+ let patterns = this.state.patterns;
+ patterns.splice(idx, 1);
+ for (let i = 0; i < patterns.length; i++)
+ patterns[i].idx = i;
+ this.setState({ patterns });
+ this.msgClient.sendMsg({ type: msgType.updatePatterns, data: patterns });
+ };
+
+ newPattern = () => {
+ let patterns = [PatternEntry.defaultPatternEntry(0), ...this.state.patterns];
+ for (let i = 1; i < patterns.length; i++)
+ patterns[i].idx = i;
+ this.setState({ patterns });
+ this.msgClient.sendMsg({ type: msgType.updatePatterns, data: patterns });
+ };
+
+ loadPatterns = patterns => {
+ this.setState({ patterns });
+ this.msgClient.sendMsg({ type: msgType.updatePatterns, data: patterns });
+ };
+
+ loadCalendars = calendars => {
+ this.setState({ calendars });
+ this.msgClient.sendMsg({ type: msgType.updateCalendars, data: calendars });
+ };
+
+ 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) }
+ }));
+ }
+
+ analyze = () => {
+ if (!(this.state.startDate && this.state.endDate)) {
+ alert("Please choose a valid time range.");
+ return;
+ }
+ let start = this.state.startDate.startOf('day').toDate();
+ let end = this.state.endDate.startOf('day').toDate();
+ let event_pms = [];
+ let cals = this.state.calendars;
+ for (let id in cals)
+ {
+ let patterns = filterPatterns(this.state.patterns, cals[id].name);
+ if (patterns.length > 0)
+ event_pms.push(this.getCalEvents(id, start, end)
+ .then(r => { return { id, events: r, patterns }; }));
+ }
+ Promise.all(event_pms).then(all_events => {
+ console.log(all_events);
+ let events = {};
+ let patterns = {};
+ let results = {}; // pattern idx => time
+ let cal_results = {}; // cal id => time
+ all_events.forEach(e => {
+ events[e.id] = e.events;
+ patterns[e.id] = e.patterns;
+ });
+ for (let i = 0; i < this.state.patterns.length; i++)
+ results[i] = 0;
+ for (let id in cals) {
+ if (!events[id]) continue;
+ events[id].forEach(event => {
+ patterns[id].forEach(p => {
+ if (!p.event.regex.test(event.summary)) return;
+ if (!cal_results.hasOwnProperty(id)) {
+ cal_results[id] = 0;
+ }
+ let duration = (event.end - event.start) / 60000;
+ results[p.idx] += duration;
+ cal_results[id] += duration;
+ });
+ });
+ }
+ let patternGraphData = [];
+ let calendarGraphData = [];
+ for (let i = 0; i < this.state.patterns.length; i++) {
+ patternGraphData.push({ name: this.state.patterns[i].name, value: results[i] / 60.0 });
+ }
+ for (let id in cal_results) {
+ calendarGraphData.push({
+ name: cals[id].name,
+ value: (cal_results[id] / 60.0),
+ color: cals[id].color.background});
+ }
+ console.log(patternGraphData, calendarGraphData);
+ this.setState({ patternGraphData, calendarGraphData });
+ });
+ };
+
+ load = () => {
+ 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]) => {
+ var cals = {};
+ items.forEach(item => {
+ cals[item.id] = {
+ name: item.summary,
+ color: colors[item.colorId],
+ //cal: new gapi.GCalendar(item.id, item.summary)
+ }});
+ this.loadCalendars(cals);
+ this.loadPatterns(items.map((item, idx) => {
+ return new PatternEntry(item.summary, idx,
+ new Pattern(item.id, false, item.summary, item.summary),
+ Pattern.anyPattern());
+ }));
+ });
+ };
+
+ render() {
+ const { classes } = this.props;
+
+ return (
+ <Grid container spacing={16}>
+ <Grid item md={6} xs={12}>
+ <FormControl fullWidth={true}>
+ <FormGroup>
+ <Typography variant="h6" component="h1" gutterBottom>
+ Event Patterns
+ <IconButton
+ style={{marginBottom: '0.12em', marginLeft: '0.5em'}}
+ onClick={() => this.newPattern()}><AddCircleIcon /></IconButton>
+ </Typography>
+ <PatternTable
+ patterns={this.state.patterns}
+ calendars={this.state.calendars}
+ onRemovePattern={this.removePattern}
+ onUpdatePattern={this.updatePattern} />
+ </FormGroup>
+ <FormGroup>
+ <Typography variant="h6" component="h1" gutterBottom>
+ Time Range
+ </Typography>
+ <div style={{textAlign: 'center'}}>
+ <DateRangePicker
+ startDate={this.state.startDate}
+ startDateId="start_date_id"
+ endDate={this.state.endDate}
+ endDateId="end_date_id"
+ onDatesChange={({ startDate, endDate }) => {
+ this.setState({ startDate, endDate });
+ }}
+ focusedInput={this.state.focusedInput}
+ onFocusChange={focusedInput => this.setState({ focusedInput })}
+ isOutsideRange={() => false} />
+ </div>
+ </FormGroup>
+ <div className={classes.buttonSpacer} />
+ <Grid container spacing={16}>
+ <Grid item md={6} xs={12}>
+ <FormGroup>
+ <Button variant="contained" color="primary" onClick={this.load}>Load</Button>
+ </FormGroup>
+ </Grid>
+ <Grid item md={6} xs={12}>
+ <FormGroup>
+ <Button variant="contained" color="primary" onClick={this.analyze}>Analyze</Button>
+ </FormGroup>
+ </Grid>
+ </Grid>
+ </FormControl>
+ </Grid>
+ <Grid item md={6} xs={12}>
+ <Typography variant="h6" component="h1" gutterBottom>
+ Graph
+ </Typography>
+ <PieChart
+ patternGraphData={this.state.patternGraphData}
+ calendarGraphData={this.state.calendarGraphData}/>
+ </Grid>
+ </Grid>
+ );
+ }
+}
+
+CustomAnalyzer.propTypes = {
+ classes: PropTypes.object.isRequired,
+};
+
+export default withStyles(styles)(CustomAnalyzer);
diff --git a/src/Settings.js b/src/Settings.js
new file mode 100644
index 0000000..11167a0
--- /dev/null
+++ b/src/Settings.js
@@ -0,0 +1,80 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withStyles } from '@material-ui/core/styles';
+import CssBaseline from '@material-ui/core/CssBaseline';
+import Typography from '@material-ui/core/Typography';
+import Button from '@material-ui/core/Button';
+import FormControl from '@material-ui/core/FormControl';
+import FormGroup from '@material-ui/core/FormGroup';
+import Grid from '@material-ui/core/Grid';
+import AddCircleIcon from '@material-ui/icons/AddCircle';
+import IconButton from '@material-ui/core/IconButton';
+import Table from '@material-ui/core/Table';
+import TableBody from '@material-ui/core/TableBody';
+import TableRow from '@material-ui/core/TableRow';
+import TableCell from '@material-ui/core/TableCell';
+import TableHead from '@material-ui/core/TableHead';
+import * as gapi from './gapi';
+import { msgType, MsgClient } from './msg';
+import { Pattern, PatternEntry } from './pattern';
+
+const styles = theme => ({
+});
+
+const STableCell = withStyles(theme => ({
+ body: {
+ fontSize: 16,
+ },
+}))(TableCell);
+
+class Settings extends React.Component {
+ state = {
+ isLoggedIn: false
+ };
+
+ constructor(props) {
+ super(props);
+ gapi.getLoggedIn().then(b => this.setState({ isLoggedIn: b }));
+ }
+
+ handleLogin = () => {
+ gapi.login().then(() => this.setState({ isLoggedIn: true }));
+ }
+
+ handleLogout = () => {
+ gapi.logout().then(() => this.setState({ isLoggedIn: false }));
+ }
+
+ render() {
+ const { classes } = this.props;
+ return (
+ <Grid container spacing={16}>
+ <Grid item md={6} xs={12}>
+ <Typography variant="h6" component="h1" gutterBottom>
+ General
+ </Typography>
+ <Table>
+ <TableBody>
+ <TableRow>
+ <STableCell align='right'>Account</STableCell>
+ <STableCell align='left'>
+ {
+ (this.state.isLoggedIn &&
+ <Button variant="contained" color="primary" onClick={this.handleLogout}>Logout</Button>) ||
+ <Button variant="contained" color="primary" onClick={this.handleLogin}>Login</Button>
+ }
+ </STableCell>
+ </TableRow>
+ </TableBody>
+ </Table>
+ </Grid>
+ </Grid>
+ );
+ }
+}
+
+Settings.propTypes = {
+ classes: PropTypes.object.isRequired,
+};
+
+export default withStyles(styles)(Settings);
diff --git a/src/gapi.js b/src/gapi.js
index 25d1cbf..60e9b1a 100644
--- a/src/gapi.js
+++ b/src/gapi.js
@@ -2,19 +2,68 @@
import LRU from "lru-cache";
const gapi_base = 'https://www.googleapis.com/calendar/v3';
-const GApiError = {
- invalidSyncToken: 1,
- otherError: 2,
-};
+const GApiError = Object.freeze({
+ invalidSyncToken: Symbol("invalidSyncToken"),
+ notLoggedIn: Symbol("notLoggedIn"),
+ notLoggedOut: Symbol("notLoggedOut"),
+ otherError: Symbol("otherError"),
+});
function to_params(dict) {
return Object.entries(dict).filter(([k, v]) => v).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&');
}
-export function getAuthToken() {
+let loggedIn = null;
+
+function _getAuthToken(interactive = false) {
return new Promise(resolver =>
chrome.identity.getAuthToken(
- {interactive: true}, token => resolver(token)));
+ { interactive }, token => resolver([token, !chrome.runtime.lastError])))
+ .then(([token, ok]) => {
+ if (ok) return token;
+ else throw GApiError.notLoggedIn;
+ });
+}
+
+function _removeCachedAuthToken(token) {
+ return new Promise(resolver =>
+ chrome.identity.removeCachedAuthToken({ token }, () => resolver()));
+}
+
+export function getLoggedIn() {
+ if (loggedIn === null)
+ {
+ return _getAuthToken(false)
+ .then(() => {loggedIn = true})
+ .catch(() => {loggedIn = false; console.log("here");})
+ .then(() => loggedIn);
+ }
+ else return Promise.resolve(loggedIn);
+}
+
+export function getAuthToken() {
+ return getLoggedIn().then(b => {
+ if (b) return _getAuthToken(false);
+ else throw GApiError.notLoggedIn;
+ });
+}
+
+export function login() {
+ return getLoggedIn().then(b => {
+ if (!b) return _getAuthToken(true).then(() => loggedIn = true);
+ else throw GApiError.notLoggedOut;
+ });
+}
+
+export function logout() {
+ return getAuthToken().then(token => {
+ return fetch(`https://accounts.google.com/o/oauth2/revoke?${to_params({ token })}`,
+ { method: 'GET', async: true }).then(response => {
+ if (response.status === 200)
+ return _removeCachedAuthToken(token);
+ else throw GApiError.otherError;
+ });
+ }).then(() => loggedIn = false);
}
export function getCalendars(token) {
@@ -51,7 +100,7 @@ function getEvents(calId, token, syncToken=null, timeMin=null, timeMax=null, res
return response.json();
else if (response.status === 410)
throw GApiError.invalidSyncToken;
- else throw GApiError.otherErrors;
+ else throw GApiError.otherError;
})
.then(data => {
results.push(...data.items);
@@ -72,7 +121,6 @@ export class GCalendar {
constructor(calId, name, options={maxCachedItems: 100, nDaysPerSlot: 10, largeQuery: 10}) {
this.calId = calId;
this.name = name;
- this.token = getAuthToken();
this.syncToken = '';
this.cache = new LRU({
max: options.maxCachedItems,
@@ -83,6 +131,8 @@ export class GCalendar {
this.divider = 8.64e7 * this.options.nDaysPerSlot;
}
+ get token() { return getAuthToken(); }
+
dateToCacheKey(date) {
return Math.floor(date / this.divider);
}
diff --git a/src/msg.js b/src/msg.js
index b6221ec..25cd53c 100644
--- a/src/msg.js
+++ b/src/msg.js
@@ -20,6 +20,7 @@ function stringifyMsgType(mt) {
case msgType.updateCalendars: return _updateCalendars;
case msgType.getCalendars: return _getCalendars;
case msgType.getCalEvents: return _getCalEvents;
+ default: console.error("unreachable");
}
}
@@ -30,6 +31,7 @@ function parseMsgType(s) {
case _updateCalendars: return msgType.updateCalendars;
case _getCalendars: return msgType.getCalendars;
case _getCalEvents: return msgType.getCalEvents;
+ default: console.error("unreachable");
}
}