aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Chart.tsx113
-rw-r--r--src/PatternTable.tsx6
-rw-r--r--src/Settings.tsx56
-rw-r--r--src/background.ts39
-rw-r--r--src/gapi.ts5
-rw-r--r--src/msg.ts4
-rw-r--r--src/tab.html17
-rw-r--r--src/tab.tsx135
8 files changed, 310 insertions, 65 deletions
diff --git a/src/Chart.tsx b/src/Chart.tsx
index af439f9..e1b9c6d 100644
--- a/src/Chart.tsx
+++ b/src/Chart.tsx
@@ -18,50 +18,79 @@ type PatternPieChartProps = {
pieChart: string
},
height?: number,
- data: PatternGraphData[]
+ data: PatternGraphData[],
+ radialLabelsLinkStrokeWidth?: number,
+ radialLabelsLinkDiagonalLength?: number,
+ borderWidth: number,
+ labelFontSize : number,
+ marginTop: number,
+ marginBottom: number,
+ marginLeft: number,
+ marginRight: number,
+ padAngle: number
};
-function PatternPieChart(props: PatternPieChartProps) {
+export class PatternPieChart extends React.Component<PatternPieChartProps> {
+ public static defaultProps = {
+ radialLabelsLinkStrokeWidth: 1,
+ borderWidth: 1,
+ radialLabelsLinkDiagonalLength: 16,
+ labelFontSize: 12,
+ marginTop: 40,
+ marginBottom: 40,
+ marginLeft: 80,
+ marginRight: 80,
+ padAngle: 0.7
+ };
+ render() {
+ let { height, data, labelFontSize } = this.props;
+ const theme = {
+ labels: {
+ text: {
+ fontSize: labelFontSize
+ }
+ }
+ };
return (
- <Grid item xs={12} lg={6}>
- <div style={{height: (props.height ? props.height : 300)}}>
- <ResponsivePie
- data={props.data.map(p => ({
- id: p.name,
- label: p.name,
- value: p.value,
- color: p.color ? p.color: defaultChartColor
- }))}
- margin={{
- top: 40,
- right: 80,
- bottom: 40,
- left: 80
- }}
- innerRadius={0.5}
- padAngle={0.7}
- cornerRadius={3}
- colorBy={d => d.color as string}
- borderWidth={1}
- borderColor="inherit:darker(0.2)"
- radialLabelsSkipAngle={10}
- radialLabelsTextXOffset={6}
- radialLabelsTextColor="#333333"
- radialLabelsLinkOffset={0}
- radialLabelsLinkDiagonalLength={16}
- radialLabelsLinkHorizontalLength={24}
- radialLabelsLinkStrokeWidth={1}
- radialLabelsLinkColor="inherit"
- sliceLabel={(d) => `${d.value.toFixed(2)} hr`}
- slicesLabelsSkipAngle={10}
- slicesLabelsTextColor="#ffffff"
- animate={true}
- motionStiffness={90}
- motionDamping={15}
- tooltipFormat={v => `${v.toFixed(2)} hr`} />
- </div>
- </Grid>
+ <div style={{height: (height ? height : 300)}}>
+ <ResponsivePie
+ data={data.map(p => ({
+ id: p.name,
+ label: p.name,
+ value: p.value,
+ color: p.color ? p.color: defaultChartColor
+ }))}
+ margin={{
+ top: this.props.marginTop,
+ right: this.props.marginRight,
+ bottom: this.props.marginBottom,
+ left: this.props.marginLeft
+ }}
+ innerRadius={0.5}
+ padAngle={this.props.padAngle}
+ cornerRadius={3}
+ colorBy={d => d.color as string}
+ borderWidth={this.props.borderWidth}
+ borderColor="inherit:darker(0.2)"
+ radialLabelsSkipAngle={10}
+ radialLabelsTextXOffset={6}
+ radialLabelsTextColor="#333333"
+ radialLabelsLinkOffset={0}
+ radialLabelsLinkDiagonalLength={this.props.radialLabelsLinkDiagonalLength}
+ radialLabelsLinkHorizontalLength={24}
+ radialLabelsLinkStrokeWidth={this.props.radialLabelsLinkStrokeWidth}
+ radialLabelsLinkColor="inherit"
+ sliceLabel={(d) => `${d.value.toFixed(2)} hr`}
+ slicesLabelsSkipAngle={10}
+ slicesLabelsTextColor="#ffffff"
+ animate={true}
+ motionStiffness={90}
+ motionDamping={15}
+ theme={theme}
+ tooltipFormat={v => `${v.toFixed(2)} hr`} />
+ </div>
);
+ }
}
export const StyledPatternPieChart = withStyles(styles)(PatternPieChart);
@@ -78,8 +107,12 @@ type DoublePieChartProps = {
function DoublePieChart(props: DoublePieChartProps) {
return (
<Grid container spacing={0}>
+ <Grid item xs={12} lg={6}>
<StyledPatternPieChart data={props.patternGraphData} height={300} />
+ </Grid>
+ <Grid item xs={12} lg={6}>
<StyledPatternPieChart data={props.calendarGraphData} height={300} />
+ </Grid>
</Grid>);
}
diff --git a/src/PatternTable.tsx b/src/PatternTable.tsx
index 3aef72f..b9d0e9e 100644
--- a/src/PatternTable.tsx
+++ b/src/PatternTable.tsx
@@ -60,12 +60,12 @@ type NameFieldProps = {
};
function NameField(props: NameFieldProps) {
- let color = props.value.color;
+ let color = props.value.color.background;
return (
<span>
<div
className={props.classes.colorSample}
- style={{backgroundColor: color ? color.background : defaultChartColor}}
+ style={{backgroundColor: (color ? color : defaultChartColor)}}
onClick={props.colorOnClick}>
</div>
<TextField
@@ -117,7 +117,7 @@ class PatternTable extends React.Component<PatternTableProps> {
handleColorPickerClose = () => {
this.setState({ colorPickerOpen: false });
- this.activeColorPattern !== null && this.chosenColor !== null &&
+ this.activeColorPattern !== null && this.chosenColor &&
this.props.onUpdatePattern('color', this.activeColorPattern,
{ background: this.chosenColor });
}
diff --git a/src/Settings.tsx b/src/Settings.tsx
index c6e5a00..13488c4 100644
--- a/src/Settings.tsx
+++ b/src/Settings.tsx
@@ -161,7 +161,8 @@ class Settings extends React.Component<SettingsProps> {
isLoggedIn: false,
patterns: [] as PatternEntry[],
calendars: {} as {[id: string]: gapi.GCalendarMeta},
- config: {} as { trackedPeriods: TrackedPeriodFlat[] },
+ trackedPeriods: [] as TrackedPeriodFlat[],
+ overrideNewTab: false,
snackBarOpen: false,
snackBarMsg: 'unknown',
snackBarVariant: 'error' as SnackbarVariant,
@@ -194,13 +195,14 @@ class Settings extends React.Component<SettingsProps> {
this.msgClient.sendMsg({
opt: MsgType.getConfig,
- data: ['trackedPeriods']
+ data: ['trackedPeriods', 'overrideNewTab']
}).then(msg => {
let config = {
- trackedPeriods: msg.data.trackedPeriods
+ trackedPeriods: msg.data.trackedPeriods,
+ overrideNewTab: msg.data.overrideNewTab
};
console.log(msg.data.trackedPeriods);
- this.setState({ config });
+ this.setState(config);
});
this.dialogPromiseResolver = null;
@@ -221,6 +223,7 @@ class Settings extends React.Component<SettingsProps> {
if (!ans) return;
try {
await gapi.logout();
+ await this.msgClient.sendMsg({ opt: MsgType.clearCache, data: {} });
this.setState({ isLoggedIn: false });
} catch (_) {
this.openSnackbar("Failed to logout!", 'error' as SnackbarVariant);
@@ -253,6 +256,7 @@ class Settings extends React.Component<SettingsProps> {
pms.push(this.loadDefaultPatterns(cals));
await Promise.all(pms);
this.setState({ calendarsLoading: false });
+ if (reloadAll) this.handleApply();
};
loadDefaultPatterns(calendars: {[ id: string ]: gapi.GCalendarMeta }) {
@@ -326,29 +330,29 @@ class Settings extends React.Component<SettingsProps> {
}
updateTrackedPeriods = (trackedPeriods: TrackedPeriodFlat[]) => {
- this.setState({...this.state.config, trackedPeriods });
+ this.setState({ trackedPeriods });
}
handlePeriodNameChange = (idx: number) => (name: string) => {
- let trackedPeriods = [...this.state.config.trackedPeriods];
+ let trackedPeriods = [...this.state.trackedPeriods];
trackedPeriods[idx].name = name;
this.updateTrackedPeriods(trackedPeriods);
}
handlePeriodFromChange = (idx: number) => (duration: DurationFlat) => {
- let trackedPeriods = [...this.state.config.trackedPeriods];
+ let trackedPeriods = [...this.state.trackedPeriods];
trackedPeriods[idx].start = duration;
this.updateTrackedPeriods(trackedPeriods);
}
handlePeriodToChange = (idx: number) => (duration: DurationFlat) => {
- let trackedPeriods = [...this.state.config.trackedPeriods];
+ let trackedPeriods = [...this.state.trackedPeriods];
trackedPeriods[idx].end = duration;
this.updateTrackedPeriods(trackedPeriods);
}
handleApply = async () => {
- let trackedPeriods = this.state.config.trackedPeriods;
+ let trackedPeriods = this.state.trackedPeriods;
if (trackedPeriods.some(p => (
TrackedPeriodInput.toValue(p.start.value) === null ||
TrackedPeriodInput.toValue(p.end.value) === null ))) {
@@ -368,6 +372,11 @@ class Settings extends React.Component<SettingsProps> {
opt: MsgType.updateConfig,
data: { trackedPeriods }
});
+ let pm4 = this.msgClient.sendMsg({
+ opt: MsgType.updateConfig,
+ data: {'overrideNewTab': this.state.overrideNewTab }
+ });
+
await Promise.all([pm1, pm2, pm3]);
this.openSnackbar("Saved changes.", 'success' as SnackbarVariant);
}
@@ -378,6 +387,10 @@ class Settings extends React.Component<SettingsProps> {
this.loadDefaultPatterns(this.state.calendars);
}
+ toggleOverrideNewTab() {
+ this.setState({ overrideNewTab: !this.state.overrideNewTab });
+ }
+
render() {
const { classes } = this.props;
return (
@@ -392,9 +405,6 @@ class Settings extends React.Component<SettingsProps> {
open={this.state.snackBarOpen}
variant={this.state.snackBarVariant}
onClose={this.handleSnackbarClose}/>
- <Typography variant="h6" component="h1" gutterBottom>
- General
- </Typography>
<Table>
<TableBody>
<TableRow>
@@ -464,8 +474,8 @@ class Settings extends React.Component<SettingsProps> {
Tracked Time Range
</STableCell>
<STableCell className={classNames(classes.tableContent, classes.trackedPeriodInput)}>
- {this.state.config.trackedPeriods &&
- this.state.config.trackedPeriods.map((p, idx) =>
+ {this.state.trackedPeriods &&
+ this.state.trackedPeriods.map((p, idx) =>
<FormGroup key={idx}>
<TrackedPeriodInput
name={p.name}
@@ -477,6 +487,24 @@ class Settings extends React.Component<SettingsProps> {
</FormGroup>)}
</STableCell>
</TableRow>
+ <TableRow>
+ <STableCell className={classes.tableHead}>
+ Misc
+ </STableCell>
+ <STableCell className={classes.tableContent}>
+ <List>
+ <CompactListItem
+ key="overrideNewTab"
+ onClick={() => this.toggleOverrideNewTab()}
+ disableGutters dense button>
+ <Checkbox
+ checked={this.state.overrideNewTab}
+ disableRipple />
+ <ListItemText primary="Show graphs when open a new tab" />
+ </CompactListItem>
+ </List>
+ </STableCell>
+ </TableRow>
</TableBody>
</Table>
<div className={classes.bottomButtons}>
diff --git a/src/background.ts b/src/background.ts
index 39c10c8..9b95061 100644
--- a/src/background.ts
+++ b/src/background.ts
@@ -14,7 +14,8 @@ let config = {
new TrackedPeriod('Today', Duration.days(1), Duration.days(0)),
new TrackedPeriod('Yesterday', Duration.days(2), Duration.days(1)),
new TrackedPeriod('This Week', Duration.weeks(1), Duration.weeks(0)),
- new TrackedPeriod('This Month', Duration.months(1), Duration.months(0))] as TrackedPeriod[]
+ new TrackedPeriod('This Month', Duration.months(1), Duration.months(0))] as TrackedPeriod[],
+ overrideNewTab: false
};
let mainGraphData: GraphData[] = [];
let dirtyMetadata = false;
@@ -48,7 +49,8 @@ async function loadMetadata() {
else
{
config = {
- trackedPeriods: items.config.trackedPeriods.map((p: TrackedPeriodFlat) => TrackedPeriod.inflate(p))
+ trackedPeriods: items.config.trackedPeriods.map((p: TrackedPeriodFlat) => TrackedPeriod.inflate(p)),
+ overrideNewTab: items.config.overrideNewTab
};
calendars = items.calendars;
mainPatterns = items.mainPatterns.map((p: PatternEntryFlat) => PatternEntry.inflate(p));
@@ -64,7 +66,8 @@ async function saveMetadata() {
await chromeStorageSet({
calendars,
config: {
- trackedPeriods: config.trackedPeriods.map(p => p.deflate())
+ trackedPeriods: config.trackedPeriods.map(p => p.deflate()),
+ overrideNewTab: config.overrideNewTab
},
mainPatterns: mainPatterns.map(p => p.deflate()),
analyzePatterns: analyzePatterns.map(p => p.deflate())
@@ -169,7 +172,10 @@ async function pollSync() {
let pms = [];
for (let id in calendars) {
if (!calendars[id].enabled) continue;
- pms.push(getCalData(id).sync());
+ pms.push(getCalData(id).sync().catch(err => {
+ console.log(`cannot sync calendar ${id}`, err);
+ calendars[id].enabled = false;
+ }));
}
await Promise.all(pms);
/* update the tracked graph data */
@@ -239,7 +245,13 @@ function handleMsg(port: chrome.runtime.Port) {
break;
}
case MsgType.updateConfig: {
- config.trackedPeriods = msg.data.trackedPeriods.map((p: TrackedPeriodFlat) => TrackedPeriod.inflate(p));
+ for (let prop in msg.data) {
+ if (prop === 'trackedPeriods') {
+ config.trackedPeriods = msg.data.trackedPeriods.map((p: TrackedPeriodFlat) => TrackedPeriod.inflate(p));
+ } else if (prop == 'overrideNewTab') {
+ config.overrideNewTab = msg.data.overrideNewTab as boolean;
+ }
+ }
dirtyMetadata = true;
port.postMessage(msg.genResp(null));
break;
@@ -249,6 +261,8 @@ function handleMsg(port: chrome.runtime.Port) {
msg.data.forEach((prop: string) => {
if (prop === 'trackedPeriods')
res.trackedPeriods = config.trackedPeriods.map(p => p.deflate());
+ else if (prop === 'overrideNewTab')
+ res.overrideNewTab = config.overrideNewTab;
});
port.postMessage(msg.genResp(res));
break;
@@ -270,6 +284,11 @@ function handleMsg(port: chrome.runtime.Port) {
})();
break;
}
+ case MsgType.clearCache: {
+ calData = {};
+ port.postMessage(msg.genResp(null));
+ break;
+ }
default: console.error("unknown msg opt");
}
});
@@ -281,3 +300,13 @@ loadPromise = (async () => {
})();
chrome.runtime.onConnect.addListener(handleMsg);
+
+chrome.tabs.onCreated.addListener(function(tab) {
+ if (tab.url === "chrome://newtab/") {
+ if (config.overrideNewTab) {
+ chrome.tabs.update(tab.id, {
+ url: chrome.extension.getURL("tab.html")
+ });
+ }
+ }
+});
diff --git a/src/gapi.ts b/src/gapi.ts
index 2252230..8e74483 100644
--- a/src/gapi.ts
+++ b/src/gapi.ts
@@ -332,8 +332,7 @@ export class GCalendar {
addEvent(e: {start: Date, end: Date, id: string, summary: string}, evict = false) {
//console.log('adding event', e);
- if (this.eventMeta.hasOwnProperty(e.id))
- this.removeEvent(e);
+ this.removeEvent(e);
let r = this.dateRangeToCacheKeys(e);
let ks = r.start;
let ke = r.end;
@@ -361,6 +360,8 @@ export class GCalendar {
}
removeEvent(e: {id: string}) {
+ if (!this.eventMeta.hasOwnProperty(e.id))
+ return;
let keys = this.eventMeta[e.id].keys;
keys.forEach(k => delete this.getSlot(k)[e.id]);
delete this.eventMeta[e.id];
diff --git a/src/msg.ts b/src/msg.ts
index efbadb5..daef6f9 100644
--- a/src/msg.ts
+++ b/src/msg.ts
@@ -8,7 +8,8 @@ export enum MsgType {
getCalEvents = "getCalEvents",
updateConfig = "updateConfig",
getConfig = "getConfig",
- getGraphData = "getGraphData"
+ getGraphData = "getGraphData",
+ clearCache = "clearCache"
}
function stringifyMsgType(opt: MsgType): string { return MsgType[opt]; }
@@ -23,6 +24,7 @@ function parseMsgType(s: string): MsgType {
case "updateConfig": return MsgType.updateConfig;
case "getConfig": return MsgType.getConfig;
case "getGraphData": return MsgType.getGraphData;
+ case "clearCache": return MsgType.clearCache;
default: console.error(`unknown MsgType: ${s}`);
}
}
diff --git a/src/tab.html b/src/tab.html
new file mode 100644
index 0000000..deaa3ec
--- /dev/null
+++ b/src/tab.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8" />
+ <meta
+ name="viewport"
+ content="width=device-width, initial-scale=1, shrink-to-fit=no"
+ />
+ <meta name="theme-color" content="#000000" />
+ <link rel="stylesheet" href="/fonts/TypoPRO-FantasqueSansMono-Regular.css" />
+ <title>New Tab</title>
+ </head>
+ <body>
+ <noscript>You need to enable JavaScript to run this app.</noscript>
+ <div id="root"></div>
+ </body>
+</html>
diff --git a/src/tab.tsx b/src/tab.tsx
new file mode 100644
index 0000000..e720844
--- /dev/null
+++ b/src/tab.tsx
@@ -0,0 +1,135 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { Theme, withStyles, StyleRules, MuiThemeProvider } 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 IconButton from '@material-ui/core/IconButton';
+import RefreshIcon from '@material-ui/icons/Refresh';
+import Grid from '@material-ui/core/Grid';
+import CircularProgress from '@material-ui/core/CircularProgress';
+
+import Logo from './Logo';
+import { theme } from './theme';
+import { StyledPatternPieChart } from './Chart';
+import { MsgType, MsgClient } from './msg';
+import { GraphData } from './graph';
+import moment from 'moment';
+
+const styles = (theme: Theme): StyleRules => ({
+ content: {
+ padding: theme.spacing.unit * 1,
+ overflow: 'auto',
+ },
+ buttons: {
+ width: '100%',
+ height: 0,
+ lineHeight: '48px'
+ },
+ buttonSpacer: {
+ marginBottom: theme.spacing.unit * 2,
+ },
+ loading: {
+ textAlign: 'center'
+ }
+});
+
+type TabProps = {
+ classes: {
+ content: string,
+ buttons: string,
+ buttonSpacer: string,
+ loading: string
+ }
+};
+
+
+class Tab extends React.Component<TabProps> {
+ msgClient: MsgClient;
+ state = {
+ patternGraphData: [] as GraphData[],
+ loading: false,
+ };
+ constructor(props: TabProps) {
+ super(props);
+ this.msgClient = new MsgClient('main');
+ this.state.loading = true;
+ this.loadGraphData(false).then(() => this.setState({ loading: false }));
+ }
+
+ loadGraphData(sync: boolean) {
+ return this.msgClient.sendMsg({
+ opt: MsgType.getGraphData,
+ data: { sync }
+ }).then(msg => {
+ this.setState({ patternGraphData: msg.data.map((d: GraphData) => ({
+ name: d.name,
+ data: d.data,
+ start: new Date(d.start),
+ end: new Date(d.end)
+ }))});
+ });
+ }
+
+ render() {
+ let { classes } = this.props;
+ let data = this.state.patternGraphData;
+ return (
+ <MuiThemeProvider theme={theme}>
+ <CssBaseline />
+ <main className={classes.content}>
+ <div className={classes.buttons}>
+ <Logo style={{height: 48, verticalAlign: 'bottom'}}/>
+ <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} />
+ <Grid container spacing={0} style={{ maxWidth: 1024, minWidth: 640, margin: '0 auto' }}>
+ {
+ (data.length > 0 &&
+ data.map((d, idx) => (
+ <Grid item key={idx} xs={12} lg={6}>
+ <Typography variant="subtitle1" align="center" color="textPrimary">
+ {d.name}
+ </Typography>
+ <Typography variant="caption" align="center">
+ {`${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}
+ height={400}
+ borderWidth={2}
+ marginTop={60} marginBottom={60}
+ marginLeft={100} marginRight={100}
+ radialLabelsLinkDiagonalLength={40}
+ labelFontSize={14}
+ padAngle={0.8} />
+ ) ||
+ <Typography variant="subtitle1" align="center" color="textSecondary">
+ No matching events.
+ </Typography>}
+ </Grid>
+ ))) || (
+ <div className={classes.loading}><CircularProgress color="primary" /></div>
+ )
+ }
+ </Grid>
+ </main>
+ </MuiThemeProvider>
+ );
+ }
+}
+
+const StyledTab = withStyles(styles)(Tab);
+
+ReactDOM.render(<StyledTab />, document.getElementById('root'));