diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/Analyze.tsx | 11 | ||||
-rw-r--r-- | src/Settings.tsx | 72 | ||||
-rw-r--r-- | src/background.ts | 75 | ||||
-rw-r--r-- | src/gapi.ts | 120 | ||||
-rw-r--r-- | src/msg.ts | 12 | ||||
-rw-r--r-- | src/tab.tsx | 16 |
6 files changed, 215 insertions, 91 deletions
diff --git a/src/Analyze.tsx b/src/Analyze.tsx index b272f30..e7563d5 100644 --- a/src/Analyze.tsx +++ b/src/Analyze.tsx @@ -77,9 +77,14 @@ class Analyze extends React.Component<AnalyzeProps> { this.setState({ calendars: msg.data }); }); - gapi.getLoggedIn().then(b => !b && - this.openSnackbar('Not logged in. Operating in offline mode.', - 'warning' as SnackbarVariant)); + this.msgClient.sendMsg({ + opt: MsgType.getLoggedIn, + data: {} + }).then(msg => { + if (!msg.data) + this.openSnackbar('Not logged in. Operating in offline mode.', + 'warning' as SnackbarVariant); + }); this.dialogPromiseResolver = null; } diff --git a/src/Settings.tsx b/src/Settings.tsx index d44f1f6..3537894 100644 --- a/src/Settings.tsx +++ b/src/Settings.tsx @@ -187,11 +187,16 @@ class Settings extends React.Component<SettingsProps> { constructor(props: SettingsProps) { super(props); - gapi.getLoggedIn().then(b => this.setState({ isLoggedIn: b })); - this.msgClient = new MsgClient('main'); this.msgClient.sendMsg({ + opt: MsgType.getLoggedIn, + data: {} + }).then(msg => { + this.setState({ isLoggedIn: msg.data }) + }); + + this.msgClient.sendMsg({ opt: MsgType.getPatterns, data: { id: 'main' } }).then(msg => { @@ -224,7 +229,8 @@ class Settings extends React.Component<SettingsProps> { handleLogin = async () => { try { - await gapi.login(); + let resp = await this.msgClient.sendMsg({ opt: MsgType.login, data: {} }); + if (!resp.data) throw new Error("backend failes to login"); this.setState({ isLoggedIn: true }); this.loadAll(true); } catch (_) { @@ -236,10 +242,12 @@ class Settings extends React.Component<SettingsProps> { let ans = await this.openDialog("Logout", "Are you sure to logout?"); if (!ans) return; try { - await gapi.logout(); + let resp = await this.msgClient.sendMsg({ opt: MsgType.logout, data: {} }); + if (!resp.data) throw new Error("backend fails to logout"); await this.msgClient.sendMsg({ opt: MsgType.clearCache, data: {} }); this.setState({ isLoggedIn: false }); - } catch (_) { + } catch (err) { + console.log(err); this.openSnackbar("Failed to logout!", 'error' as SnackbarVariant); } } @@ -250,30 +258,38 @@ class Settings extends React.Component<SettingsProps> { this.setState({ calendars }); } - async loadAll(reloadAll = false) { + async loadAll(reloadAll = false): Promise<void> { await new Promise(resolver => (this.setState({ calendarsLoading: true }, resolver))); - let pm_colors = gapi.getAuthToken().then(gapi.getColors).then(color => color.calendar); - let pm_cals = gapi.getAuthToken().then(gapi.getCalendars); - let [colors, _cals] = await Promise.all([pm_colors, pm_cals]); - var cals: { [id: string]: gapi.GCalendarMeta } = {}; - _cals.forEach((cal: any) => { - let _color = colors[cal.colorId]; - cals[cal.id] = { - name: cal.summary, - color: { - background: ('#' + getColorFamily(_color.background)[300]).toLowerCase() - }, - enabled: true - }; - }); + try { + let pm_colors = this.msgClient.sendMsg( + { opt: MsgType.fetchColors, data: {} }).then(msg => msg.data.calendar); + let pm_cals = this.msgClient.sendMsg( + { opt: MsgType.fetchCalendars, data: {} }).then(msg => msg.data); + let [colors, _cals] = await Promise.all([pm_colors, pm_cals]); + var cals: { [id: string]: gapi.GCalendarMeta } = {}; + _cals.forEach((cal: any) => { + let _color = colors[cal.colorId]; + cals[cal.id] = { + name: cal.summary, + color: { + background: ('#' + getColorFamily(_color.background)[300]).toLowerCase() + }, + enabled: true + }; + }); - let pms = [this.loadCalendars(cals, reloadAll)]; - if (reloadAll) - pms.push(this.loadDefaultPatterns(cals)); - await Promise.all(pms); - this.setState({ calendarsLoading: false }); - if (reloadAll) this.handleApply(); + let pms = [this.loadCalendars(cals, reloadAll)]; + if (reloadAll) + pms.push(this.loadDefaultPatterns(cals)); + await Promise.all(pms); + if (reloadAll) this.handleApply(); + } catch (err) { + console.log(err); + this.openSnackbar("Failed to update calendars!", 'error' as SnackbarVariant); + } finally { + this.setState({ calendarsLoading: false }); + } }; loadDefaultPatterns(calendars: {[ id: string ]: gapi.GCalendarMeta }) { @@ -445,8 +461,8 @@ class Settings extends React.Component<SettingsProps> { Calendars </STableCell> <STableCell className={classes.tableContent} style={{paddingRight: 0}}> - <div className={classNames(classes.calendarList, classes.list)}> {(this.state.isLoggedIn && + <div className={classNames(classes.calendarList, classes.list)}> <List disablePadding> {Object.keys(this.state.calendars).sort().map(id => <CompactListItem @@ -459,7 +475,7 @@ class Settings extends React.Component<SettingsProps> { disableRipple /> <ListItemText primary={this.state.calendars[id].name} /> </CompactListItem>)} - </List>) || 'Please Login.'}</div> + </List></div>) || 'Please Login.'} </STableCell> </TableRow> <TableRow> diff --git a/src/background.ts b/src/background.ts index f536005..c42281d 100644 --- a/src/background.ts +++ b/src/background.ts @@ -21,6 +21,7 @@ let mainGraphData: GraphData[] = []; let dirtyMetadata = false; let dirtyCalData = false; let loadPromise: Promise<void> = null; +let auth = new gapi.Auth(); enum ChromeError { storageGetError = "storageGetError", @@ -85,7 +86,7 @@ async function loadCachedCals() { let calDataFlat: {[id: string]: gapi.GCalendarFlat} = items.calData; console.log(calDataFlat); for (let id in calDataFlat) { - calData[id] = gapi.GCalendar.inflate(calDataFlat[id]); + calData[id] = gapi.GCalendar.inflate(calDataFlat[id], auth); } console.log("cached cals loaded"); } @@ -112,10 +113,24 @@ async function saveCachedCals() { function getCalData(id: string) { if (!calData.hasOwnProperty(id)) - calData[id] = new gapi.GCalendar(id, calendars[id].name); + calData[id] = new gapi.GCalendar(id, calendars[id].name, auth); return calData[id]; } +function handleGApiError(id: string, err: gapi.GApiError) { + if (err === gapi.GApiError.fetchError) { + console.log(`${id}: fetch error`); + } else if (err === gapi.GApiError.invalidAuthToken) { + console.log(`${id}: invalid auth token`); + calendars[id].enabled = false; + } else if (err === gapi.GApiError.notLoggedIn) { + console.log(`${id}: not logged in`); + } else { + console.log(`${id}: ${err}`); + calendars[id].enabled = false; + } +} + async function getCalEvents(id: string, start: Date, end: Date) { let gcal = getCalData(id); try { @@ -123,8 +138,8 @@ async function getCalEvents(id: string, start: Date, end: Date) { dirtyCalData = res.changed; return res.events; } catch(err) { - console.log(`cannot load calendar ${id}`, err); - calendars[id].enabled = false; + handleGApiError(id, err); + console.log(`cannot load calendar ${id}`); return []; } } @@ -173,8 +188,8 @@ async function pollSync() { for (let id in calendars) { if (!calendars[id].enabled) continue; pms.push(getCalData(id).sync().catch(err => { - console.log(`cannot sync calendar ${id}`, err); - calendars[id].enabled = false; + handleGApiError(id, err); + console.log(`cannot sync calendar ${id}`); })); } await Promise.all(pms); @@ -289,6 +304,50 @@ function handleMsg(port: chrome.runtime.Port) { port.postMessage(msg.genResp(null)); break; } + case MsgType.fetchCalendars: { + (async () => { + let token = await auth.getAuthToken(); + let results = await gapi.getCalendars(token); + port.postMessage(msg.genResp(results)); + })(); + break; + } + case MsgType.fetchColors: { + (async () => { + let token = await auth.getAuthToken(); + let results = await gapi.getColors(token); + port.postMessage(msg.genResp(results)); + })(); + break; + } + case MsgType.login: { + (async () => { + let succ = true; + try { + await auth.login(); + } catch (_) { + succ = false; + } + port.postMessage(msg.genResp(succ)); + })(); + break; + } + case MsgType.logout: { + (async () => { + let succ = true; + try { + await auth.logout(); + } catch (_) { + succ = false; + } + port.postMessage(msg.genResp(succ)); + })(); + break; + } + case MsgType.getLoggedIn: { + auth.loggedIn().then(b => port.postMessage(msg.genResp(b))); + break; + } default: console.error("unknown msg opt"); } }); @@ -310,3 +369,7 @@ chrome.tabs.onCreated.addListener(function(tab) { } } }); + +chrome.runtime.onInstalled.addListener(async () => { + try { await auth.logout(); } catch (_) {} +}); diff --git a/src/gapi.ts b/src/gapi.ts index 9ddf751..4178426 100644 --- a/src/gapi.ts +++ b/src/gapi.ts @@ -3,13 +3,13 @@ import LRU from "lru-cache"; const gapiBase = 'https://www.googleapis.com/calendar/v3'; -let loggedIn: boolean = null; -enum GApiError { +export enum GApiError { invalidSyncToken = "invalidSyncToken", invalidAuthToken = "invalidAuthToken", notLoggedIn = "notLoggedIn", notLoggedOut = "notLoggedOut", + fetchError = "fetchError", otherError = "otherError", } @@ -34,43 +34,51 @@ function _removeCachedAuthToken(token: string) { chrome.identity.removeCachedAuthToken({ token }, () => resolver())); } -export async function getLoggedIn(): Promise<boolean> { - if (loggedIn === null) - { - try { - await _getAuthToken(false); - loggedIn = true; - } catch(_) { - loggedIn = false; +export class Auth { + _loggedIn: boolean; + + constructor() { + this._loggedIn = null; + } + + async loggedIn(): Promise<boolean> { + if (this._loggedIn === null) + { + try { + await _getAuthToken(false); + this._loggedIn = true; + } catch(_) { + this._loggedIn = false; + } } + return this._loggedIn; } - return loggedIn; -} -export async function getAuthToken(): Promise<string> { - let b = await getLoggedIn(); - if (b) return _getAuthToken(false); - else throw GApiError.notLoggedIn; -} + async getAuthToken(): Promise<string> { + let b = await this.loggedIn(); + if (b) return _getAuthToken(false); + else throw GApiError.notLoggedIn; + } -export async function login(): Promise<void> { - let b = await getLoggedIn(); - if (!b) { - await _getAuthToken(true); - loggedIn = true; + async login(): Promise<void> { + let b = await this.loggedIn(); + if (!b) { + await _getAuthToken(true); + this._loggedIn = true; + } + else throw GApiError.notLoggedOut; } - else throw GApiError.notLoggedOut; -} -export async function logout(): Promise<void> { - let token = await getAuthToken(); - let response = await fetch( - `https://accounts.google.com/o/oauth2/revoke?${toParams({ token })}`, - { method: 'GET' }); - //if (response.status === 200) - await _removeCachedAuthToken(token); - //else throw GApiError.otherError; - loggedIn = false; + async logout(): Promise<void> { + let token = await this.getAuthToken(); + this._loggedIn = false; + let response = await fetch( + `https://accounts.google.com/o/oauth2/revoke?${toParams({ token })}`, + { method: 'GET' }); + //if (response.status === 200) + await _removeCachedAuthToken(token); + //else throw GApiError.otherError; + } } export type GCalendarColor = { @@ -87,14 +95,24 @@ export async function getCalendars(token: string): Promise<any> { let response = await fetch( `${gapiBase}/users/me/calendarList?${toParams({access_token: token})}`, { method: 'GET' }); - return (await response.json()).items; + try { + return (await response.json()).items; + } catch (err) { + console.log(err); + throw GApiError.fetchError; + } } export async function getColors(token: string): Promise<any> { let response = await fetch( `${gapiBase}/colors?${toParams({access_token: token})}`, { method: 'GET' }); - return response.json(); + try { + return response.json(); + } catch (err) { + console.log(err); + throw GApiError.fetchError; + } } async function getEvent(calId: string, eventId: string, token: string): Promise<any> { @@ -113,14 +131,20 @@ function getEvents(calId: string, token: string, let results = [] as any[]; const singleFetch = async (pageToken: string, syncToken: string): Promise<{nextSyncToken: string, results: any[]}> => { - let response = await fetch(`${gapiBase}/calendars/${calId}/events?${toParams({ - access_token: token, - pageToken, - syncToken, - timeMin, - timeMax, - maxResults: resultsPerRequest - })}`, { method: 'GET' }); + let response; + try { + response = await fetch(`${gapiBase}/calendars/${calId}/events?${toParams({ + access_token: token, + pageToken, + syncToken, + timeMin, + timeMax, + maxResults: resultsPerRequest + })}`, { method: 'GET' }); + } catch (err) { + console.log(err); + throw GApiError.fetchError; + } switch (response.status) { case 200: { let data = await response.json(); @@ -235,11 +259,13 @@ export class GCalendar { eventMeta: { [id: string]: { keys: Set<number>, summary: string } }; options: GCalendarOptions; divider: number; + auth: Auth; - constructor(calId: string, name: string, + constructor(calId: string, name: string, auth: Auth, options={maxCachedItems: 100, nDaysPerSlot: 10, largeQuery: 10}) { this.calId = calId; this.name = name; + this.auth = auth; this.syncToken = ''; this.cache = new LRU<number, GCalendarSlot>({ max: options.maxCachedItems, @@ -275,7 +301,7 @@ export class GCalendar { } } - static inflate(obj: GCalendarFlat) { + static inflate(obj: GCalendarFlat, auth: Auth) { let cache = obj.cache.map(t => { let slot: GCalendarSlot = {}; for (let id in t.v) @@ -289,7 +315,7 @@ export class GCalendar { eventMeta[id] = { keys: new Set(m.keys), summary: m.summary }; } - let gcal = new GCalendar(obj.calId, obj.name, obj.options); + let gcal = new GCalendar(obj.calId, obj.name, auth, obj.options); gcal.syncToken = obj.syncToken; gcal.cache.load(cache); gcal.eventMeta = eventMeta; @@ -297,7 +323,7 @@ export class GCalendar { return gcal; } - get token() { return getAuthToken(); } + get token() { return this.auth.getAuthToken(); } dateToCacheKey(date: Date) { return Math.floor(date.getTime() / this.divider); @@ -9,7 +9,12 @@ export enum MsgType { updateConfig = "updateConfig", getConfig = "getConfig", getGraphData = "getGraphData", - clearCache = "clearCache" + clearCache = "clearCache", + fetchCalendars = "fetchCalendars", + fetchColors = "fetchColors", + login = "login", + logout = "logout", + getLoggedIn = "getLoggedIn" } function stringifyMsgType(opt: MsgType): string { return MsgType[opt]; } @@ -25,6 +30,11 @@ function parseMsgType(s: string): MsgType { case "getConfig": return MsgType.getConfig; case "getGraphData": return MsgType.getGraphData; case "clearCache": return MsgType.clearCache; + case "fetchCalendars": return MsgType.fetchCalendars; + case "fetchColors": return MsgType.fetchColors; + case "login": return MsgType.login; + case "logout": return MsgType.logout; + case "getLoggedIn": return MsgType.getLoggedIn; default: console.error(`unknown MsgType: ${s}`); } } diff --git a/src/tab.tsx b/src/tab.tsx index 727a3a5..8edd67e 100644 --- a/src/tab.tsx +++ b/src/tab.tsx @@ -94,11 +94,11 @@ class Tab extends React.Component<TabProps> { </IconButton> </div> <div className={classes.buttonSpacer} /> - <Grid container spacing={0} style={{ maxWidth: 1024, minWidth: 640, margin: '0 auto' }}> + <Grid container spacing={0} style={{ maxWidth: 900, minWidth: 640, margin: '0 auto' }}> { (data.length > 0 && data.map((d, idx) => ( - <Grid item key={idx} xs={12} lg={6}> + <Grid item key={idx} xs={12} md={6}> <Typography variant="subtitle1" align="center" color="textPrimary"> {d.name} </Typography> @@ -119,26 +119,30 @@ class Tab extends React.Component<TabProps> { padAngle={0.8} /> </div>) || <div style={{ - height: 250, marginLeft: 100, marginRight: 100, marginTop: 60, marginBottom: 60, - textAlign: 'center', - position: 'relative' + textAlign: 'center' }}> + <div style={{ + position: 'relative', + height: 250, + display: 'inline-block' + }}> <Donut style={{ height: '100%' }} /> <div style={{ position: 'absolute', top: -40, - left: 83, + left: 55, }}> <Typography variant="subtitle1" align="center" color="textSecondary"> No matching events. </Typography> </div> + </div> </div>} </Grid> ))) || ( |