767 lines
27 KiB
JavaScript
767 lines
27 KiB
JavaScript
// ==UserScript==
|
|
// @id iitc-plugin-sync@xelio
|
|
// @name IITC plugin: Sync
|
|
// @category Misc
|
|
// @version 0.2.3.@@DATETIMEVERSION@@
|
|
// @namespace https://github.com/jonatkins/ingress-intel-total-conversion
|
|
// @updateURL @@UPDATEURL@@
|
|
// @downloadURL @@DOWNLOADURL@@
|
|
// @description [@@BUILDNAME@@-@@BUILDDATE@@] Sync data between clients via Google Realtime API. Only syncs data from specific plugins (currently: Keys, Bookmarks). Sign in via the 'Sync' link.
|
|
// @include https://*.ingress.com/intel*
|
|
// @include http://*.ingress.com/intel*
|
|
// @match https://*.ingress.com/intel*
|
|
// @match http://*.ingress.com/intel*
|
|
// @include https://*.ingress.com/mission/*
|
|
// @include http://*.ingress.com/mission/*
|
|
// @match https://*.ingress.com/mission/*
|
|
// @match http://*.ingress.com/mission/*
|
|
// @grant none
|
|
// ==/UserScript==
|
|
|
|
@@PLUGINSTART@@
|
|
|
|
// PLUGIN START ////////////////////////////////////////////////////////
|
|
|
|
////////////////////////////////////////////////////////////////////////
|
|
// Notice for developers:
|
|
//
|
|
// You should treat the data stored on Google Realtime API as volatile.
|
|
// Because if there are change in Google API client ID, Google will
|
|
// treat it as another application and could not access the data created
|
|
// by old client ID. Store any important data locally and only use this
|
|
// plugin as syncing function.
|
|
//
|
|
// Google Realtime API reference
|
|
// https://developers.google.com/drive/realtime/application
|
|
////////////////////////////////////////////////////////////////////////
|
|
|
|
// use own namespace for plugin
|
|
window.plugin.sync = function() {};
|
|
|
|
window.plugin.sync.KEY_UUID = {key: 'plugin-sync-data-uuid', field: 'uuid'};
|
|
|
|
// Each client has an unique UUID, to identify remote data is updated by other clients or not
|
|
window.plugin.sync.uuid = null;
|
|
|
|
window.plugin.sync.dialogHTML = null;
|
|
window.plugin.sync.authorizer = null;
|
|
|
|
// Store registered CollaborativeMap
|
|
window.plugin.sync.registeredPluginsFields = null;
|
|
window.plugin.sync.logger = null;
|
|
|
|
// Other plugin call this function to push update to Google Realtime API
|
|
// example:
|
|
// plugin.sync.updateMap('keys', 'keysdata', ['guid1', 'guid2', 'guid3'])
|
|
// Which will push plugin.keys.keysdata['guid1'] etc. to Google Realtime API
|
|
window.plugin.sync.updateMap = function(pluginName, fieldName, keyArray) {
|
|
var registeredMap = plugin.sync.registeredPluginsFields.get(pluginName, fieldName);
|
|
if(!registeredMap) return false;
|
|
registeredMap.updateMap(keyArray);
|
|
}
|
|
|
|
// Other plugin call this to register a field as CollaborativeMap to sync with Google Realtime API
|
|
// example: plugin.sync.registerMapForSync('keys', 'keysdata', plugin.keys.updateCallback, plugin.keys.initializedCallback)
|
|
// which register plugin.keys.keysdata
|
|
//
|
|
// updateCallback function format: function(pluginName, fieldName, eventObject, fullUpdated)
|
|
// updateCallback will be fired when local or remote pushed update to Google Realtime API
|
|
// fullUpdated is true when remote update occur during local client offline, all data is replaced by remote data
|
|
// eventObject is a ValueChangedEvent, is null if fullUpdated is true
|
|
//
|
|
// detail of ValueChangedEvent refer to following url
|
|
// https://developers.google.com/drive/realtime/reference/gapi.drive.realtime.ValueChangedEvent
|
|
//
|
|
// initializedCallback function format: function(pluginName, fieldName)
|
|
// initializedCallback will be fired when the CollaborativeMap finished initialize and good to use
|
|
window.plugin.sync.registerMapForSync = function(pluginName, fieldName, callback, initializedCallback) {
|
|
var options, registeredMap;
|
|
options = {'pluginName': pluginName,
|
|
'fieldName': fieldName,
|
|
'callback': callback,
|
|
'initializedCallback': initializedCallback,
|
|
'authorizer': plugin.sync.authorizer,
|
|
'uuid': plugin.sync.uuid};
|
|
registeredMap = new plugin.sync.RegisteredMap(options);
|
|
plugin.sync.registeredPluginsFields.add(registeredMap);
|
|
}
|
|
|
|
|
|
|
|
//// RegisteredMap
|
|
// Create a file named pluginName[fieldName] in folder specified by authorizer
|
|
// The file use as realtime document with CollaborativeMap to store the data and a
|
|
// CollaborativeString to store uuid of last update client
|
|
// callback will called when any local/remote update happen
|
|
// initializedCallback will called when RegisteredMap initialized and good to use.
|
|
window.plugin.sync.RegisteredMap = function(options) {
|
|
this.pluginName = options['pluginName'];
|
|
this.fieldName = options['fieldName'];
|
|
this.callback = options['callback'];
|
|
this.initializedCallback = options['initializedCallback'];
|
|
this.authorizer = options['authorizer'];
|
|
this.uuid = options['uuid'];
|
|
|
|
this.fileId = null;
|
|
this.doc = null;
|
|
this.model = null;
|
|
this.map = null;
|
|
this.lastUpdateUUID = null;
|
|
this.fileSearcher = null;
|
|
|
|
this.forceFileSearch = false;
|
|
this.initializing = false;
|
|
this.initialized = false;
|
|
this.failed = false;
|
|
|
|
this.updateListener = this.updateListener.bind(this);
|
|
this.initialize = this.initialize.bind(this);
|
|
this.loadRealtimeDocument = this.loadRealtimeDocument.bind(this);
|
|
}
|
|
|
|
window.plugin.sync.RegisteredMap.prototype.updateMap = function(keyArray) {
|
|
var _this = this;
|
|
// Use compound operation to ensure update pushed as a batch
|
|
this.model.beginCompoundOperation();
|
|
try {
|
|
// Remove before set text to ensure full text change
|
|
if (this.lastUpdateUUID.length > 0)
|
|
this.lastUpdateUUID.removeRange(0, this.lastUpdateUUID.length);
|
|
this.lastUpdateUUID.setText(this.uuid);
|
|
|
|
$.each(keyArray, function(ind, key) {
|
|
var value = window.plugin[_this.pluginName][_this.fieldName][key];
|
|
if(typeof(value) !== 'undefined') {
|
|
_this.map.set(key, value);
|
|
} else {
|
|
_this.map.delete(key);
|
|
}
|
|
});
|
|
} finally {
|
|
// Ensure endCompoundOperation is always called (see bug #896)
|
|
this.model.endCompoundOperation();
|
|
}
|
|
}
|
|
|
|
window.plugin.sync.RegisteredMap.prototype.isUpdatedByOthers = function() {
|
|
var remoteUUID = this.lastUpdateUUID.toString();
|
|
return (remoteUUID !== '') && (remoteUUID !== this.uuid);
|
|
}
|
|
|
|
window.plugin.sync.RegisteredMap.prototype.getFileName = function() {
|
|
return this.pluginName + '[' + this.fieldName + ']'
|
|
}
|
|
|
|
window.plugin.sync.RegisteredMap.prototype.initFile = function(callback) {
|
|
var assignIdCallback, failedCallback, _this;
|
|
_this = this;
|
|
|
|
assignIdCallback = function(id) {
|
|
_this.forceFileSearch = false;
|
|
_this.fileId = id;
|
|
if(callback) callback();
|
|
};
|
|
|
|
failedCallback = function(resp) {
|
|
_this.initializing = false;
|
|
_this.failed = true;
|
|
plugin.sync.logger.log('Could not create file: ' + _this.getFileName() + '. If this problem persist, delete this file in IITC-SYNC-DATA-V2 and empty trash in your Google drive and try again.');
|
|
}
|
|
|
|
this.fileSearcher = new plugin.sync.FileSearcher({'fileName': this.getFileName(),
|
|
'description': 'IITC plugin data for ' + this.getFileName()});
|
|
this.fileSearcher.initialize(this.forceFileSearch, assignIdCallback, failedCallback);
|
|
}
|
|
|
|
window.plugin.sync.RegisteredMap.prototype.updateListener = function(e) {
|
|
if(!e.isLocal) {
|
|
if(!window.plugin[this.pluginName][this.fieldName]) {
|
|
window.plugin[this.pluginName][this.fieldName] = {};
|
|
}
|
|
if(typeof(e.newValue) !== 'undefined' && e.newValue !== null) {
|
|
window.plugin[this.pluginName][this.fieldName][e.property] = e.newValue;
|
|
} else {
|
|
delete window.plugin[this.pluginName][this.fieldName][e.property];
|
|
}
|
|
}
|
|
if(this.callback) this.callback(this.pluginName, this.fieldName, e);
|
|
}
|
|
|
|
window.plugin.sync.RegisteredMap.prototype.initialize = function(callback) {
|
|
this.initFile(this.loadRealtimeDocument);
|
|
}
|
|
|
|
window.plugin.sync.RegisteredMap.prototype.loadRealtimeDocument = function(callback) {
|
|
this.initializing = true;
|
|
var initRealtime, initializeModel, onFileLoaded, handleError, _this;
|
|
_this = this;
|
|
|
|
// this function called when the document is created first time
|
|
// and the CollaborativeMap is populated with data in plugin field
|
|
initializeModel = function(model) {
|
|
var empty = true;
|
|
var map = model.createMap();
|
|
var lastUpdateUUID = model.createString();
|
|
|
|
// Init the map values if this map is first created
|
|
$.each(window.plugin[_this.pluginName][_this.fieldName], function(key, val) {
|
|
map.set(key, val);
|
|
empty = false;
|
|
});
|
|
|
|
// Only set the update client if the map is not empty, avoid clearing data of other clients
|
|
lastUpdateUUID.setText(empty ? '' : _this.uuid);
|
|
|
|
model.getRoot().set('map', map);
|
|
model.getRoot().set('last-udpate-uuid', lastUpdateUUID);
|
|
plugin.sync.logger.log('Model initialized: ' + _this.pluginName + '[' + _this.fieldName + ']');
|
|
};
|
|
|
|
// this function called when the document is loaded
|
|
// update local data if the document is updated by other
|
|
// and add update listener to CollaborativeMap
|
|
onFileLoaded = function(doc) {
|
|
_this.doc = doc;
|
|
_this.model = doc.getModel();
|
|
_this.map = doc.getModel().getRoot().get('map');
|
|
_this.lastUpdateUUID = doc.getModel().getRoot().get('last-udpate-uuid');
|
|
_this.map.addEventListener(gapi.drive.realtime.EventType.VALUE_CHANGED, _this.updateListener);
|
|
|
|
// Replace local value if data is changed by others
|
|
if(_this.isUpdatedByOthers()) {
|
|
plugin.sync.logger.log('Updated by others, replacing content: ' + _this.pluginName + '[' + _this.fieldName + ']');
|
|
window.plugin[_this.pluginName][_this.fieldName] = {};
|
|
$.each(_this.map.keys(), function(ind, key) {
|
|
window.plugin[_this.pluginName][_this.fieldName][key] = _this.map.get(key);
|
|
});
|
|
if(_this.callback) _this.callback(_this.pluginName, _this.fieldName, null, true);
|
|
}
|
|
|
|
_this.initialized = true;
|
|
_this.initializing = false;
|
|
plugin.sync.logger.log('Data loaded: ' + _this.pluginName + '[' + _this.fieldName + ']');
|
|
if(callback) callback();
|
|
if(_this.initializedCallback) _this.initializedCallback(_this.pluginName, _this.fieldName);
|
|
};
|
|
|
|
// Stop the sync if any error occur and try to re-authorize
|
|
handleError = function(e) {
|
|
plugin.sync.logger.log('Realtime API Error: ' + e.type);
|
|
_this.stopSync();
|
|
if(e.type === gapi.drive.realtime.ErrorType.TOKEN_REFRESH_REQUIRED) {
|
|
_this.authorizer.authorize();
|
|
} else if(e.type === gapi.drive.realtime.ErrorType.NOT_FOUND) {
|
|
_this.forceFileSearch = true;
|
|
} else if(e.type === gapi.drive.realtime.ErrorType.CLIENT_ERROR) {
|
|
// Workaround: if Realtime API open a second document and the file do not exist,
|
|
// it will rasie 'CLIENT_ERROR' instead of 'NOT_FOUND'. So we do a force file search here.
|
|
_this.forceFileSearch = true;
|
|
} else {
|
|
alert('Plugin Sync error: ' + e.type + ', ' + e.message);
|
|
}
|
|
};
|
|
|
|
gapi.drive.realtime.load(_this.fileId, onFileLoaded, initializeModel, handleError);
|
|
}
|
|
|
|
window.plugin.sync.RegisteredMap.prototype.stopSync = function() {
|
|
if(this.map) this.map.removeEventListener(gapi.drive.realtime.EventType.VALUE_CHANGED, this.updateListener);
|
|
this.fileId = null;
|
|
this.doc = null;
|
|
this.model = null;
|
|
this.map = null;
|
|
this.lastUpdateUUID = null;
|
|
this.initializing = false;
|
|
this.initialized = false;
|
|
plugin.sync.registeredPluginsFields.addToWaitingInitialize(this.pluginName, this.fieldName);
|
|
}
|
|
//// end RegisteredMap
|
|
|
|
|
|
|
|
|
|
//// RegisteredPluginsFields
|
|
// Store RegisteredMap and handle initialization of RegisteredMap
|
|
window.plugin.sync.RegisteredPluginsFields = function(options) {
|
|
this.authorizer = options['authorizer'];
|
|
this.pluginsfields = {};
|
|
this.waitingInitialize = {};
|
|
|
|
this.anyFail = false;
|
|
|
|
this.initializeRegistered = this.initializeRegistered.bind(this);
|
|
this.cleanWaitingInitialize = this.cleanWaitingInitialize.bind(this);
|
|
this.initializeWorker = this.initializeWorker.bind(this);
|
|
|
|
this.authorizer.addAuthCallback(this.initializeRegistered);
|
|
}
|
|
|
|
window.plugin.sync.RegisteredPluginsFields.prototype.add = function(registeredMap) {
|
|
var pluginName, fieldName;
|
|
pluginName = registeredMap.pluginName;
|
|
fieldName = registeredMap.fieldName;
|
|
this.pluginsfields[pluginName] = this.pluginsfields[pluginName] || {};
|
|
|
|
if(this.pluginsfields[pluginName][fieldName]) return false;
|
|
|
|
this.pluginsfields[pluginName][fieldName] = registeredMap;
|
|
this.waitingInitialize[registeredMap.getFileName()] = registeredMap;
|
|
|
|
this.initializeWorker();
|
|
}
|
|
|
|
window.plugin.sync.RegisteredPluginsFields.prototype.addToWaitingInitialize = function(pluginName, fieldName) {
|
|
var registeredMap, _this;
|
|
_this = this;
|
|
|
|
registeredMap = this.get(pluginName, fieldName);
|
|
if(!registeredMap) return;
|
|
this.waitingInitialize[registeredMap.getFileName()] = registeredMap;
|
|
|
|
clearTimeout(this.timer);
|
|
this.timer = setTimeout(function() {_this.initializeWorker()}, 10000);
|
|
plugin.sync.logger.log('Retry in 10 sec.: ' + pluginName + '[' + fieldName + ']');
|
|
}
|
|
|
|
window.plugin.sync.RegisteredPluginsFields.prototype.get = function(pluginName, fieldName) {
|
|
if(!this.pluginsfields[pluginName]) return;
|
|
return this.pluginsfields[pluginName][fieldName];
|
|
}
|
|
|
|
window.plugin.sync.RegisteredPluginsFields.prototype.initializeRegistered = function() {
|
|
var _this = this;
|
|
if(this.authorizer.isAuthed()) {
|
|
$.each(this.waitingInitialize, function(key, map) {
|
|
if(!map.initializing && !map.initialized) {
|
|
map.initialize(_this.cleanWaitingInitialize);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
window.plugin.sync.RegisteredPluginsFields.prototype.cleanWaitingInitialize = function() {
|
|
var newWaitingInitialize, _this;
|
|
_this = this;
|
|
|
|
newWaitingInitialize = {};
|
|
$.each(this.waitingInitialize, function(key,map) {
|
|
if(map.failed) _this.anyFail = true;
|
|
if(map.initialized || map.failed) return true;
|
|
newWaitingInitialize[map.getFileName()] = map;
|
|
});
|
|
this.waitingInitialize = newWaitingInitialize;
|
|
}
|
|
|
|
window.plugin.sync.RegisteredPluginsFields.prototype.initializeWorker = function() {
|
|
var _this = this;
|
|
|
|
this.cleanWaitingInitialize();
|
|
plugin.sync.toggleDialogLink();
|
|
this.initializeRegistered();
|
|
|
|
clearTimeout(this.timer);
|
|
if(Object.keys(this.waitingInitialize).length > 0) {
|
|
this.timer = setTimeout(function() {_this.initializeWorker()}, 10000);
|
|
}
|
|
}
|
|
//// end RegisteredPluginsFields
|
|
|
|
|
|
|
|
|
|
//// FileSearcher
|
|
//
|
|
// assignIdCallback function format: function(id)
|
|
// allow you to assign the file/folder id elsewhere
|
|
//
|
|
// failedCallback function format: function()
|
|
// call when the file/folder couldn't create
|
|
window.plugin.sync.FileSearcher = function(options) {
|
|
// return object created previously
|
|
if(this.instances[options['fileName']]) return this.instances[options['fileName']];
|
|
|
|
this.fileName = options['fileName'];
|
|
this.description = options['description'];
|
|
this.isFolder = options['isFolder'];
|
|
|
|
this.force = false;
|
|
this.parent = null;
|
|
this.fileId = null;
|
|
this.retryCount = 0;
|
|
this.loadFileId();
|
|
|
|
this.instances[this.fileName] = this;
|
|
}
|
|
|
|
window.plugin.sync.FileSearcher.prototype.instances = {};
|
|
window.plugin.sync.FileSearcher.prototype.RETRY_LIMIT = 2;
|
|
window.plugin.sync.FileSearcher.prototype.MIMETYPE_FILE = 'application/vnd.google-apps.drive-sdk';
|
|
window.plugin.sync.FileSearcher.prototype.MIMETYPE_FOLDER = 'application/vnd.google-apps.folder';
|
|
window.plugin.sync.FileSearcher.prototype.parentName = 'IITC-SYNC-DATA-V2';
|
|
window.plugin.sync.FileSearcher.prototype.parentDescription = 'Store IITC sync data';
|
|
|
|
window.plugin.sync.FileSearcher.prototype.initialize = function(force, assignIdCallback, failedCallback) {
|
|
this.force = force;
|
|
// throw error if too many retry
|
|
if(this.retryCount >= this.RETRY_LIMIT) {
|
|
plugin.sync.logger.log('Too many file operation: ' + this.fileName);
|
|
failedCallback();
|
|
return;
|
|
}
|
|
if(this.force) this.retryCount++;
|
|
|
|
if(this.isFolder) {
|
|
this.initFile(assignIdCallback, failedCallback);
|
|
} else {
|
|
this.initParent(assignIdCallback, failedCallback);
|
|
}
|
|
}
|
|
|
|
window.plugin.sync.FileSearcher.prototype.initFile = function(assignIdCallback, failedCallback) {
|
|
// If not force search and have cached fileId, return the fileId
|
|
if(!this.force && this.fileId) {
|
|
assignIdCallback(this.fileId);
|
|
return;
|
|
}
|
|
|
|
var searchCallback, createCallback, handleFileId, handleFailed, _this;
|
|
_this = this;
|
|
|
|
handleFileId = function(id) {
|
|
_this.fileId = id;
|
|
_this.saveFileId();
|
|
assignIdCallback(id);
|
|
};
|
|
|
|
handleFailed = function(resp) {
|
|
_this.fileId = null;
|
|
_this.saveFileId();
|
|
plugin.sync.logger.log('File operation failed: ' + (resp.error || 'unknown error'));
|
|
failedCallback(resp);
|
|
}
|
|
|
|
createCallback = function(resp) {
|
|
if(resp.id) {
|
|
handleFileId(resp.id); // file created
|
|
} else {
|
|
handleFailed(resp) // could not create file
|
|
}
|
|
};
|
|
|
|
searchCallback = function(resp) {
|
|
if(resp.items && resp.items[0]) {
|
|
handleFileId(resp.items[0].id);// file found
|
|
} else if(!resp.error) {
|
|
_this.createFileOrFolder(createCallback); // file not found, create file
|
|
} else {
|
|
handleFailed(resp); // Error
|
|
}
|
|
};
|
|
|
|
this.searchFileOrFolder(searchCallback);
|
|
}
|
|
|
|
window.plugin.sync.FileSearcher.prototype.initParent = function(assignIdCallback, failedCallback) {
|
|
var parentAssignIdCallback, parentFailedCallback, _this;
|
|
_this = this;
|
|
|
|
parentAssignIdCallback = function(id) {
|
|
_this.initFile(assignIdCallback, failedCallback);
|
|
}
|
|
|
|
parentFailedCallback = function(resp) {
|
|
_this.fileId = null;
|
|
_this.saveFileId();
|
|
plugin.sync.logger.log('File operation failed: ' + (resp.error || 'unknown error'));
|
|
failedCallback(resp);
|
|
}
|
|
|
|
this.parent = new plugin.sync.FileSearcher({'fileName': this.parentName,
|
|
'description': this.parentDescription,
|
|
'isFolder': true});
|
|
this.parent.initialize(this.force, parentAssignIdCallback, parentFailedCallback);
|
|
}
|
|
|
|
window.plugin.sync.FileSearcher.prototype.createFileOrFolder = function(callback) {
|
|
var _this = this;
|
|
gapi.client.load('drive', 'v2', function() {
|
|
gapi.client.drive.files.insert(_this.getCreateOption()).execute(callback);
|
|
});
|
|
}
|
|
|
|
window.plugin.sync.FileSearcher.prototype.searchFileOrFolder = function(callback) {
|
|
var _this = this;
|
|
gapi.client.load('drive', 'v2', function() {
|
|
gapi.client.drive.files.list(_this.getSearchOption()).execute(callback);
|
|
});
|
|
}
|
|
|
|
window.plugin.sync.FileSearcher.prototype.getCreateOption = function() {
|
|
var resource = {
|
|
'title': this.fileName,
|
|
'description': this.description,
|
|
'mimeType': (this.isFolder ? this.MIMETYPE_FOLDER : this.MIMETYPE_FILE)
|
|
};
|
|
if(this.parent) $.extend(resource, {'parents': [{'id': this.parent.fileId}]});
|
|
|
|
return {'convert': 'false',
|
|
'ocr': 'false',
|
|
'resource': resource};
|
|
}
|
|
|
|
window.plugin.sync.FileSearcher.prototype.getSearchOption = function() {
|
|
var q = 'title = "' + this.fileName +'" and trashed = false';
|
|
if(this.parent) q += ' and "' + this.parent.fileId + '" in parents';
|
|
return {'q': q};
|
|
}
|
|
|
|
window.plugin.sync.FileSearcher.prototype.localStorageKey = function() {
|
|
return 'sync-file-' + this.fileName;
|
|
}
|
|
|
|
window.plugin.sync.FileSearcher.prototype.saveFileId = function() {
|
|
if(this.fileId) {
|
|
localStorage[this.localStorageKey()] = this.fileId;
|
|
} else {
|
|
localStorage.removeItem(this.localStorageKey());
|
|
}
|
|
}
|
|
|
|
window.plugin.sync.FileSearcher.prototype.loadFileId = function() {
|
|
var storedFileId = localStorage[this.localStorageKey()];
|
|
if(storedFileId) this.fileId = storedFileId;
|
|
}
|
|
//// end FileSearcher
|
|
|
|
|
|
|
|
|
|
//// Authorizer
|
|
// authorize user's google account
|
|
window.plugin.sync.Authorizer = function(options) {
|
|
this.authCallback = options['authCallback'];
|
|
this.authorizing = false;
|
|
this.authorized = false;
|
|
this.isAuthed = this.isAuthed.bind(this);
|
|
this.isAuthorizing = this.isAuthorizing.bind(this);
|
|
this.authorize = this.authorize.bind(this);
|
|
}
|
|
|
|
window.plugin.sync.Authorizer.prototype.CLIENT_ID = '893806110732.apps.googleusercontent.com';
|
|
window.plugin.sync.Authorizer.prototype.SCOPES = ['https://www.googleapis.com/auth/drive.file', 'https://www.googleapis.com/auth/drive.metadata.readonly'];
|
|
|
|
window.plugin.sync.Authorizer.prototype.isAuthed = function() {
|
|
return this.authorized;
|
|
}
|
|
|
|
window.plugin.sync.Authorizer.prototype.isAuthorizing = function() {
|
|
return this.authorizing;
|
|
}
|
|
window.plugin.sync.Authorizer.prototype.addAuthCallback = function(callback) {
|
|
if(typeof(this.authCallback) === 'function') this.authCallback = [this.authCallback];
|
|
this.authCallback.push(callback);
|
|
}
|
|
|
|
window.plugin.sync.Authorizer.prototype.authComplete = function() {
|
|
this.authorizing = false;
|
|
if(this.authCallback) {
|
|
if(typeof(this.authCallback) === 'function') this.authCallback();
|
|
if(this.authCallback instanceof Array && this.authCallback.length > 0) {
|
|
$.each(this.authCallback, function(ind, func) {
|
|
func();
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
window.plugin.sync.Authorizer.prototype.authorize = function(popup) {
|
|
this.authorizing = true;
|
|
this.authorized = false;
|
|
var handleAuthResult, _this;
|
|
_this = this;
|
|
|
|
handleAuthResult = function(authResult) {
|
|
if(authResult && !authResult.error) {
|
|
_this.authorized = true;
|
|
plugin.sync.logger.log('Authorized');
|
|
} else {
|
|
_this.authorized = false;
|
|
var error = (authResult && authResult.error) ? authResult.error : 'not authorized';
|
|
plugin.sync.logger.log('Authorization error: ' + error);
|
|
}
|
|
_this.authComplete();
|
|
};
|
|
|
|
gapi.auth.authorize({'client_id': this.CLIENT_ID, 'scope': this.SCOPES, 'immediate': !popup}
|
|
, handleAuthResult);
|
|
}
|
|
//// end Authorizer
|
|
|
|
|
|
|
|
|
|
//// Logger
|
|
window.plugin.sync.Logger = function(options) {
|
|
this.logLimit = options['logLimit'];
|
|
this.logUpdateCallback = options['logUpdateCallback'];
|
|
this.logs = [];
|
|
this.log = this.log.bind(this);
|
|
this.getLogs = this.getLogs.bind(this);
|
|
}
|
|
|
|
window.plugin.sync.Logger.prototype.log = function(message) {
|
|
var log = {'time': new Date(), 'message': message};
|
|
this.logs.unshift(log);
|
|
if(this.logs.length > this.logLimit) {
|
|
this.logs.pop();
|
|
}
|
|
if(this.logUpdateCallback) this.logUpdateCallback(this.getLogs());
|
|
}
|
|
|
|
window.plugin.sync.Logger.prototype.getLogs = function() {
|
|
var allLogs = '';
|
|
$.each(this.logs, function(ind,log) {
|
|
allLogs += log.time.toLocaleTimeString() + ': ' + log.message + '<br />';
|
|
});
|
|
return allLogs;
|
|
}
|
|
|
|
|
|
//// end Logger
|
|
|
|
|
|
|
|
|
|
// http://stackoverflow.com/a/8809472/2322660
|
|
// http://stackoverflow.com/a/7221797/2322660
|
|
// With format fixing: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx where y in [8,9,a,b]
|
|
window.plugin.sync.generateUUID = function() {
|
|
if(window.crypto && window.crypto.getRandomValues) {
|
|
var buf = new Uint16Array(8);
|
|
window.crypto.getRandomValues(buf);
|
|
var S4 = function(num) {
|
|
var ret = num.toString(16);
|
|
return '000'.substring(0, 4-ret.length) + ret;
|
|
};
|
|
var yxxx = function(num) {
|
|
return num&0x3fff|0x8000;
|
|
}
|
|
return (S4(buf[0])+S4(buf[1])+'-'+S4(buf[2])+'-4'+S4(buf[3]).substring(1)+'-'+S4(yxxx(buf[4]))+'-'+S4(buf[5])+S4(buf[6])+S4(buf[7]));
|
|
} else {
|
|
var d = new Date().getTime();
|
|
var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
var r = (d + Math.random()*16)%16 | 0;
|
|
d = Math.floor(d/16);
|
|
return (c=='x' ? r : (r&0x3|0x8)).toString(16);
|
|
});
|
|
return uuid;
|
|
}
|
|
}
|
|
|
|
window.plugin.sync.storeLocal = function(mapping) {
|
|
if(typeof(plugin.sync[mapping.field]) !== 'undefined' && plugin.sync[mapping.field] !== null) {
|
|
localStorage[mapping.key] = JSON.stringify(plugin.sync[mapping.field]);
|
|
} else {
|
|
localStorage.removeItem(mapping.key);
|
|
}
|
|
}
|
|
|
|
window.plugin.sync.loadLocal = function(mapping) {
|
|
var objectJSON = localStorage[mapping.key];
|
|
if(!objectJSON) return;
|
|
plugin.sync[mapping.field] = mapping.convertFunc
|
|
? mapping.convertFunc(JSON.parse(objectJSON))
|
|
: JSON.parse(objectJSON);
|
|
}
|
|
|
|
window.plugin.sync.loadUUID = function() {
|
|
plugin.sync.loadLocal(plugin.sync.KEY_UUID);
|
|
if(!plugin.sync.uuid) {
|
|
plugin.sync.uuid = plugin.sync.generateUUID();
|
|
plugin.sync.storeLocal(plugin.sync.KEY_UUID);
|
|
}
|
|
}
|
|
|
|
window.plugin.sync.updateLog = function(messages) {
|
|
$('#sync-log').html(messages);
|
|
}
|
|
|
|
window.plugin.sync.toggleAuthButton = function() {
|
|
var authed, authorizing;
|
|
authed = plugin.sync.authorizer.isAuthed();
|
|
authorizing = plugin.sync.authorizer.isAuthorizing();
|
|
|
|
$('#sync-authButton').html(authed ? 'Authorized' : 'Authorize');
|
|
|
|
$('#sync-authButton').attr('disabled', (authed || authorizing));
|
|
$('#sync-authButton').toggleClass('sync-authButton-dimmed', authed || authorizing);
|
|
}
|
|
|
|
window.plugin.sync.toggleDialogLink = function() {
|
|
var authed, anyFail;
|
|
authed = plugin.sync.authorizer.isAuthed();
|
|
anyFail = plugin.sync.registeredPluginsFields.anyFail;
|
|
|
|
$('#sync-show-dialog').toggleClass('sync-show-dialog-error', !authed || anyFail);
|
|
}
|
|
|
|
window.plugin.sync.showDialog = function() {
|
|
window.dialog({html: plugin.sync.dialogHTML, title: 'Sync', modal: true, id: 'sync-setting'});
|
|
plugin.sync.toggleAuthButton();
|
|
plugin.sync.toggleDialogLink();
|
|
plugin.sync.updateLog(plugin.sync.logger.getLogs());
|
|
}
|
|
|
|
window.plugin.sync.setupDialog = function() {
|
|
plugin.sync.dialogHTML = '<div id="sync-dialog">'
|
|
+ '<button id="sync-authButton" class="sync-authButton-dimmed" '
|
|
+ 'onclick="setTimeout(function(){window.plugin.sync.authorizer.authorize(true)}, 1)" '
|
|
+ 'disabled="disabled">Authorize</button>'
|
|
+ '<div id="sync-log"></div>'
|
|
+ '</div>';
|
|
$('#toolbox').append('<a id="sync-show-dialog" onclick="window.plugin.sync.showDialog();">Sync</a> ');
|
|
}
|
|
|
|
window.plugin.sync.setupCSS = function() {
|
|
$("<style>")
|
|
.prop("type", "text/css")
|
|
.html(".sync-authButton-dimmed {\
|
|
opacity: 0.5;\
|
|
}\
|
|
.sync-show-dialog-error {\
|
|
color: #FF2222;\
|
|
}\
|
|
#sync-log {\
|
|
height: 300px;\
|
|
white-space: pre-wrap;\
|
|
white-space: -moz-pre-wrap;\
|
|
white-space: -o-pre-wrap;\
|
|
word-wrap: break-word;\
|
|
overflow-y: auto;\
|
|
}")
|
|
.appendTo("head");
|
|
}
|
|
|
|
var setup = function() {
|
|
window.plugin.sync.logger = new plugin.sync.Logger({'logLimit':10, 'logUpdateCallback': plugin.sync.updateLog});
|
|
window.plugin.sync.loadUUID();
|
|
window.plugin.sync.setupCSS();
|
|
window.plugin.sync.setupDialog();
|
|
|
|
window.plugin.sync.authorizer = new window.plugin.sync.Authorizer({
|
|
'authCallback': [plugin.sync.toggleAuthButton, plugin.sync.toggleDialogLink]
|
|
});
|
|
window.plugin.sync.registeredPluginsFields = new window.plugin.sync.RegisteredPluginsFields({
|
|
'authorizer': window.plugin.sync.authorizer
|
|
});
|
|
|
|
var GOOGLEAPI = 'https://apis.google.com/js/api.js';
|
|
load(GOOGLEAPI).thenRun(function() {
|
|
gapi.load('auth:client,drive-realtime,drive-share', window.plugin.sync.authorizer.authorize);
|
|
});
|
|
}
|
|
|
|
// PLUGIN END //////////////////////////////////////////////////////////
|
|
|
|
@@PLUGINEND@@
|