Add: sync database to a cloud drive (#787)
* Add key * Add: a second index uuid, push changes to sync controller * Add: sync.js * Add: tokenManager * Change: log entire body for http error * Add: token flow * Fix: minor * Fix: move cleanup to stop function * Add: syncNow * Update dependencies * Fix: handle 401 error * Add: handle 401 error * Fix: then -> catch * Add: sync options to options page * Update db-to-cloud * Change: make prefs.set return a promise * Add: disble selector if connected * Add: update selector state * Fix: return promise in prefs.set * Fix: manage complex state * Fix: handle prefs change * Change: manage sync status in background * Add: show current status in the UI * Add: schedule a faster sync when db changed * Update dependencies * Add: include progress in sync status * Add: more detail status * Show status text only * Bump dependencies * Change: show loaded and total * Fix: syncTarget is undefined * Add: google and onedrive * Fix: token is not reused * Bump dependencies * Don't use minified version since it is hard to debug * Fix: expire time is incorrect * Change: switch google to code flow * Bump dependencies * Change: only modify pref if the initialization success? * Don't stop the sync if the first sync is not triggered by the user * Add: implement refresh token * Change: switch microsoft to code flow * Add: subtract expire with a latency * Add: microsoft client secret * Add: display error message * Fix: fromPref is not used * Change: try to revoke the token when log out * Add: revoke dropbox token * Fix: Google only generates one refresh token for one user by default * Bump dependencies, fix onedrive list issue * Fix: arguments sent to sync.put is wrong * Fix: don't schedule a sync on db changed if not connected * Bump dependencies. Fix issue of switching drives * Bump db-to-cloud, fix switching drive issue * Fix: only auth user on 401 error, don't display login window without user interaction * Fix: don't call revoke() if token is undefined * Add: login button to generate the access token interactively * Fix: make addMissingProperties a local * Fix: store missing props in an object * Fix: sync.getStatus should be sync * LATENCY -> NETWORK_LATENCY * Fix: cache the token forever if there is no expire time e.g. dropbox * Add some comments * Fix: i18n * Fix: i18n sync status * fixup! Fix: i18n sync status * Fix: 'sync to cloud' is displayed twice
This commit is contained in:
parent
c0fd71dda6
commit
f9db43a2e9
|
@ -974,6 +974,9 @@
|
||||||
"optionsCustomizeUpdate": {
|
"optionsCustomizeUpdate": {
|
||||||
"message": "Updates"
|
"message": "Updates"
|
||||||
},
|
},
|
||||||
|
"optionsCustomizeSync": {
|
||||||
|
"message": "Sync to cloud"
|
||||||
|
},
|
||||||
"optionsHeading": {
|
"optionsHeading": {
|
||||||
"message": "Options",
|
"message": "Options",
|
||||||
"description": "Heading for options section on manage page."
|
"description": "Heading for options section on manage page."
|
||||||
|
@ -1009,6 +1012,58 @@
|
||||||
"optionsUpdateInterval": {
|
"optionsUpdateInterval": {
|
||||||
"message": "Userstyle autoupdate interval in hours (specify 0 to disable)"
|
"message": "Userstyle autoupdate interval in hours (specify 0 to disable)"
|
||||||
},
|
},
|
||||||
|
"optionsSyncNone": {
|
||||||
|
"message": "None"
|
||||||
|
},
|
||||||
|
"optionsSyncConnect": {
|
||||||
|
"message": "Connect"
|
||||||
|
},
|
||||||
|
"optionsSyncDisconnect": {
|
||||||
|
"message": "Disconnect"
|
||||||
|
},
|
||||||
|
"optionsSyncSyncNow": {
|
||||||
|
"message": "Sync now"
|
||||||
|
},
|
||||||
|
"optionsSyncLogin": {
|
||||||
|
"message": "Login"
|
||||||
|
},
|
||||||
|
"optionsSyncStatusPull": {
|
||||||
|
"message": "Pulling style $loaded$ of $total$",
|
||||||
|
"placeholders": {
|
||||||
|
"loaded": {
|
||||||
|
"content": "$1"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"content": "$2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"optionsSyncStatusPush": {
|
||||||
|
"message": "Pushing style $loaded$ of $total$",
|
||||||
|
"placeholders": {
|
||||||
|
"loaded": {
|
||||||
|
"content": "$1"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"content": "$2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"optionsSyncStatusSyncing": {
|
||||||
|
"message": "Syncing..."
|
||||||
|
},
|
||||||
|
"optionsSyncStatusConnecting": {
|
||||||
|
"message": "Connecting..."
|
||||||
|
},
|
||||||
|
"optionsSyncStatusConnected": {
|
||||||
|
"message": "Connected"
|
||||||
|
},
|
||||||
|
"optionsSyncStatusDisconnecting": {
|
||||||
|
"message": "Disconnecting..."
|
||||||
|
},
|
||||||
|
"optionsSyncStatusDisconnected": {
|
||||||
|
"message": "Disconnected"
|
||||||
|
},
|
||||||
"paginationCurrent": {
|
"paginationCurrent": {
|
||||||
"message": "Current page",
|
"message": "Current page",
|
||||||
"description": "Tooltip for the current page index in search results"
|
"description": "Tooltip for the current page index in search results"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/* global download prefs openURL FIREFOX CHROME VIVALDI
|
/* global download prefs openURL FIREFOX CHROME VIVALDI
|
||||||
debounce URLS ignoreChromeError getTab
|
debounce URLS ignoreChromeError getTab
|
||||||
styleManager msg navigatorUtil iconUtil workerUtil contentScripts */
|
styleManager msg navigatorUtil iconUtil workerUtil contentScripts sync */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// eslint-disable-next-line no-var
|
// eslint-disable-next-line no-var
|
||||||
|
@ -63,7 +63,13 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, {
|
||||||
return browser.runtime.openOptionsPage()
|
return browser.runtime.openOptionsPage()
|
||||||
.then(() => new Promise(resolve => setTimeout(resolve, 100)))
|
.then(() => new Promise(resolve => setTimeout(resolve, 100)))
|
||||||
.then(() => msg.broadcastExtension({method: 'optionsCustomizeHotkeys'}));
|
.then(() => msg.broadcastExtension({method: 'optionsCustomizeHotkeys'}));
|
||||||
}
|
},
|
||||||
|
|
||||||
|
syncStart: sync.start,
|
||||||
|
syncStop: sync.stop,
|
||||||
|
syncNow: sync.syncNow,
|
||||||
|
getSyncStatus: sync.getStatus,
|
||||||
|
syncLogin: sync.login
|
||||||
});
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line no-var
|
// eslint-disable-next-line no-var
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/* eslint no-eq-null: 0, eqeqeq: [2, "smart"] */
|
/* eslint no-eq-null: 0, eqeqeq: [2, "smart"] */
|
||||||
/* global createCache db calcStyleDigest db tryRegExp styleCodeEmpty
|
/* global createCache db calcStyleDigest db tryRegExp styleCodeEmpty
|
||||||
getStyleWithNoCode msg */
|
getStyleWithNoCode msg sync uuid */
|
||||||
/* exported styleManager */
|
/* exported styleManager */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ const styleManager = (() => {
|
||||||
appliesTo: Set<url>
|
appliesTo: Set<url>
|
||||||
} */
|
} */
|
||||||
const styles = new Map();
|
const styles = new Map();
|
||||||
|
const uuidIndex = new Map();
|
||||||
|
|
||||||
/* url => {
|
/* url => {
|
||||||
maybeMatch: Set<styleId>,
|
maybeMatch: Set<styleId>,
|
||||||
|
@ -62,11 +63,16 @@ const styleManager = (() => {
|
||||||
|
|
||||||
handleLivePreviewConnections();
|
handleLivePreviewConnections();
|
||||||
|
|
||||||
return ensurePrepared({
|
return Object.assign({
|
||||||
|
compareRevision
|
||||||
|
}, ensurePrepared({
|
||||||
get,
|
get,
|
||||||
|
getByUUID,
|
||||||
getSectionsByUrl,
|
getSectionsByUrl,
|
||||||
|
putByUUID,
|
||||||
installStyle,
|
installStyle,
|
||||||
deleteStyle,
|
deleteStyle,
|
||||||
|
deleteByUUID,
|
||||||
editSave,
|
editSave,
|
||||||
findStyle,
|
findStyle,
|
||||||
importStyle,
|
importStyle,
|
||||||
|
@ -79,7 +85,7 @@ const styleManager = (() => {
|
||||||
removeExclusion,
|
removeExclusion,
|
||||||
addInclusion,
|
addInclusion,
|
||||||
removeInclusion
|
removeInclusion
|
||||||
});
|
}));
|
||||||
|
|
||||||
function handleLivePreviewConnections() {
|
function handleLivePreviewConnections() {
|
||||||
chrome.runtime.onConnect.addListener(port => {
|
chrome.runtime.onConnect.addListener(port => {
|
||||||
|
@ -120,11 +126,48 @@ const styleManager = (() => {
|
||||||
return noCode ? getStyleWithNoCode(data) : data;
|
return noCode ? getStyleWithNoCode(data) : data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getByUUID(uuid) {
|
||||||
|
const id = uuidIndex.get(uuid);
|
||||||
|
if (id) {
|
||||||
|
return get(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getAllStyles(noCode = false) {
|
function getAllStyles(noCode = false) {
|
||||||
const datas = [...styles.values()].map(s => s.data);
|
const datas = [...styles.values()].map(s => s.data);
|
||||||
return noCode ? datas.map(getStyleWithNoCode) : datas;
|
return noCode ? datas.map(getStyleWithNoCode) : datas;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function compareRevision(rev1, rev2) {
|
||||||
|
return rev1 - rev2;
|
||||||
|
}
|
||||||
|
|
||||||
|
function putByUUID(doc) {
|
||||||
|
const id = uuidIndex.get(doc._id);
|
||||||
|
if (id) {
|
||||||
|
doc.id = id;
|
||||||
|
} else {
|
||||||
|
delete doc.id;
|
||||||
|
}
|
||||||
|
const oldDoc = id && styles.has(id) && styles.get(id).data;
|
||||||
|
let diff = -1;
|
||||||
|
if (oldDoc) {
|
||||||
|
diff = compareRevision(oldDoc._rev, doc._rev);
|
||||||
|
if (diff > 0) {
|
||||||
|
sync.put(oldDoc._id, oldDoc._rev);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (diff < 0) {
|
||||||
|
return db.exec('put', doc)
|
||||||
|
.then(event => {
|
||||||
|
doc.id = event.target.result;
|
||||||
|
uuidIndex.set(doc._id, doc.id);
|
||||||
|
return handleSave(doc, 'sync');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function toggleStyle(id, enabled) {
|
function toggleStyle(id, enabled) {
|
||||||
const style = styles.get(id);
|
const style = styles.get(id);
|
||||||
const data = Object.assign({}, style.data, {enabled});
|
const data = Object.assign({}, style.data, {enabled});
|
||||||
|
@ -163,12 +206,11 @@ const styleManager = (() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function importMany(items) {
|
function importMany(items) {
|
||||||
|
items.forEach(beforeSave);
|
||||||
return db.exec('putMany', items)
|
return db.exec('putMany', items)
|
||||||
.then(events => {
|
.then(events => {
|
||||||
for (let i = 0; i < items.length; i++) {
|
for (let i = 0; i < items.length; i++) {
|
||||||
if (!items[i].id) {
|
afterSave(items[i], events[i].target.result);
|
||||||
items[i].id = events[i].target.result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return Promise.all(items.map(i => handleSave(i, 'import')));
|
return Promise.all(items.map(i => handleSave(i, 'import')));
|
||||||
});
|
});
|
||||||
|
@ -247,10 +289,14 @@ const styleManager = (() => {
|
||||||
return removeIncludeExclude(id, rule, 'inclusions');
|
return removeIncludeExclude(id, rule, 'inclusions');
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteStyle(id) {
|
function deleteStyle(id, reason) {
|
||||||
const style = styles.get(id);
|
const style = styles.get(id);
|
||||||
|
const rev = Date.now();
|
||||||
return db.exec('delete', id)
|
return db.exec('delete', id)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
if (reason !== 'sync') {
|
||||||
|
sync.delete(style.data._id, rev);
|
||||||
|
}
|
||||||
for (const url of style.appliesTo) {
|
for (const url of style.appliesTo) {
|
||||||
const cache = cachedStyleForUrl.get(url);
|
const cache = cachedStyleForUrl.get(url);
|
||||||
if (cache) {
|
if (cache) {
|
||||||
|
@ -258,6 +304,7 @@ const styleManager = (() => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
styles.delete(id);
|
styles.delete(id);
|
||||||
|
uuidIndex.delete(style.data._id);
|
||||||
return msg.broadcast({
|
return msg.broadcast({
|
||||||
method: 'styleDeleted',
|
method: 'styleDeleted',
|
||||||
style: {id}
|
style: {id}
|
||||||
|
@ -266,6 +313,15 @@ const styleManager = (() => {
|
||||||
.then(() => id);
|
.then(() => id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function deleteByUUID(_id, rev) {
|
||||||
|
const id = uuidIndex.get(_id);
|
||||||
|
const oldDoc = id && styles.has(id) && styles.get(id).data;
|
||||||
|
if (oldDoc && compareRevision(oldDoc._rev, rev) <= 0) {
|
||||||
|
// FIXME: does it make sense to set reason to 'sync' in deleteByUUID?
|
||||||
|
return deleteStyle(id, 'sync');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function ensurePrepared(methods) {
|
function ensurePrepared(methods) {
|
||||||
const prepared = {};
|
const prepared = {};
|
||||||
for (const [name, fn] of Object.entries(methods)) {
|
for (const [name, fn] of Object.entries(methods)) {
|
||||||
|
@ -320,19 +376,33 @@ const styleManager = (() => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveStyle(style) {
|
function beforeSave(style) {
|
||||||
if (!style.name) {
|
if (!style.name) {
|
||||||
throw new Error('style name is empty');
|
throw new Error('style name is empty');
|
||||||
}
|
}
|
||||||
if (style.id == null) {
|
if (style.id == null) {
|
||||||
delete style.id;
|
delete style.id;
|
||||||
}
|
}
|
||||||
|
if (!style._id) {
|
||||||
|
style._id = uuid();
|
||||||
|
}
|
||||||
|
style._rev = Date.now();
|
||||||
fixUsoMd5Issue(style);
|
fixUsoMd5Issue(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
function afterSave(style, newId) {
|
||||||
|
if (style.id == null) {
|
||||||
|
style.id = newId;
|
||||||
|
}
|
||||||
|
uuidIndex.set(style._id, style.id);
|
||||||
|
sync.put(style._id, style._rev);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveStyle(style) {
|
||||||
|
beforeSave(style);
|
||||||
return db.exec('put', style)
|
return db.exec('put', style)
|
||||||
.then(event => {
|
.then(event => {
|
||||||
if (style.id == null) {
|
afterSave(style, event.target.result);
|
||||||
style.id = event.target.result;
|
|
||||||
}
|
|
||||||
return style;
|
return style;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -451,22 +521,49 @@ const styleManager = (() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepare() {
|
function prepare() {
|
||||||
return db.exec('getAll').then(event => {
|
const ADD_MISSING_PROPS = {
|
||||||
const styleList = event.target.result;
|
name: style => `ID: ${style.id}`,
|
||||||
if (!styleList) {
|
_id: () => uuid(),
|
||||||
return;
|
_rev: () => Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
return db.exec('getAll')
|
||||||
|
.then(event => event.target.result || [])
|
||||||
|
.then(styleList => {
|
||||||
|
// setup missing _id, _rev
|
||||||
|
const updated = [];
|
||||||
|
for (const style of styleList) {
|
||||||
|
if (addMissingProperties(style)) {
|
||||||
|
updated.push(style);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (updated.length) {
|
||||||
|
return db.exec('putMany', updated)
|
||||||
|
.then(() => styleList);
|
||||||
|
}
|
||||||
|
return styleList;
|
||||||
|
})
|
||||||
|
.then(styleList => {
|
||||||
for (const style of styleList) {
|
for (const style of styleList) {
|
||||||
fixUsoMd5Issue(style);
|
fixUsoMd5Issue(style);
|
||||||
styles.set(style.id, {
|
styles.set(style.id, {
|
||||||
appliesTo: new Set(),
|
appliesTo: new Set(),
|
||||||
data: style
|
data: style
|
||||||
});
|
});
|
||||||
if (!style.name) {
|
uuidIndex.set(style._id, style.id);
|
||||||
style.name = 'ID: ' + style.id;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function addMissingProperties(style) {
|
||||||
|
let touched = false;
|
||||||
|
for (const key in ADD_MISSING_PROPS) {
|
||||||
|
if (!style[key]) {
|
||||||
|
style[key] = ADD_MISSING_PROPS[key](style);
|
||||||
|
touched = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return touched;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function urlMatchStyle(query, style) {
|
function urlMatchStyle(query, style) {
|
||||||
|
|
238
background/sync.js
Normal file
238
background/sync.js
Normal file
|
@ -0,0 +1,238 @@
|
||||||
|
/* global dbToCloud styleManager chromeLocal prefs tokenManager msg */
|
||||||
|
/* exported sync */
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const sync = (() => {
|
||||||
|
const SYNC_DELAY = 1; // minutes
|
||||||
|
const SYNC_INTERVAL = 30; // minutes
|
||||||
|
|
||||||
|
const status = {
|
||||||
|
state: 'disconnected',
|
||||||
|
syncing: false,
|
||||||
|
progress: null,
|
||||||
|
currentDriveName: null,
|
||||||
|
errorMessage: null,
|
||||||
|
login: false
|
||||||
|
};
|
||||||
|
let currentDrive;
|
||||||
|
const ctrl = dbToCloud.dbToCloud({
|
||||||
|
onGet(id) {
|
||||||
|
return styleManager.getByUUID(id);
|
||||||
|
},
|
||||||
|
onPut(doc) {
|
||||||
|
return styleManager.putByUUID(doc);
|
||||||
|
},
|
||||||
|
onDelete(id, rev) {
|
||||||
|
return styleManager.deleteByUUID(id, rev);
|
||||||
|
},
|
||||||
|
onFirstSync() {
|
||||||
|
return styleManager.getAllStyles()
|
||||||
|
.then(styles => {
|
||||||
|
styles.forEach(i => ctrl.put(i._id, i._rev));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onProgress,
|
||||||
|
compareRevision(a, b) {
|
||||||
|
return styleManager.compareRevision(a, b);
|
||||||
|
},
|
||||||
|
getState(drive) {
|
||||||
|
const key = `sync/state/${drive.name}`;
|
||||||
|
return chromeLocal.get(key)
|
||||||
|
.then(obj => obj[key]);
|
||||||
|
},
|
||||||
|
setState(drive, state) {
|
||||||
|
const key = `sync/state/${drive.name}`;
|
||||||
|
return chromeLocal.set({
|
||||||
|
[key]: state
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const initializing = prefs.initializing.then(() => {
|
||||||
|
prefs.subscribe(['sync.enabled'], onPrefChange);
|
||||||
|
onPrefChange(null, prefs.get('sync.enabled'));
|
||||||
|
});
|
||||||
|
|
||||||
|
chrome.alarms.onAlarm.addListener(info => {
|
||||||
|
if (info.name === 'syncNow') {
|
||||||
|
syncNow().catch(console.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.assign({
|
||||||
|
getStatus: () => status
|
||||||
|
}, ensurePrepared({
|
||||||
|
start,
|
||||||
|
stop,
|
||||||
|
put: (...args) => {
|
||||||
|
if (!currentDrive) return;
|
||||||
|
schedule();
|
||||||
|
return ctrl.put(...args);
|
||||||
|
},
|
||||||
|
delete: (...args) => {
|
||||||
|
if (!currentDrive) return;
|
||||||
|
schedule();
|
||||||
|
return ctrl.delete(...args);
|
||||||
|
},
|
||||||
|
syncNow,
|
||||||
|
login
|
||||||
|
}));
|
||||||
|
|
||||||
|
function ensurePrepared(obj) {
|
||||||
|
return Object.entries(obj).reduce((o, [key, fn]) => {
|
||||||
|
o[key] = (...args) =>
|
||||||
|
initializing.then(() => fn(...args));
|
||||||
|
return o;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onProgress(e) {
|
||||||
|
if (e.phase === 'start') {
|
||||||
|
status.syncing = true;
|
||||||
|
} else if (e.phase === 'end') {
|
||||||
|
status.syncing = false;
|
||||||
|
status.progress = null;
|
||||||
|
} else {
|
||||||
|
status.progress = e;
|
||||||
|
}
|
||||||
|
emitStatusChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
function schedule(delay = SYNC_DELAY) {
|
||||||
|
chrome.alarms.create('syncNow', {
|
||||||
|
delayInMinutes: delay,
|
||||||
|
periodInMinutes: SYNC_INTERVAL
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPrefChange(key, value) {
|
||||||
|
if (value === 'none') {
|
||||||
|
stop().catch(console.error);
|
||||||
|
} else {
|
||||||
|
start(value, true).catch(console.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function withFinally(p, cleanup) {
|
||||||
|
return p.then(
|
||||||
|
result => {
|
||||||
|
cleanup(undefined, result);
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
cleanup(err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncNow() {
|
||||||
|
if (!currentDrive) {
|
||||||
|
return Promise.reject(new Error('cannot sync when disconnected'));
|
||||||
|
}
|
||||||
|
return withFinally(
|
||||||
|
(ctrl.isInit() ? ctrl.syncNow() : ctrl.start())
|
||||||
|
.catch(handle401Error),
|
||||||
|
err => {
|
||||||
|
status.errorMessage = err ? err.message : null;
|
||||||
|
emitStatusChange();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle401Error(err) {
|
||||||
|
if (err.code === 401) {
|
||||||
|
return tokenManager.revokeToken(currentDrive.name)
|
||||||
|
.catch(console.error)
|
||||||
|
.then(() => {
|
||||||
|
status.login = false;
|
||||||
|
emitStatusChange();
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (/User interaction required|Requires user interaction/i.test(err.message)) {
|
||||||
|
status.login = false;
|
||||||
|
emitStatusChange();
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitStatusChange() {
|
||||||
|
msg.broadcastExtension({method: 'syncStatusUpdate', status});
|
||||||
|
}
|
||||||
|
|
||||||
|
function login(name = prefs.get('sync.enabled')) {
|
||||||
|
return tokenManager.getToken(name, true)
|
||||||
|
.catch(err => {
|
||||||
|
if (/Authorization page could not be loaded/i.test(err.message)) {
|
||||||
|
// FIXME: Chrome always fails at the first login so we try again
|
||||||
|
return tokenManager.getToken(name);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
status.login = true;
|
||||||
|
emitStatusChange();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function start(name, fromPref = false) {
|
||||||
|
if (currentDrive) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
currentDrive = getDrive(name);
|
||||||
|
ctrl.use(currentDrive);
|
||||||
|
status.state = 'connecting';
|
||||||
|
status.currentDriveName = currentDrive.name;
|
||||||
|
status.login = true;
|
||||||
|
emitStatusChange();
|
||||||
|
return withFinally(
|
||||||
|
(fromPref ? Promise.resolve() : login(name))
|
||||||
|
.catch(handle401Error)
|
||||||
|
.then(() => syncNow()),
|
||||||
|
err => {
|
||||||
|
// FIXME: should we move this logic to options.js?
|
||||||
|
if (err && !fromPref) {
|
||||||
|
console.error(err);
|
||||||
|
return stop();
|
||||||
|
}
|
||||||
|
prefs.set('sync.enabled', name);
|
||||||
|
schedule(SYNC_INTERVAL);
|
||||||
|
status.state = 'connected';
|
||||||
|
emitStatusChange();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDrive(name) {
|
||||||
|
if (name === 'dropbox' || name === 'google' || name === 'onedrive') {
|
||||||
|
return dbToCloud.drive[name]({
|
||||||
|
getAccessToken: () => tokenManager.getToken(name)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw new Error(`unknown cloud name: ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stop() {
|
||||||
|
if (!currentDrive) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
chrome.alarms.clear('syncNow');
|
||||||
|
status.state = 'disconnecting';
|
||||||
|
emitStatusChange();
|
||||||
|
return withFinally(
|
||||||
|
ctrl.stop()
|
||||||
|
.then(() => tokenManager.revokeToken(currentDrive.name))
|
||||||
|
.then(() => chromeLocal.remove(`sync/state/${currentDrive.name}`)),
|
||||||
|
() => {
|
||||||
|
currentDrive = null;
|
||||||
|
prefs.set('sync.enabled', 'none');
|
||||||
|
status.state = 'disconnected';
|
||||||
|
status.currentDriveName = null;
|
||||||
|
status.login = false;
|
||||||
|
emitStatusChange();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})();
|
226
background/token-manager.js
Normal file
226
background/token-manager.js
Normal file
|
@ -0,0 +1,226 @@
|
||||||
|
/* global chromeLocal promisify */
|
||||||
|
/* exported tokenManager */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const tokenManager = (() => {
|
||||||
|
const launchWebAuthFlow = promisify(chrome.identity.launchWebAuthFlow.bind(chrome.identity));
|
||||||
|
const AUTH = {
|
||||||
|
dropbox: {
|
||||||
|
flow: 'token',
|
||||||
|
clientId: 'zg52vphuapvpng9',
|
||||||
|
authURL: 'https://www.dropbox.com/oauth2/authorize',
|
||||||
|
tokenURL: 'https://api.dropboxapi.com/oauth2/token',
|
||||||
|
revoke: token =>
|
||||||
|
fetch('https://api.dropboxapi.com/2/auth/token/revoke', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
google: {
|
||||||
|
flow: 'code',
|
||||||
|
clientId: '283762574871-d4u58s4arra5jdan2gr00heasjlttt1e.apps.googleusercontent.com',
|
||||||
|
clientSecret: 'J0nc5TlR_0V_ex9-sZk-5faf',
|
||||||
|
authURL: 'https://accounts.google.com/o/oauth2/v2/auth',
|
||||||
|
authQuery: {
|
||||||
|
// NOTE: Google needs 'prompt' parameter to deliver multiple refresh
|
||||||
|
// tokens for multiple machines.
|
||||||
|
// https://stackoverflow.com/q/18519185
|
||||||
|
access_type: 'offline',
|
||||||
|
prompt: 'consent'
|
||||||
|
},
|
||||||
|
tokenURL: 'https://oauth2.googleapis.com/token',
|
||||||
|
scopes: ['https://www.googleapis.com/auth/drive.appdata'],
|
||||||
|
revoke: token => {
|
||||||
|
const params = {token};
|
||||||
|
return postQuery(`https://accounts.google.com/o/oauth2/revoke?${stringifyQuery(params)}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onedrive: {
|
||||||
|
flow: 'code',
|
||||||
|
clientId: '3864ce03-867c-4ad8-9856-371a097d47b1',
|
||||||
|
clientSecret: '9Pj=TpsrStq8K@1BiwB9PIWLppM:@s=w',
|
||||||
|
authURL: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
|
||||||
|
tokenURL: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||||
|
redirect_uri: 'https://clngdbkpkpeebahjckkjfobafhncgmne.chromiumapp.org/',
|
||||||
|
scopes: ['Files.ReadWrite.AppFolder', 'offline_access']
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const NETWORK_LATENCY = 30; // seconds
|
||||||
|
|
||||||
|
return {getToken, revokeToken, getClientId, buildKeys};
|
||||||
|
|
||||||
|
function getClientId(name) {
|
||||||
|
return AUTH[name].clientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildKeys(name) {
|
||||||
|
const k = {
|
||||||
|
TOKEN: `secure/token/${name}/token`,
|
||||||
|
EXPIRE: `secure/token/${name}/expire`,
|
||||||
|
REFRESH: `secure/token/${name}/refresh`,
|
||||||
|
};
|
||||||
|
k.LIST = Object.values(k);
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getToken(name, interactive) {
|
||||||
|
const k = buildKeys(name);
|
||||||
|
return chromeLocal.get(k.LIST)
|
||||||
|
.then(obj => {
|
||||||
|
if (!obj[k.TOKEN]) {
|
||||||
|
return authUser(name, k, interactive);
|
||||||
|
}
|
||||||
|
if (!obj[k.EXPIRE] || Date.now() < obj[k.EXPIRE]) {
|
||||||
|
return obj[k.TOKEN];
|
||||||
|
}
|
||||||
|
if (obj[k.REFRESH]) {
|
||||||
|
return refreshToken(name, k, obj)
|
||||||
|
.catch(err => {
|
||||||
|
if (err.code === 401) {
|
||||||
|
return authUser(name, k, interactive);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return authUser(name, k, interactive);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function revokeToken(name) {
|
||||||
|
const provider = AUTH[name];
|
||||||
|
const k = buildKeys(name);
|
||||||
|
return revoke()
|
||||||
|
.then(() => chromeLocal.remove(k.LIST));
|
||||||
|
|
||||||
|
function revoke() {
|
||||||
|
if (!provider.revoke) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return chromeLocal.get(k.TOKEN)
|
||||||
|
.then(obj => {
|
||||||
|
if (obj[k.TOKEN]) {
|
||||||
|
return provider.revoke(obj[k.TOKEN]);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshToken(name, k, obj) {
|
||||||
|
if (!obj[k.REFRESH]) {
|
||||||
|
return Promise.reject(new Error('no refresh token'));
|
||||||
|
}
|
||||||
|
const provider = AUTH[name];
|
||||||
|
const body = {
|
||||||
|
client_id: provider.clientId,
|
||||||
|
refresh_token: obj[k.REFRESH],
|
||||||
|
grant_type: 'refresh_token',
|
||||||
|
scope: provider.scopes.join(' ')
|
||||||
|
};
|
||||||
|
if (provider.clientSecret) {
|
||||||
|
body.client_secret = provider.clientSecret;
|
||||||
|
}
|
||||||
|
return postQuery(provider.tokenURL, body)
|
||||||
|
.then(result => {
|
||||||
|
if (!result.refresh_token) {
|
||||||
|
// reuse old refresh token
|
||||||
|
result.refresh_token = obj[k.REFRESH];
|
||||||
|
}
|
||||||
|
return handleTokenResult(result, k);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringifyQuery(obj) {
|
||||||
|
const search = new URLSearchParams();
|
||||||
|
for (const key of Object.keys(obj)) {
|
||||||
|
search.set(key, obj[key]);
|
||||||
|
}
|
||||||
|
return search.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function authUser(name, k, interactive = false) {
|
||||||
|
const provider = AUTH[name];
|
||||||
|
const state = Math.random().toFixed(8).slice(2);
|
||||||
|
const query = {
|
||||||
|
response_type: provider.flow,
|
||||||
|
client_id: provider.clientId,
|
||||||
|
redirect_uri: provider.redirect_uri || chrome.identity.getRedirectURL(),
|
||||||
|
state
|
||||||
|
};
|
||||||
|
if (provider.scopes) {
|
||||||
|
query.scope = provider.scopes.join(' ');
|
||||||
|
}
|
||||||
|
if (provider.authQuery) {
|
||||||
|
Object.assign(query, provider.authQuery);
|
||||||
|
}
|
||||||
|
const url = `${provider.authURL}?${stringifyQuery(query)}`;
|
||||||
|
return launchWebAuthFlow({
|
||||||
|
url,
|
||||||
|
interactive
|
||||||
|
})
|
||||||
|
.then(url => {
|
||||||
|
const params = new URLSearchParams(
|
||||||
|
provider.flow === 'token' ?
|
||||||
|
new URL(url).hash.slice(1) :
|
||||||
|
new URL(url).search.slice(1)
|
||||||
|
);
|
||||||
|
if (params.get('state') !== state) {
|
||||||
|
throw new Error(`unexpected state: ${params.get('state')}, expected: ${state}`);
|
||||||
|
}
|
||||||
|
if (provider.flow === 'token') {
|
||||||
|
const obj = {};
|
||||||
|
for (const [key, value] of params.entries()) {
|
||||||
|
obj[key] = value;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
const code = params.get('code');
|
||||||
|
const body = {
|
||||||
|
code,
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
client_id: provider.clientId,
|
||||||
|
redirect_uri: provider.redirect_uri || chrome.identity.getRedirectURL()
|
||||||
|
};
|
||||||
|
if (provider.clientSecret) {
|
||||||
|
body.client_secret = provider.clientSecret;
|
||||||
|
}
|
||||||
|
return postQuery(provider.tokenURL, body);
|
||||||
|
})
|
||||||
|
.then(result => handleTokenResult(result, k));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTokenResult(result, k) {
|
||||||
|
return chromeLocal.set({
|
||||||
|
[k.TOKEN]: result.access_token,
|
||||||
|
[k.EXPIRE]: result.expires_in ? Date.now() + (Number(result.expires_in) - NETWORK_LATENCY) * 1000 : undefined,
|
||||||
|
[k.REFRESH]: result.refresh_token
|
||||||
|
})
|
||||||
|
.then(() => result.access_token);
|
||||||
|
}
|
||||||
|
|
||||||
|
function postQuery(url, body) {
|
||||||
|
const options = {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (body) {
|
||||||
|
options.body = stringifyQuery(body);
|
||||||
|
}
|
||||||
|
return fetch(url, options)
|
||||||
|
.then(r => {
|
||||||
|
if (r.ok) {
|
||||||
|
return r.json();
|
||||||
|
}
|
||||||
|
return r.text()
|
||||||
|
.then(body => {
|
||||||
|
const err = new Error(`failed to fetch (${r.status}): ${body}`);
|
||||||
|
err.code = r.status;
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
|
@ -1,6 +1,6 @@
|
||||||
/* exported getActiveTab onTabReady stringAsRegExp getTabRealURL openURL
|
/* exported getActiveTab onTabReady stringAsRegExp getTabRealURL openURL
|
||||||
getStyleWithNoCode tryRegExp sessionStorageHash download deepEqual
|
getStyleWithNoCode tryRegExp sessionStorageHash download deepEqual
|
||||||
closeCurrentTab */
|
closeCurrentTab capitalize */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const CHROME = Boolean(chrome.app) && parseInt(navigator.userAgent.match(/Chrom\w+\/(?:\d+\.){2}(\d+)|$/)[1]);
|
const CHROME = Boolean(chrome.app) && parseInt(navigator.userAgent.match(/Chrom\w+\/(?:\d+\.){2}(\d+)|$/)[1]);
|
||||||
|
@ -509,3 +509,7 @@ function closeCurrentTab() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function capitalize(s) {
|
||||||
|
return s[0].toUpperCase() + s.slice(1);
|
||||||
|
}
|
||||||
|
|
23
js/prefs.js
23
js/prefs.js
|
@ -88,6 +88,8 @@ const prefs = (() => {
|
||||||
'hotkey.openManage': '',
|
'hotkey.openManage': '',
|
||||||
'hotkey.styleDisableAll': '',
|
'hotkey.styleDisableAll': '',
|
||||||
|
|
||||||
|
'sync.enabled': 'none',
|
||||||
|
|
||||||
'iconset': 0, // 0 = dark-themed icon
|
'iconset': 0, // 0 = dark-themed icon
|
||||||
// 1 = light-themed icon
|
// 1 = light-themed icon
|
||||||
|
|
||||||
|
@ -105,7 +107,10 @@ const prefs = (() => {
|
||||||
specific: new Map(),
|
specific: new Map(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const initializing = promisify(chrome.storage.sync.get.bind(chrome.storage.sync))('settings')
|
const syncSet = promisify(chrome.storage.sync.set.bind(chrome.storage.sync));
|
||||||
|
const syncGet = promisify(chrome.storage.sync.get.bind(chrome.storage.sync));
|
||||||
|
|
||||||
|
const initializing = syncGet('settings')
|
||||||
.then(result => {
|
.then(result => {
|
||||||
if (result.settings) {
|
if (result.settings) {
|
||||||
setAll(result.settings, true);
|
setAll(result.settings, true);
|
||||||
|
@ -208,10 +213,10 @@ const prefs = (() => {
|
||||||
}
|
}
|
||||||
values[key] = value;
|
values[key] = value;
|
||||||
emitChange(key, value);
|
emitChange(key, value);
|
||||||
if (synced || timer) {
|
if (!synced && !timer) {
|
||||||
return;
|
timer = syncPrefsLater();
|
||||||
}
|
}
|
||||||
timer = setTimeout(syncPrefs);
|
return timer;
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitChange(key, value) {
|
function emitChange(key, value) {
|
||||||
|
@ -228,10 +233,14 @@ const prefs = (() => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncPrefs() {
|
function syncPrefsLater() {
|
||||||
// FIXME: we always set the entire object? Ideally, this should only use `changes`.
|
return new Promise((resolve, reject) => {
|
||||||
chrome.storage.sync.set({settings: values});
|
setTimeout(() => {
|
||||||
timer = null;
|
timer = null;
|
||||||
|
syncSet({settings: values})
|
||||||
|
.then(resolve, reject);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function equal(a, b) {
|
function equal(a, b) {
|
||||||
|
|
|
@ -35,6 +35,11 @@
|
||||||
"js/script-loader.js",
|
"js/script-loader.js",
|
||||||
"js/usercss.js",
|
"js/usercss.js",
|
||||||
"js/cache.js",
|
"js/cache.js",
|
||||||
|
"vendor/semver-bundle/semver.js",
|
||||||
|
"vendor/db-to-cloud/db-to-cloud.min.js",
|
||||||
|
"vendor/uuid/uuid.min.js",
|
||||||
|
"background/token-manager.js",
|
||||||
|
"background/sync.js",
|
||||||
"background/content-scripts.js",
|
"background/content-scripts.js",
|
||||||
"background/db.js",
|
"background/db.js",
|
||||||
"background/style-manager.js",
|
"background/style-manager.js",
|
||||||
|
@ -45,8 +50,7 @@
|
||||||
"background/style-via-api.js",
|
"background/style-via-api.js",
|
||||||
"background/search-db.js",
|
"background/search-db.js",
|
||||||
"background/update.js",
|
"background/update.js",
|
||||||
"background/openusercss-api.js",
|
"background/openusercss-api.js"
|
||||||
"vendor/semver-bundle/semver.js"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"commands": {
|
"commands": {
|
||||||
|
@ -130,5 +134,6 @@
|
||||||
"id": "{7a7a4a92-a2a0-41d1-9fd7-1e92480d612d}",
|
"id": "{7a7a4a92-a2a0-41d1-9fd7-1e92480d612d}",
|
||||||
"strict_min_version": "53.0"
|
"strict_min_version": "53.0"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2ypG+Z/beZtoYrxxwXYhMwQiAiwRVnSHqdpOSzJdjsXVWdvJjlgEuZcU8kte75w58P45LsRUrwvU6N9x12S6eW84KNEBC8rlZj0RGNoxuhSAcdxneYzjJ9tBkZKOidVedYHHsi3LeaXiLuTNTBR+2lf3uCNcP0ebaFML9uDLdYTGEW4eL3hnEKYPSmT1/xkh4bSGTToCg4YNuWWWoTA0beEOpBWYkPVMarLDQgPzMN5Byu5w3lOS2zL0PPJcmdyk3ez/ZsB4PZKU+h17fVA6+YTvUfxUqLde5i2RiuZhEb6Coo5/W90ZW1yCDC9osjWrxMGYeUMQWHPIgFtDhk4K6QIDAQAB"
|
||||||
}
|
}
|
||||||
|
|
21
options.html
21
options.html
|
@ -137,6 +137,27 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="block sync-options">
|
||||||
|
<h1 i18n-text="optionsCustomizeSync"></h1>
|
||||||
|
<div class="items">
|
||||||
|
<label>
|
||||||
|
<span class="sync-status"></span>
|
||||||
|
<select class="cloud-name">
|
||||||
|
<option value="none" i18n-text="optionsSyncNone"></option>
|
||||||
|
<option value="dropbox">Dropbox</option>
|
||||||
|
<option value="google">Google Drive</option>
|
||||||
|
<option value="onedrive">OneDrive</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<div class="actions">
|
||||||
|
<button type="button" class="connect" i18n-text="optionsSyncConnect"></button>
|
||||||
|
<button type="button" class="disconnect" i18n-text="optionsSyncDisconnect"></button>
|
||||||
|
<button type="button" class="sync-now" i18n-text="optionsSyncSyncNow"></button>
|
||||||
|
<button type="button" class="sync-login" i18n-text="optionsSyncLogin"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="block" id="advanced">
|
<div class="block" id="advanced">
|
||||||
<div class="collapsible-resizer">
|
<div class="collapsible-resizer">
|
||||||
<h1 i18n-text="optionsAdvanced">
|
<h1 i18n-text="optionsAdvanced">
|
||||||
|
|
|
@ -313,3 +313,12 @@ html:not(.firefox):not(.opera) #updates {
|
||||||
hyphens: auto;
|
hyphens: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sync-status {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.sync-status::first-letter {
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/* global messageBox msg setupLivePrefs enforceInputRange
|
/* global messageBox msg setupLivePrefs enforceInputRange
|
||||||
$ $$ $create $createLink
|
$ $$ $create $createLink
|
||||||
FIREFOX OPERA CHROME URLS openURL prefs t API ignoreChromeError
|
FIREFOX OPERA CHROME URLS openURL prefs t API ignoreChromeError
|
||||||
CHROME_HAS_BORDER_BUG */
|
CHROME_HAS_BORDER_BUG capitalize */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
setupLivePrefs();
|
setupLivePrefs();
|
||||||
|
@ -82,6 +82,88 @@ document.onclick = e => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// sync to cloud
|
||||||
|
(() => {
|
||||||
|
const cloud = document.querySelector('.sync-options .cloud-name');
|
||||||
|
const connectButton = document.querySelector('.sync-options .connect');
|
||||||
|
const disconnectButton = document.querySelector('.sync-options .disconnect');
|
||||||
|
const syncButton = document.querySelector('.sync-options .sync-now');
|
||||||
|
const statusText = document.querySelector('.sync-options .sync-status');
|
||||||
|
const loginButton = document.querySelector('.sync-options .sync-login');
|
||||||
|
|
||||||
|
let status = {};
|
||||||
|
|
||||||
|
msg.onExtension(e => {
|
||||||
|
if (e.method === 'syncStatusUpdate') {
|
||||||
|
status = e.status;
|
||||||
|
updateButtons();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
API.getSyncStatus()
|
||||||
|
.then(_status => {
|
||||||
|
status = _status;
|
||||||
|
updateButtons();
|
||||||
|
});
|
||||||
|
|
||||||
|
function validClick(e) {
|
||||||
|
return e.button === 0 && !e.ctrl && !e.alt && !e.shift;
|
||||||
|
}
|
||||||
|
|
||||||
|
cloud.addEventListener('change', updateButtons);
|
||||||
|
|
||||||
|
function updateButtons() {
|
||||||
|
if (status.currentDriveName) {
|
||||||
|
cloud.value = status.currentDriveName;
|
||||||
|
}
|
||||||
|
cloud.disabled = status.state !== 'disconnected';
|
||||||
|
connectButton.disabled = status.state !== 'disconnected' || cloud.value === 'none';
|
||||||
|
disconnectButton.disabled = status.state !== 'connected' || status.syncing;
|
||||||
|
syncButton.disabled = status.state !== 'connected' || status.syncing;
|
||||||
|
statusText.textContent = getStatusText();
|
||||||
|
loginButton.style.display = status.state === 'connected' && !status.login ? '' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusText() {
|
||||||
|
if (status.syncing) {
|
||||||
|
if (status.progress) {
|
||||||
|
const {phase, loaded, total} = status.progress;
|
||||||
|
return chrome.i18n.getMessage(`optionsSyncStatus${capitalize(phase)}`, [loaded + 1, total]) ||
|
||||||
|
`${phase} ${loaded} / ${total}`;
|
||||||
|
}
|
||||||
|
return chrome.i18n.getMessage('optionsSyncStatusSyncing') || 'syncing';
|
||||||
|
}
|
||||||
|
if ((status.state === 'connected' || status.state === 'disconnected') && status.errorMessage) {
|
||||||
|
return status.errorMessage;
|
||||||
|
}
|
||||||
|
return chrome.i18n.getMessage(`optionsSyncStatus${capitalize(status.state)}`) || status.state;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectButton.addEventListener('click', e => {
|
||||||
|
if (validClick(e)) {
|
||||||
|
API.syncStart(cloud.value).catch(console.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
disconnectButton.addEventListener('click', e => {
|
||||||
|
if (validClick(e)) {
|
||||||
|
API.syncStop().catch(console.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
syncButton.addEventListener('click', e => {
|
||||||
|
if (validClick(e)) {
|
||||||
|
API.syncNow().catch(console.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
loginButton.addEventListener('click', e => {
|
||||||
|
if (validClick(e)) {
|
||||||
|
API.syncLogin().catch(console.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
function checkUpdates() {
|
function checkUpdates() {
|
||||||
let total = 0;
|
let total = 0;
|
||||||
let checked = 0;
|
let checked = 0;
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"archiver": "^3.1.1",
|
"archiver": "^3.1.1",
|
||||||
"codemirror": "^5.48.4",
|
"codemirror": "^5.48.4",
|
||||||
|
"db-to-cloud": "^0.4.5",
|
||||||
"dropbox": "^4.0.30",
|
"dropbox": "^4.0.30",
|
||||||
"endent": "^1.3.0",
|
"endent": "^1.3.0",
|
||||||
"eslint": "^6.3.0",
|
"eslint": "^6.3.0",
|
||||||
|
|
|
@ -40,6 +40,9 @@ const files = {
|
||||||
'vendor/inflate.js → inflate.js',
|
'vendor/inflate.js → inflate.js',
|
||||||
'vendor/z-worker.js → z-worker.js',
|
'vendor/z-worker.js → z-worker.js',
|
||||||
'vendor/zip.js → zip.js'
|
'vendor/zip.js → zip.js'
|
||||||
|
],
|
||||||
|
'db-to-cloud': [
|
||||||
|
'dist/db-to-cloud.min.js → db-to-cloud.min.js'
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,8 @@ function createZip() {
|
||||||
'package.json',
|
'package.json',
|
||||||
'package-lock.json',
|
'package-lock.json',
|
||||||
'yarn.lock',
|
'yarn.lock',
|
||||||
'*.zip'
|
'*.zip',
|
||||||
|
'*.map'
|
||||||
];
|
];
|
||||||
|
|
||||||
const file = fs.createWriteStream(fileName);
|
const file = fs.createWriteStream(fileName);
|
||||||
|
|
2
vendor/codemirror/README.md
vendored
2
vendor/codemirror/README.md
vendored
|
@ -1,3 +1,3 @@
|
||||||
## CodeMirror v5.48.0
|
## CodeMirror v5.48.4
|
||||||
|
|
||||||
Only files & folders that exist in the `vendor/codemirror` folder are copied from the `node_modules/codemirror` folder. Except all theme files are copied, in case new themes have been added.
|
Only files & folders that exist in the `vendor/codemirror` folder are copied from the `node_modules/codemirror` folder. Except all theme files are copied, in case new themes have been added.
|
||||||
|
|
11
vendor/codemirror/addon/fold/foldgutter.js
vendored
11
vendor/codemirror/addon/fold/foldgutter.js
vendored
|
@ -51,8 +51,13 @@
|
||||||
|
|
||||||
function isFolded(cm, line) {
|
function isFolded(cm, line) {
|
||||||
var marks = cm.findMarks(Pos(line, 0), Pos(line + 1, 0));
|
var marks = cm.findMarks(Pos(line, 0), Pos(line + 1, 0));
|
||||||
for (var i = 0; i < marks.length; ++i)
|
for (var i = 0; i < marks.length; ++i) {
|
||||||
if (marks[i].__isFold && marks[i].find().from.line == line) return marks[i];
|
if (marks[i].__isFold) {
|
||||||
|
var fromPos = marks[i].find(-1);
|
||||||
|
if (fromPos && fromPos.line === line)
|
||||||
|
return marks[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function marker(spec) {
|
function marker(spec) {
|
||||||
|
@ -100,7 +105,7 @@
|
||||||
if (gutter != opts.gutter) return;
|
if (gutter != opts.gutter) return;
|
||||||
var folded = isFolded(cm, line);
|
var folded = isFolded(cm, line);
|
||||||
if (folded) folded.clear();
|
if (folded) folded.clear();
|
||||||
else cm.foldCode(Pos(line, 0), opts.rangeFinder);
|
else cm.foldCode(Pos(line, 0), opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onChange(cm) {
|
function onChange(cm) {
|
||||||
|
|
11
vendor/codemirror/keymap/vim.js
vendored
11
vendor/codemirror/keymap/vim.js
vendored
|
@ -4073,7 +4073,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unescape \ and / in the replace part, for PCRE mode.
|
// Unescape \ and / in the replace part, for PCRE mode.
|
||||||
var unescapes = {'\\/': '/', '\\\\': '\\', '\\n': '\n', '\\r': '\r', '\\t': '\t'};
|
var unescapes = {'\\/': '/', '\\\\': '\\', '\\n': '\n', '\\r': '\r', '\\t': '\t', '\\&':'&'};
|
||||||
function unescapeRegexReplace(str) {
|
function unescapeRegexReplace(str) {
|
||||||
var stream = new CodeMirror.StringStream(str);
|
var stream = new CodeMirror.StringStream(str);
|
||||||
var output = [];
|
var output = [];
|
||||||
|
@ -4861,6 +4861,9 @@
|
||||||
var global = false; // True to replace all instances on a line, false to replace only 1.
|
var global = false; // True to replace all instances on a line, false to replace only 1.
|
||||||
if (tokens.length) {
|
if (tokens.length) {
|
||||||
regexPart = tokens[0];
|
regexPart = tokens[0];
|
||||||
|
if (getOption('pcre') && regexPart !== '') {
|
||||||
|
regexPart = new RegExp(regexPart).source; //normalize not escaped characters
|
||||||
|
}
|
||||||
replacePart = tokens[1];
|
replacePart = tokens[1];
|
||||||
if (regexPart && regexPart[regexPart.length - 1] === '$') {
|
if (regexPart && regexPart[regexPart.length - 1] === '$') {
|
||||||
regexPart = regexPart.slice(0, regexPart.length - 1) + '\\n';
|
regexPart = regexPart.slice(0, regexPart.length - 1) + '\\n';
|
||||||
|
@ -4868,7 +4871,7 @@
|
||||||
}
|
}
|
||||||
if (replacePart !== undefined) {
|
if (replacePart !== undefined) {
|
||||||
if (getOption('pcre')) {
|
if (getOption('pcre')) {
|
||||||
replacePart = unescapeRegexReplace(replacePart);
|
replacePart = unescapeRegexReplace(replacePart.replace(/([^\\])&/g,"$1$$&"));
|
||||||
} else {
|
} else {
|
||||||
replacePart = translateRegexReplace(replacePart);
|
replacePart = translateRegexReplace(replacePart);
|
||||||
}
|
}
|
||||||
|
@ -4899,9 +4902,13 @@
|
||||||
global = true;
|
global = true;
|
||||||
flagsPart.replace('g', '');
|
flagsPart.replace('g', '');
|
||||||
}
|
}
|
||||||
|
if (getOption('pcre')) {
|
||||||
|
regexPart = regexPart + '/' + flagsPart;
|
||||||
|
} else {
|
||||||
regexPart = regexPart.replace(/\//g, "\\/") + '/' + flagsPart;
|
regexPart = regexPart.replace(/\//g, "\\/") + '/' + flagsPart;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (regexPart) {
|
if (regexPart) {
|
||||||
// If regex part is empty, then use the previous query. Otherwise use
|
// If regex part is empty, then use the previous query. Otherwise use
|
||||||
// the regex part as the new query.
|
// the regex part as the new query.
|
||||||
|
|
11
vendor/codemirror/lib/codemirror.css
vendored
11
vendor/codemirror/lib/codemirror.css
vendored
|
@ -13,7 +13,8 @@
|
||||||
.CodeMirror-lines {
|
.CodeMirror-lines {
|
||||||
padding: 4px 0; /* Vertical padding around content */
|
padding: 4px 0; /* Vertical padding around content */
|
||||||
}
|
}
|
||||||
.CodeMirror pre {
|
.CodeMirror pre.CodeMirror-line,
|
||||||
|
.CodeMirror pre.CodeMirror-line-like {
|
||||||
padding: 0 4px; /* Horizontal padding of content */
|
padding: 0 4px; /* Horizontal padding of content */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,7 +97,7 @@
|
||||||
|
|
||||||
.CodeMirror-rulers {
|
.CodeMirror-rulers {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0; right: 0; top: -50px; bottom: -20px;
|
left: 0; right: 0; top: -50px; bottom: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.CodeMirror-ruler {
|
.CodeMirror-ruler {
|
||||||
|
@ -236,7 +237,8 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;}
|
||||||
cursor: text;
|
cursor: text;
|
||||||
min-height: 1px; /* prevents collapsing before first draw */
|
min-height: 1px; /* prevents collapsing before first draw */
|
||||||
}
|
}
|
||||||
.CodeMirror pre {
|
.CodeMirror pre.CodeMirror-line,
|
||||||
|
.CodeMirror pre.CodeMirror-line-like {
|
||||||
/* Reset some styles that the rest of the page might have set */
|
/* Reset some styles that the rest of the page might have set */
|
||||||
-moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
|
-moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
|
||||||
border-width: 0;
|
border-width: 0;
|
||||||
|
@ -255,7 +257,8 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;}
|
||||||
-webkit-font-variant-ligatures: contextual;
|
-webkit-font-variant-ligatures: contextual;
|
||||||
font-variant-ligatures: contextual;
|
font-variant-ligatures: contextual;
|
||||||
}
|
}
|
||||||
.CodeMirror-wrap pre {
|
.CodeMirror-wrap pre.CodeMirror-line,
|
||||||
|
.CodeMirror-wrap pre.CodeMirror-line-like {
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: normal;
|
word-break: normal;
|
||||||
|
|
21
vendor/codemirror/lib/codemirror.js
vendored
21
vendor/codemirror/lib/codemirror.js
vendored
|
@ -2284,7 +2284,7 @@
|
||||||
function paddingVert(display) {return display.mover.offsetHeight - display.lineSpace.offsetHeight}
|
function paddingVert(display) {return display.mover.offsetHeight - display.lineSpace.offsetHeight}
|
||||||
function paddingH(display) {
|
function paddingH(display) {
|
||||||
if (display.cachedPaddingH) { return display.cachedPaddingH }
|
if (display.cachedPaddingH) { return display.cachedPaddingH }
|
||||||
var e = removeChildrenAndAdd(display.measure, elt("pre", "x"));
|
var e = removeChildrenAndAdd(display.measure, elt("pre", "x", "CodeMirror-line-like"));
|
||||||
var style = window.getComputedStyle ? window.getComputedStyle(e) : e.currentStyle;
|
var style = window.getComputedStyle ? window.getComputedStyle(e) : e.currentStyle;
|
||||||
var data = {left: parseInt(style.paddingLeft), right: parseInt(style.paddingRight)};
|
var data = {left: parseInt(style.paddingLeft), right: parseInt(style.paddingRight)};
|
||||||
if (!isNaN(data.left) && !isNaN(data.right)) { display.cachedPaddingH = data; }
|
if (!isNaN(data.left) && !isNaN(data.right)) { display.cachedPaddingH = data; }
|
||||||
|
@ -2678,7 +2678,7 @@
|
||||||
function PosWithInfo(line, ch, sticky, outside, xRel) {
|
function PosWithInfo(line, ch, sticky, outside, xRel) {
|
||||||
var pos = Pos(line, ch, sticky);
|
var pos = Pos(line, ch, sticky);
|
||||||
pos.xRel = xRel;
|
pos.xRel = xRel;
|
||||||
if (outside) { pos.outside = true; }
|
if (outside) { pos.outside = outside; }
|
||||||
return pos
|
return pos
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2687,16 +2687,16 @@
|
||||||
function coordsChar(cm, x, y) {
|
function coordsChar(cm, x, y) {
|
||||||
var doc = cm.doc;
|
var doc = cm.doc;
|
||||||
y += cm.display.viewOffset;
|
y += cm.display.viewOffset;
|
||||||
if (y < 0) { return PosWithInfo(doc.first, 0, null, true, -1) }
|
if (y < 0) { return PosWithInfo(doc.first, 0, null, -1, -1) }
|
||||||
var lineN = lineAtHeight(doc, y), last = doc.first + doc.size - 1;
|
var lineN = lineAtHeight(doc, y), last = doc.first + doc.size - 1;
|
||||||
if (lineN > last)
|
if (lineN > last)
|
||||||
{ return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, null, true, 1) }
|
{ return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, null, 1, 1) }
|
||||||
if (x < 0) { x = 0; }
|
if (x < 0) { x = 0; }
|
||||||
|
|
||||||
var lineObj = getLine(doc, lineN);
|
var lineObj = getLine(doc, lineN);
|
||||||
for (;;) {
|
for (;;) {
|
||||||
var found = coordsCharInner(cm, lineObj, lineN, x, y);
|
var found = coordsCharInner(cm, lineObj, lineN, x, y);
|
||||||
var collapsed = collapsedSpanAround(lineObj, found.ch + (found.xRel > 0 ? 1 : 0));
|
var collapsed = collapsedSpanAround(lineObj, found.ch + (found.xRel > 0 || found.outside > 0 ? 1 : 0));
|
||||||
if (!collapsed) { return found }
|
if (!collapsed) { return found }
|
||||||
var rangeEnd = collapsed.find(1);
|
var rangeEnd = collapsed.find(1);
|
||||||
if (rangeEnd.line == lineN) { return rangeEnd }
|
if (rangeEnd.line == lineN) { return rangeEnd }
|
||||||
|
@ -2784,7 +2784,7 @@
|
||||||
// base X position
|
// base X position
|
||||||
var coords = cursorCoords(cm, Pos(lineNo$$1, ch, sticky), "line", lineObj, preparedMeasure);
|
var coords = cursorCoords(cm, Pos(lineNo$$1, ch, sticky), "line", lineObj, preparedMeasure);
|
||||||
baseX = coords.left;
|
baseX = coords.left;
|
||||||
outside = y < coords.top || y >= coords.bottom;
|
outside = y < coords.top ? -1 : y >= coords.bottom ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
ch = skipExtendingChars(lineObj.text, ch, 1);
|
ch = skipExtendingChars(lineObj.text, ch, 1);
|
||||||
|
@ -2853,7 +2853,7 @@
|
||||||
function textHeight(display) {
|
function textHeight(display) {
|
||||||
if (display.cachedTextHeight != null) { return display.cachedTextHeight }
|
if (display.cachedTextHeight != null) { return display.cachedTextHeight }
|
||||||
if (measureText == null) {
|
if (measureText == null) {
|
||||||
measureText = elt("pre");
|
measureText = elt("pre", null, "CodeMirror-line-like");
|
||||||
// Measure a bunch of lines, for browsers that compute
|
// Measure a bunch of lines, for browsers that compute
|
||||||
// fractional heights.
|
// fractional heights.
|
||||||
for (var i = 0; i < 49; ++i) {
|
for (var i = 0; i < 49; ++i) {
|
||||||
|
@ -2873,7 +2873,7 @@
|
||||||
function charWidth(display) {
|
function charWidth(display) {
|
||||||
if (display.cachedCharWidth != null) { return display.cachedCharWidth }
|
if (display.cachedCharWidth != null) { return display.cachedCharWidth }
|
||||||
var anchor = elt("span", "xxxxxxxxxx");
|
var anchor = elt("span", "xxxxxxxxxx");
|
||||||
var pre = elt("pre", [anchor]);
|
var pre = elt("pre", [anchor], "CodeMirror-line-like");
|
||||||
removeChildrenAndAdd(display.measure, pre);
|
removeChildrenAndAdd(display.measure, pre);
|
||||||
var rect = anchor.getBoundingClientRect(), width = (rect.right - rect.left) / 10;
|
var rect = anchor.getBoundingClientRect(), width = (rect.right - rect.left) / 10;
|
||||||
if (width > 2) { display.cachedCharWidth = width; }
|
if (width > 2) { display.cachedCharWidth = width; }
|
||||||
|
@ -5403,6 +5403,9 @@
|
||||||
if (doc.cm) { makeChangeSingleDocInEditor(doc.cm, change, spans); }
|
if (doc.cm) { makeChangeSingleDocInEditor(doc.cm, change, spans); }
|
||||||
else { updateDoc(doc, change, spans); }
|
else { updateDoc(doc, change, spans); }
|
||||||
setSelectionNoUndo(doc, selAfter, sel_dontScroll);
|
setSelectionNoUndo(doc, selAfter, sel_dontScroll);
|
||||||
|
|
||||||
|
if (doc.cantEdit && skipAtomic(doc, Pos(doc.firstLine(), 0)))
|
||||||
|
{ doc.cantEdit = false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle the interaction of a change to a document with the editor
|
// Handle the interaction of a change to a document with the editor
|
||||||
|
@ -9755,7 +9758,7 @@
|
||||||
|
|
||||||
addLegacyProps(CodeMirror);
|
addLegacyProps(CodeMirror);
|
||||||
|
|
||||||
CodeMirror.version = "5.48.0";
|
CodeMirror.version = "5.48.4";
|
||||||
|
|
||||||
return CodeMirror;
|
return CodeMirror;
|
||||||
|
|
||||||
|
|
18
vendor/codemirror/mode/javascript/javascript.js
vendored
18
vendor/codemirror/mode/javascript/javascript.js
vendored
|
@ -67,7 +67,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
|
||||||
if (ch == '"' || ch == "'") {
|
if (ch == '"' || ch == "'") {
|
||||||
state.tokenize = tokenString(ch);
|
state.tokenize = tokenString(ch);
|
||||||
return state.tokenize(stream, state);
|
return state.tokenize(stream, state);
|
||||||
} else if (ch == "." && stream.match(/^\d+(?:[eE][+\-]?\d+)?/)) {
|
} else if (ch == "." && stream.match(/^\d[\d_]*(?:[eE][+\-]?[\d_]+)?/)) {
|
||||||
return ret("number", "number");
|
return ret("number", "number");
|
||||||
} else if (ch == "." && stream.match("..")) {
|
} else if (ch == "." && stream.match("..")) {
|
||||||
return ret("spread", "meta");
|
return ret("spread", "meta");
|
||||||
|
@ -75,10 +75,10 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
|
||||||
return ret(ch);
|
return ret(ch);
|
||||||
} else if (ch == "=" && stream.eat(">")) {
|
} else if (ch == "=" && stream.eat(">")) {
|
||||||
return ret("=>", "operator");
|
return ret("=>", "operator");
|
||||||
} else if (ch == "0" && stream.match(/^(?:x[\da-f]+|o[0-7]+|b[01]+)n?/i)) {
|
} else if (ch == "0" && stream.match(/^(?:x[\dA-Fa-f_]+|o[0-7_]+|b[01_]+)n?/)) {
|
||||||
return ret("number", "number");
|
return ret("number", "number");
|
||||||
} else if (/\d/.test(ch)) {
|
} else if (/\d/.test(ch)) {
|
||||||
stream.match(/^\d*(?:n|(?:\.\d*)?(?:[eE][+\-]?\d+)?)?/);
|
stream.match(/^[\d_]*(?:n|(?:\.[\d_]*)?(?:[eE][+\-]?[\d_]+)?)?/);
|
||||||
return ret("number", "number");
|
return ret("number", "number");
|
||||||
} else if (ch == "/") {
|
} else if (ch == "/") {
|
||||||
if (stream.eat("*")) {
|
if (stream.eat("*")) {
|
||||||
|
@ -195,8 +195,12 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
|
||||||
++depth;
|
++depth;
|
||||||
} else if (wordRE.test(ch)) {
|
} else if (wordRE.test(ch)) {
|
||||||
sawSomething = true;
|
sawSomething = true;
|
||||||
} else if (/["'\/]/.test(ch)) {
|
} else if (/["'\/`]/.test(ch)) {
|
||||||
return;
|
for (;; --pos) {
|
||||||
|
if (pos == 0) return
|
||||||
|
var next = stream.string.charAt(pos - 1)
|
||||||
|
if (next == ch && stream.string.charAt(pos - 2) != "\\") { pos--; break }
|
||||||
|
}
|
||||||
} else if (sawSomething && !depth) {
|
} else if (sawSomething && !depth) {
|
||||||
++pos;
|
++pos;
|
||||||
break;
|
break;
|
||||||
|
@ -525,7 +529,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
|
||||||
cx.marked = "keyword"
|
cx.marked = "keyword"
|
||||||
return cont(objprop)
|
return cont(objprop)
|
||||||
} else if (type == "[") {
|
} else if (type == "[") {
|
||||||
return cont(expression, maybetypeOrIn, expect("]"), afterprop);
|
return cont(expression, maybetype, expect("]"), afterprop);
|
||||||
} else if (type == "spread") {
|
} else if (type == "spread") {
|
||||||
return cont(expressionNoComma, afterprop);
|
return cont(expressionNoComma, afterprop);
|
||||||
} else if (value == "*") {
|
} else if (value == "*") {
|
||||||
|
@ -621,7 +625,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
|
||||||
} else if (type == ":") {
|
} else if (type == ":") {
|
||||||
return cont(typeexpr)
|
return cont(typeexpr)
|
||||||
} else if (type == "[") {
|
} else if (type == "[") {
|
||||||
return cont(expect("variable"), maybetype, expect("]"), typeprop)
|
return cont(expect("variable"), maybetypeOrIn, expect("]"), typeprop)
|
||||||
} else if (type == "(") {
|
} else if (type == "(") {
|
||||||
return pass(functiondecl, typeprop)
|
return pass(functiondecl, typeprop)
|
||||||
}
|
}
|
||||||
|
|
4
vendor/codemirror/theme/yonce.css
vendored
4
vendor/codemirror/theme/yonce.css
vendored
|
@ -24,8 +24,8 @@
|
||||||
.cm-s-yonce .CodeMirror-activeline .CodeMirror-linenumber.CodeMirror-gutter-elt { background: #1C1C1C; color: #fc4384; }
|
.cm-s-yonce .CodeMirror-activeline .CodeMirror-linenumber.CodeMirror-gutter-elt { background: #1C1C1C; color: #fc4384; }
|
||||||
.cm-s-yonce .CodeMirror-linenumber { color: #777; }
|
.cm-s-yonce .CodeMirror-linenumber { color: #777; }
|
||||||
.cm-s-yonce .CodeMirror-cursor { border-left: 2px solid #FC4384; }
|
.cm-s-yonce .CodeMirror-cursor { border-left: 2px solid #FC4384; }
|
||||||
.cm-s-yonce .cm-searching { background: rgb(243, 155, 53, .3) !important; outline: 1px solid #F39B35; }
|
.cm-s-yonce .cm-searching { background: rgba(243, 155, 53, .3) !important; outline: 1px solid #F39B35; }
|
||||||
.cm-s-yonce .cm-searching.CodeMirror-selectedtext { background: rgb(243, 155, 53, .7) !important; color: white; }
|
.cm-s-yonce .cm-searching.CodeMirror-selectedtext { background: rgba(243, 155, 53, .7) !important; color: white; }
|
||||||
|
|
||||||
.cm-s-yonce .cm-keyword { color: #00A7AA; } /**/
|
.cm-s-yonce .cm-keyword { color: #00A7AA; } /**/
|
||||||
.cm-s-yonce .cm-atom { color: #F39B35; }
|
.cm-s-yonce .cm-atom { color: #F39B35; }
|
||||||
|
|
22
vendor/db-to-cloud/LICENSE
vendored
Normal file
22
vendor/db-to-cloud/LICENSE
vendored
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2019 eight
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
9
vendor/db-to-cloud/README.md
vendored
Normal file
9
vendor/db-to-cloud/README.md
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
## db-to-cloud v0.4.5
|
||||||
|
|
||||||
|
Installed via npm - source code:
|
||||||
|
|
||||||
|
https://github.com/eight04/db-to-cloud/tree/v0.4.5
|
||||||
|
|
||||||
|
Bundled code:
|
||||||
|
|
||||||
|
https://unpkg.com/db-to-cloud@0.4.5/dist/db-to-cloud.min.js
|
2
vendor/db-to-cloud/db-to-cloud.min.js
vendored
Normal file
2
vendor/db-to-cloud/db-to-cloud.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
vendor/dropbox/README.md
vendored
4
vendor/dropbox/README.md
vendored
|
@ -1,8 +1,8 @@
|
||||||
## Dropbox SDK v4.0.28
|
## Dropbox SDK v4.0.30
|
||||||
|
|
||||||
Dropbox SDK JS installed via npm - source repo:
|
Dropbox SDK JS installed via npm - source repo:
|
||||||
|
|
||||||
https://github.com/dropbox/dropbox-sdk-js/tree/v4.0.28
|
https://github.com/dropbox/dropbox-sdk-js/tree/v4.0.30
|
||||||
|
|
||||||
The source repo **does not** include the `dist` folder with the generated `dropbox-sdk.js`
|
The source repo **does not** include the `dist` folder with the generated `dropbox-sdk.js`
|
||||||
distribution file. It can only be obtained from the npm `node_modules` folder after installing
|
distribution file. It can only be obtained from the npm `node_modules` folder after installing
|
||||||
|
|
86
vendor/dropbox/dropbox-sdk.js
vendored
86
vendor/dropbox/dropbox-sdk.js
vendored
|
@ -236,6 +236,17 @@ routes.filePropertiesTemplatesUpdateForUser = function (arg) {
|
||||||
return this.request('file_properties/templates/update_for_user', arg, 'user', 'api', 'rpc');
|
return this.request('file_properties/templates/update_for_user', arg, 'user', 'api', 'rpc');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the total number of file requests owned by this user. Includes both
|
||||||
|
* open and closed file requests.
|
||||||
|
* @function Dropbox#fileRequestsCount
|
||||||
|
* @arg {void} arg - The request parameters.
|
||||||
|
* @returns {Promise.<FileRequestsCountFileRequestsResult, Error.<FileRequestsCountFileRequestsError>>}
|
||||||
|
*/
|
||||||
|
routes.fileRequestsCount = function (arg) {
|
||||||
|
return this.request('file_requests/count', arg, 'user', 'api', 'rpc');
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a file request for this user.
|
* Creates a file request for this user.
|
||||||
* @function Dropbox#fileRequestsCreate
|
* @function Dropbox#fileRequestsCreate
|
||||||
|
@ -246,6 +257,26 @@ routes.fileRequestsCreate = function (arg) {
|
||||||
return this.request('file_requests/create', arg, 'user', 'api', 'rpc');
|
return this.request('file_requests/create', arg, 'user', 'api', 'rpc');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a batch of closed file requests.
|
||||||
|
* @function Dropbox#fileRequestsDelete
|
||||||
|
* @arg {FileRequestsDeleteFileRequestArgs} arg - The request parameters.
|
||||||
|
* @returns {Promise.<FileRequestsDeleteFileRequestsResult, Error.<FileRequestsDeleteFileRequestError>>}
|
||||||
|
*/
|
||||||
|
routes.fileRequestsDelete = function (arg) {
|
||||||
|
return this.request('file_requests/delete', arg, 'user', 'api', 'rpc');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete all closed file requests owned by this user.
|
||||||
|
* @function Dropbox#fileRequestsDeleteAllClosed
|
||||||
|
* @arg {void} arg - The request parameters.
|
||||||
|
* @returns {Promise.<FileRequestsDeleteAllClosedFileRequestsResult, Error.<FileRequestsDeleteAllClosedFileRequestsError>>}
|
||||||
|
*/
|
||||||
|
routes.fileRequestsDeleteAllClosed = function (arg) {
|
||||||
|
return this.request('file_requests/delete_all_closed', arg, 'user', 'api', 'rpc');
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the specified file request.
|
* Returns the specified file request.
|
||||||
* @function Dropbox#fileRequestsGet
|
* @function Dropbox#fileRequestsGet
|
||||||
|
@ -256,6 +287,18 @@ routes.fileRequestsGet = function (arg) {
|
||||||
return this.request('file_requests/get', arg, 'user', 'api', 'rpc');
|
return this.request('file_requests/get', arg, 'user', 'api', 'rpc');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of file requests owned by this user. For apps with the app
|
||||||
|
* folder permission, this will only return file requests with destinations in
|
||||||
|
* the app folder.
|
||||||
|
* @function Dropbox#fileRequestsListV2
|
||||||
|
* @arg {FileRequestsListFileRequestsArg} arg - The request parameters.
|
||||||
|
* @returns {Promise.<FileRequestsListFileRequestsV2Result, Error.<FileRequestsListFileRequestsError>>}
|
||||||
|
*/
|
||||||
|
routes.fileRequestsListV2 = function (arg) {
|
||||||
|
return this.request('file_requests/list_v2', arg, 'user', 'api', 'rpc');
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a list of file requests owned by this user. For apps with the app
|
* Returns a list of file requests owned by this user. For apps with the app
|
||||||
* folder permission, this will only return file requests with destinations in
|
* folder permission, this will only return file requests with destinations in
|
||||||
|
@ -268,6 +311,18 @@ routes.fileRequestsList = function (arg) {
|
||||||
return this.request('file_requests/list', arg, 'user', 'api', 'rpc');
|
return this.request('file_requests/list', arg, 'user', 'api', 'rpc');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Once a cursor has been retrieved from list_v2, use this to paginate through
|
||||||
|
* all file requests. The cursor must come from a previous call to list_v2 or
|
||||||
|
* list/continue.
|
||||||
|
* @function Dropbox#fileRequestsListContinue
|
||||||
|
* @arg {FileRequestsListFileRequestsContinueArg} arg - The request parameters.
|
||||||
|
* @returns {Promise.<FileRequestsListFileRequestsV2Result, Error.<FileRequestsListFileRequestsContinueError>>}
|
||||||
|
*/
|
||||||
|
routes.fileRequestsListContinue = function (arg) {
|
||||||
|
return this.request('file_requests/list/continue', arg, 'user', 'api', 'rpc');
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update a file request.
|
* Update a file request.
|
||||||
* @function Dropbox#fileRequestsUpdate
|
* @function Dropbox#fileRequestsUpdate
|
||||||
|
@ -331,7 +386,7 @@ routes.filesCopy = function (arg) {
|
||||||
/**
|
/**
|
||||||
* Copy multiple files or folders to different locations at once in the user's
|
* Copy multiple files or folders to different locations at once in the user's
|
||||||
* Dropbox. This route will replace copy_batch. The main difference is this
|
* Dropbox. This route will replace copy_batch. The main difference is this
|
||||||
* route will return stutus for each entry, while copy_batch raises failure if
|
* route will return status for each entry, while copy_batch raises failure if
|
||||||
* any entry fails. This route will either finish synchronously, or return a job
|
* any entry fails. This route will either finish synchronously, or return a job
|
||||||
* ID and do the async copy job in background. Please use copy_batch/check_v2 to
|
* ID and do the async copy job in background. Please use copy_batch/check_v2 to
|
||||||
* check the job status.
|
* check the job status.
|
||||||
|
@ -526,6 +581,18 @@ routes.filesDownloadZip = function (arg) {
|
||||||
return this.request('files/download_zip', arg, 'user', 'content', 'download');
|
return this.request('files/download_zip', arg, 'user', 'content', 'download');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export a file from a user's Dropbox. This route only supports exporting files
|
||||||
|
* that cannot be downloaded directly and whose ExportResult.file_metadata has
|
||||||
|
* ExportInfo.export_as populated.
|
||||||
|
* @function Dropbox#filesExport
|
||||||
|
* @arg {FilesExportArg} arg - The request parameters.
|
||||||
|
* @returns {Promise.<FilesExportResult, Error.<FilesExportError>>}
|
||||||
|
*/
|
||||||
|
routes.filesExport = function (arg) {
|
||||||
|
return this.request('files/export', arg, 'user', 'content', 'download');
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the metadata for a file or folder. Note: Metadata for the root folder
|
* Returns the metadata for a file or folder. Note: Metadata for the root folder
|
||||||
* is unsupported.
|
* is unsupported.
|
||||||
|
@ -539,10 +606,11 @@ routes.filesGetMetadata = function (arg) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a preview for a file. Currently, PDF previews are generated for files
|
* Get a preview for a file. Currently, PDF previews are generated for files
|
||||||
* with the following extensions: .ai, .doc, .docm, .docx, .eps, .odp, .odt,
|
* with the following extensions: .ai, .doc, .docm, .docx, .eps, .gdoc,
|
||||||
* .pps, .ppsm, .ppsx, .ppt, .pptm, .pptx, .rtf. HTML previews are generated for
|
* .gslides, .odp, .odt, .pps, .ppsm, .ppsx, .ppt, .pptm, .pptx, .rtf. HTML
|
||||||
* files with the following extensions: .csv, .ods, .xls, .xlsm, .xlsx. Other
|
* previews are generated for files with the following extensions: .csv, .ods,
|
||||||
* formats will return an unsupported extension error.
|
* .xls, .xlsm, .gsheet, .xlsx. Other formats will return an unsupported
|
||||||
|
* extension error.
|
||||||
* @function Dropbox#filesGetPreview
|
* @function Dropbox#filesGetPreview
|
||||||
* @arg {FilesPreviewArg} arg - The request parameters.
|
* @arg {FilesPreviewArg} arg - The request parameters.
|
||||||
* @returns {Promise.<FilesFileMetadata, Error.<FilesPreviewError>>}
|
* @returns {Promise.<FilesFileMetadata, Error.<FilesPreviewError>>}
|
||||||
|
@ -553,8 +621,8 @@ routes.filesGetPreview = function (arg) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a temporary link to stream content of a file. This link will expire in
|
* Get a temporary link to stream content of a file. This link will expire in
|
||||||
* four hours and afterwards you will get 410 Gone. So this URL should not be
|
* four hours and afterwards you will get 410 Gone. This URL should not be used
|
||||||
* used to display content directly in the browser. Content-Type of the link is
|
* to display content directly in the browser. The Content-Type of the link is
|
||||||
* determined automatically by the file's mime type.
|
* determined automatically by the file's mime type.
|
||||||
* @function Dropbox#filesGetTemporaryLink
|
* @function Dropbox#filesGetTemporaryLink
|
||||||
* @arg {FilesGetTemporaryLinkArg} arg - The request parameters.
|
* @arg {FilesGetTemporaryLinkArg} arg - The request parameters.
|
||||||
|
@ -736,8 +804,8 @@ routes.filesMove = function (arg) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move multiple files or folders to different locations at once in the user's
|
* Move multiple files or folders to different locations at once in the user's
|
||||||
* Dropbox. This route will replace move_batch_v2. The main difference is this
|
* Dropbox. This route will replace move_batch. The main difference is this
|
||||||
* route will return stutus for each entry, while move_batch raises failure if
|
* route will return status for each entry, while move_batch raises failure if
|
||||||
* any entry fails. This route will either finish synchronously, or return a job
|
* any entry fails. This route will either finish synchronously, or return a job
|
||||||
* ID and do the async move job in background. Please use move_batch/check_v2 to
|
* ID and do the async move job in background. Please use move_batch/check_v2 to
|
||||||
* check the job status.
|
* check the job status.
|
||||||
|
|
21
vendor/uuid/LICENSE
vendored
Normal file
21
vendor/uuid/LICENSE
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2010-2016 Robert Kieffer and other contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
9
vendor/uuid/README.md
vendored
Normal file
9
vendor/uuid/README.md
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
## uuid v3.3.3
|
||||||
|
|
||||||
|
Installed via npm - source code:
|
||||||
|
|
||||||
|
https://github.com/kelektiv/node-uuid/tree/v3.3.3
|
||||||
|
|
||||||
|
Bundled code:
|
||||||
|
|
||||||
|
https://bundle.run/uuid@3.3.3/v4.js
|
1
vendor/uuid/uuid.min.js
vendored
Normal file
1
vendor/uuid/uuid.min.js
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
!function(n){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=n();else if("function"==typeof define&&define.amd)define([],n);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).uuid=n()}}(function(){return function(){return function n(e,r,t){function o(f,u){if(!r[f]){if(!e[f]){var d="function"==typeof require&&require;if(!u&&d)return d(f,!0);if(i)return i(f,!0);var a=new Error("Cannot find module '"+f+"'");throw a.code="MODULE_NOT_FOUND",a}var p=r[f]={exports:{}};e[f][0].call(p.exports,function(n){return o(e[f][1][n]||n)},p,p.exports,n,e,r,t)}return r[f].exports}for(var i="function"==typeof require&&require,f=0;f<t.length;f++)o(t[f]);return o}}()({1:[function(n,e,r){for(var t=[],o=0;o<256;++o)t[o]=(o+256).toString(16).substr(1);e.exports=function(n,e){var r=e||0,o=t;return[o[n[r++]],o[n[r++]],o[n[r++]],o[n[r++]],"-",o[n[r++]],o[n[r++]],"-",o[n[r++]],o[n[r++]],"-",o[n[r++]],o[n[r++]],"-",o[n[r++]],o[n[r++]],o[n[r++]],o[n[r++]],o[n[r++]],o[n[r++]]].join("")}},{}],2:[function(n,e,r){var t="undefined"!=typeof crypto&&crypto.getRandomValues&&crypto.getRandomValues.bind(crypto)||"undefined"!=typeof msCrypto&&"function"==typeof window.msCrypto.getRandomValues&&msCrypto.getRandomValues.bind(msCrypto);if(t){var o=new Uint8Array(16);e.exports=function(){return t(o),o}}else{var i=new Array(16);e.exports=function(){for(var n,e=0;e<16;e++)0==(3&e)&&(n=4294967296*Math.random()),i[e]=n>>>((3&e)<<3)&255;return i}}},{}],3:[function(n,e,r){var t=n("./lib/rng"),o=n("./lib/bytesToUuid");e.exports=function(n,e,r){var i=e&&r||0;"string"==typeof n&&(e="binary"===n?new Array(16):null,n=null);var f=(n=n||{}).random||(n.rng||t)();if(f[6]=15&f[6]|64,f[8]=63&f[8]|128,e)for(var u=0;u<16;++u)e[i+u]=f[u];return e||o(f)}},{"./lib/bytesToUuid":1,"./lib/rng":2}]},{},[3])(3)});
|
Loading…
Reference in New Issue
Block a user