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:
eight 2019-11-06 03:30:45 +08:00 committed by narcolepticinsomniac
parent c0fd71dda6
commit f9db43a2e9
29 changed files with 992 additions and 81 deletions

View File

@ -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"

View File

@ -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

View File

@ -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
View 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
View 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;
});
});
}
})();

View File

@ -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);
}

View File

@ -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) {

View File

@ -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"
} }

View File

@ -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">

View File

@ -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;
}

View File

@ -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;

View File

@ -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",

View File

@ -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'
] ]
}; };

View File

@ -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);

View File

@ -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.

View File

@ -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) {

View File

@ -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.

View File

@ -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;

View File

@ -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;

View File

@ -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)
} }

View File

@ -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
View 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
View 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

File diff suppressed because one or more lines are too long

View File

@ -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

View File

@ -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
View 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
View 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
View 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)});