From 6f4a579c18b0e0ee344438dc3aff46f7658a2491 Mon Sep 17 00:00:00 2001 From: suziwen Date: Sat, 3 Aug 2024 15:15:20 +0800 Subject: [PATCH] Use Gist as a new synchronization method --- .../overlay/localstorage-polyfill.js | 37 +- .../overlay/log.js | 19 +- .../src/coffee/background.coffee | 21 +- .../src/coffee/omega_debug.coffee | 42 +- .../src/coffee/omega_target_web.coffee | 3 +- .../src/module/index.coffee | 1 + .../src/module/sync_storage.coffee | 384 ++++++++++++++++++ omega-target/src/browser_storage.coffee | 4 +- omega-target/src/log.coffee | 2 +- omega-target/src/options.coffee | 115 ++++-- omega-target/src/options_sync.coffee | 16 +- omega-web/lib/themes/base.css | 10 +- omega-web/src/omega/controllers/about.coffee | 27 +- omega-web/src/omega/controllers/io.coffee | 63 ++- omega-web/src/omega/controllers/master.coffee | 3 - omega-web/src/partials/about.jade | 6 +- omega-web/src/partials/io.jade | 38 +- 17 files changed, 679 insertions(+), 112 deletions(-) create mode 100644 omega-target-chromium-extension/src/module/sync_storage.coffee diff --git a/omega-target-chromium-extension/overlay/localstorage-polyfill.js b/omega-target-chromium-extension/overlay/localstorage-polyfill.js index 754de83..ba2af9b 100644 --- a/omega-target-chromium-extension/overlay/localstorage-polyfill.js +++ b/omega-target-chromium-extension/overlay/localstorage-polyfill.js @@ -42,23 +42,24 @@ class LocalStorage { } const instance = new LocalStorage() -if (!globalThis.localStorage) { - globalThis.localStorage = new Proxy(instance, { - set: function (obj, prop, value) { - if (LocalStorage.prototype.hasOwnProperty(prop)) { - instance[prop] = value - } else { - instance.setItem(prop, value) - } - return true - }, - get: function (target, name) { - if (LocalStorage.prototype.hasOwnProperty(name)) { - return instance[name] - } - if (valuesMap.has(name)) { - return instance.getItem(name) - } +globalThis.zeroLocalStorage = new Proxy(instance, { + set: function (obj, prop, value) { + if (LocalStorage.prototype.hasOwnProperty(prop)) { + instance[prop] = value + } else { + instance.setItem(prop, value) } - }) + return true + }, + get: function (target, name) { + if (LocalStorage.prototype.hasOwnProperty(name)) { + return instance[name] + } + if (valuesMap.has(name)) { + return instance.getItem(name) + } + } +}) +if (!globalThis.localStorage) { + globalThis.localStorage = globalThis.zeroLocalStorage; } diff --git a/omega-target-chromium-extension/overlay/log.js b/omega-target-chromium-extension/overlay/log.js index 5552a8e..6b7cae7 100644 --- a/omega-target-chromium-extension/overlay/log.js +++ b/omega-target-chromium-extension/overlay/log.js @@ -1,7 +1,5 @@ const logStore = idbKeyval.createStore('log-store', 'log-store'); -const dayOfWeek = moment().format('E') // Day of Week (ISO), keep logs max 7 day -const logKey = 'zerolog-' + dayOfWeek const logSequence = [] let isRunning = false let splitStr = '\n------------------\n' @@ -12,15 +10,21 @@ const originConsoleError = console.error const _logFn = async function(){ if (isRunning) return isRunning = true + const _moment = moment() + + const dayOfWeek = _moment.format('E') // Day of Week (ISO), keep logs max 7 day + const monthNum = _moment.format('DD') + const logKey = 'zerolog-' + dayOfWeek while (logSequence.length > 0) { const str = logSequence.join('\n'); logSequence.length = 0; let logInfo = await idbKeyval.get(logKey, logStore) + let date = _moment.format('YYYY-MM-DD') if (!logInfo || !logInfo.date) { - logInfo = { date: moment().format('YYYY-MM-DD'), val: ''} + logInfo = { date: date, val: ''} } - let { date, val } = logInfo - if ( !date.endsWith(dayOfWeek)) { + let { val } = logInfo + if ( logInfo.date != date) { val = '' } val += splitStr @@ -43,6 +47,9 @@ const replacerFn = (key, value)=>{ case 'password': case 'host': case 'port': + case 'token': + case 'gistToken': + case 'gistId': return '' default: return value @@ -84,7 +91,7 @@ const _lastErrorLogFn = async ()=>{ _lastErrorLogFn.isRunning = false } -const lastErrorLogFn = async ()=>{ +const lastErrorLogFn = async function (){ const val = getStr.apply(null, arguments) _lastErrorLogFn.val = val _lastErrorLogFn() diff --git a/omega-target-chromium-extension/src/coffee/background.coffee b/omega-target-chromium-extension/src/coffee/background.coffee index bfef23e..192a6d7 100644 --- a/omega-target-chromium-extension/src/coffee/background.coffee +++ b/omega-target-chromium-extension/src/coffee/background.coffee @@ -154,13 +154,11 @@ actionForUrl = (url) -> storage = new OmegaTargetCurrent.Storage('local') -state = new OmegaTargetCurrent.BrowserStorage(localStorage, 'omega.local.') +state = new OmegaTargetCurrent.BrowserStorage(zeroLocalStorage, 'omega.local.') if chrome?.storage?.sync or browser?.storage?.sync - syncStorage = new OmegaTargetCurrent.Storage('sync') + syncStorage = new OmegaTargetCurrent.SyncStorage('sync', state) sync = new OmegaTargetCurrent.OptionsSync(syncStorage) - if localStorage['omega.local.syncOptions'] != '"sync"' - sync.enabled = false sync.transformValue = OmegaTargetCurrent.Options.transformValueForSync proxyImpl = OmegaTargetCurrent.proxy.getProxyImpl(Log) @@ -315,10 +313,23 @@ refreshActivePageIfEnabled = -> else chrome.tabs.reload(tabs[0].id, {bypassCache: true}) + +resetAllOptions = -> + options.ready.then -> + options._watchStop?() + options._syncWatchStop?() + Promise.all([ + chrome.storage.sync.clear(), + chrome.storage.local.clear() + ]) + chrome.runtime.onMessage.addListener (request, sender, respond) -> return unless request and request.method options.ready.then -> - if request.method == 'getState' + if request.method == 'resetAllOptions' + target = globalThis + method = resetAllOptions + else if request.method == 'getState' target = state method = state.get else if request.method == 'setState' diff --git a/omega-target-chromium-extension/src/coffee/omega_debug.coffee b/omega-target-chromium-extension/src/coffee/omega_debug.coffee index 216d069..f7caa99 100644 --- a/omega-target-chromium-extension/src/coffee/omega_debug.coffee +++ b/omega-target-chromium-extension/src/coffee/omega_debug.coffee @@ -1,7 +1,13 @@ logStore = idbKeyval.createStore('log-store', 'log-store') +syncStore = idbKeyval.createStore('sync-store', 'sync') -isProcessing = false +waitTimeFn = (timeout = 1000) -> + return new Promise((resolve, reject) -> + setTimeout( -> + resolve() + , timeout) + ) window.OmegaDebug = getProjectVersion: -> @@ -9,8 +15,6 @@ window.OmegaDebug = getExtensionVersion: -> chrome.runtime.getManifest().version downloadLog: -> - return if isProcessing - isProcessing = true idbKeyval.entries(logStore).then((entries) -> zip = new JSZip() zipFolder = zip.folder('ZeroOmega') @@ -28,26 +32,30 @@ window.OmegaDebug = ).then((blob) -> filename = "ZeroOmegaLog_#{Date.now()}.zip" saveAs(blob, filename) - isProcessing = false ) resetOptions: -> - return if isProcessing - isProcessing = true - Promise.all([ - idbKeyval.clear(logStore), - chrome.storage.local.clear() - ]).then( -> + chrome.runtime.sendMessage({ + method: 'resetAllOptions' + }, (response) -> + # firefox still use localStorage localStorage.clear() - # Prevent options loading from sync storage after reload. - localStorage['omega.local.syncOptions'] = '"conflict"' - isProcessing = false - chrome.runtime.reload() + # as storage watch value changed + # and background localStorage state delay saved + # this must after storage and wait 2 seconds + Promise.all([ + idbKeyval.clear(logStore), + idbKeyval.clear(syncStore), + waitTimeFn(2000) + ]).then( -> + idbKeyval.clear() + ).then( -> + # Prevent options loading from sync storage after reload. + #localStorage['omega.local.syncOptions'] = '"conflict"' + chrome.runtime.reload() + ) ) reportIssue: -> - return if isProcessing - isProcessing = true idbKeyval.get('lastError', logStore).then((lastError) -> - isProcessing = false url = 'https://github.com/suziwen/ZeroOmega/issues/new?title=&body=' finalUrl = url try diff --git a/omega-target-chromium-extension/src/coffee/omega_target_web.coffee b/omega-target-chromium-extension/src/coffee/omega_target_web.coffee index 1530c5f..cb64e24 100644 --- a/omega-target-chromium-extension/src/coffee/omega_target_web.coffee +++ b/omega-target-chromium-extension/src/coffee/omega_target_web.coffee @@ -158,7 +158,8 @@ angular.module('omegaTarget', []).factory 'omegaTarget', ($q) -> chrome.tabs.create url: 'chrome://extensions/configureCommands' setOptionsSync: (enabled, args) -> callBackground('setOptionsSync', enabled, args) - resetOptionsSync: (enabled, args) -> callBackground('resetOptionsSync') + resetOptionsSync: (args) -> callBackground('resetOptionsSync', args) + checkOptionsSyncChange: -> callBackground('checkOptionsSyncChange') setRequestInfoCallback: (callback) -> requestInfoCallback = callback diff --git a/omega-target-chromium-extension/src/module/index.coffee b/omega-target-chromium-extension/src/module/index.coffee index cedc339..cfa0253 100644 --- a/omega-target-chromium-extension/src/module/index.coffee +++ b/omega-target-chromium-extension/src/module/index.coffee @@ -1,5 +1,6 @@ module.exports = Storage: require('./storage') + SyncStorage: require('./sync_storage') Options: require('./options') ChromeTabs: require('./tabs') SwitchySharp: require('./switchysharp') diff --git a/omega-target-chromium-extension/src/module/sync_storage.coffee b/omega-target-chromium-extension/src/module/sync_storage.coffee new file mode 100644 index 0000000..a647ac2 --- /dev/null +++ b/omega-target-chromium-extension/src/module/sync_storage.coffee @@ -0,0 +1,384 @@ +OmegaTarget = require('omega-target') +Promise = OmegaTarget.Promise + + +onChangedListenerInstalled = false +isPulling = false +isPushing = false + +state = null + +optionFilename = 'ZeroOmega.json' +gistId = '' +gistToken = '' +gistHost = 'https://api.github.com' + +processCheckCommit = -> + getLastCommit(gistId).then((remoteCommit) -> + state.set({ + 'lastGistSync': Date.now() + }).then(-> + state.get({'lastGistCommit': '-2'}).then(({ lastGistCommit }) -> + return lastGistCommit isnt remoteCommit + ) + ) + ).catch( -> + return true + ) + +processPull = (syncStore) -> + return new Promise((resolve, reject) -> + getGist(gistId).then((gist) -> + if isPushing + resolve({changes: {}}) + else + changes = {} + getAll(syncStore).then((data) -> + try + optionsStr = gist.files[optionFilename]?.content + options = JSON.parse(optionsStr) + for own key, val of data + changes[key] = { + oldValue: val + } + for own key, val of options + target = changes[key] + unless target + changes[key] = {} + target = changes[key] + target.newValue = val + for own key,val of changes + if JSON.stringify(val.oldValue) is JSON.stringify(val.newValue) + delete changes[key] + catch e + changes = {} + state?.set({ + 'lastGistCommit': gist.history[0]?.version + 'lastGistState': 'success' + 'lastGistSync': Date.now() + }) + resolve({ + changes: changes, + remoteOptions: options + }) + ) + ).catch((e) -> + state?.set({ + 'lastGistSync': Date.now() + 'lastGistState': 'fail: ' + e + }) + resolve({changes: {}}) + ) + ) +getAll = (syncStore) -> + idbKeyval.entries(syncStore).then((entries) -> + data = {} + entries.forEach((entry) -> + data[entry[0]] = entry[1] + ) + return data + ) + +_processPush = -> + if processPush.sequence.length > 0 + # syncStore = processPush.sequence.shift() + syncStore = processPush.sequence[processPush.sequence.length - 1] + processPush.sequence.length = 0 + getAll(syncStore).then((data) -> + updateGist(gistId, data) + ).then( -> + _processPush() + ) + else + isPushing = false + +processPush = (syncStore) -> + processPush.sequence.push(syncStore) + return if isPushing + isPushing = true + _processPush() + +processPush.sequence = [] + +getLastCommit = (gistId) -> + fetch(gistHost + '/gists/' + gistId + '/commits?per_page=1', { + headers: { + "Accept": "application/vnd.github+json" + "Authorization": "Bearer " + gistToken + "X-GitHub-Api-Version": "2022-11-28" + } + }).then((res) -> res.json()).then((data) -> + if data.message + throw data.message + return data[0]?.version + ) + + + +getGist = (gistId) -> +#curl -L \ +# -H "Accept: application/vnd.github+json" \ +# -H "Authorization: Bearer " \ +# -H "X-GitHub-Api-Version: 2022-11-28" \ +# https://api.github.com/gists/GIST_ID + fetch(gistHost + '/gists/' + gistId, { + headers: { + "Accept": "application/vnd.github+json" + "Authorization": "Bearer " + gistToken + "X-GitHub-Api-Version": "2022-11-28" + } + }).then((res) -> res.json()).then((data) -> + if data.message + throw data.message + return data + ) + +updateGist = (gistId, options) -> + postBody = { + description: 'ZeroOmega Sync' + files: {} + } + postBody.files[optionFilename] = { + content: JSON.stringify(options, null, 4) + } + fetch(gistHost + '/gists/' + gistId, { + headers: { + "Accept": "application/vnd.github+json" + "Authorization": "Bearer " + gistToken + "X-GitHub-Api-Version": "2022-11-28" + } + "method": "PATCH" + body: JSON.stringify(postBody) + }).then((res) -> + res.json() + ).then((data) -> + if data.message + throw data.message + state?.set({ + 'lastGistCommit': data.history[0]?.version + 'lastGistState': 'success' + 'lastGistSync': Date.now() + }) + return data + ).catch((e) -> + state?.set({ + 'lastGistState': 'fail: ' + e + 'lastGistSync': Date.now() + }) + console.error('update gist fail::', e) + ) + + +class ChromeSyncStorage extends OmegaTarget.Storage + @parseStorageErrors: (err) -> + if err?.message + sustainedPerMinute = 'MAX_SUSTAINED_WRITE_OPERATIONS_PER_MINUTE' + if err.message.indexOf('QUOTA_BYTES_PER_ITEM') >= 0 + err = new OmegaTarget.Storage.QuotaExceededError() + err.perItem = true + else if err.message.indexOf('QUOTA_BYTES') >= 0 + err = new OmegaTarget.Storage.QuotaExceededError() + else if err.message.indexOf('MAX_ITEMS') >= 0 + err = new OmegaTarget.Storage.QuotaExceededError() + err.maxItems = true + else if err.message.indexOf('MAX_WRITE_OPERATIONS_') >= 0 + err = new OmegaTarget.Storage.RateLimitExceededError() + if err.message.indexOf('MAX_WRITE_OPERATIONS_PER_HOUR') >= 0 + err.perHour = true + else if err.message.indexOf('MAX_WRITE_OPERATIONS_PER_MINUTE') >= 0 + err.perMinute = true + else if err.message.indexOf(sustainedPerMinute) >= 0 + err = new OmegaTarget.Storage.RateLimitExceededError() + err.perMinute = true + err.sustained = 10 + else if err.message.indexOf('is not available') >= 0 + # This could happen if the storage area is not available. For example, + # some Chromium-based browsers disable access to the sync storage. + err = new OmegaTarget.Storage.StorageUnavailableError() + else if err.message.indexOf( + 'Please set webextensions.storage.sync.enabled to true') >= 0 + # This happens when sync storage is disabled in flags. + err = new OmegaTarget.Storage.StorageUnavailableError() + + return Promise.reject(err) + + constructor: (@areaName, _state) -> + state = _state + syncStore = idbKeyval.createStore('sync-store', 'sync') + @syncStore = syncStore + get = (key) -> + return new Promise((resolve, reject) -> + getAll(syncStore).then((data) -> + result = {} + if Array.isArray(key) + key.forEach( _key -> + result[_key] = data[_key] + ) + else if key is null + result = data + else + result[key] = data[key] + resolve(result) + ) + ) + set = (record) -> + return new Promise((resolve, reject) -> + try + if !record or typeof record isnt 'object' or Array.isArray(record) + throw new SyntaxError( + 'Only Object with key value pairs are acceptable') + entries = [] + for own key, value of record + entries.push([key, value]) + idbKeyval.setMany(entries, syncStore).then( -> + processPush(syncStore) + resolve(record) + ) + catch e + reject(e) + ) + _remove = (key) -> + if Array.isArray(key) + Promise.resolve(idbKeyval.delMany(key, syncStore)) + else + Promise.resolve(idbKeyval.del(key, syncStore)) + remove = (key) -> + Promise.resolve(_remove(key).then( -> + processPush(syncStore) + return + )) + clear = -> + Promise.resolve(idbKeyval.clear(syncStore).then(-> + processPush(syncStore) + return + )) + @storage = + get: get + set: set + remove: remove + clear: clear + get: (keys) -> + keys ?= null + Promise.resolve(@storage.get(keys)) + .catch(ChromeSyncStorage.parseStorageErrors) + + set: (items) -> + if Object.keys(items).length == 0 + return Promise.resolve({}) + Promise.resolve(@storage.set(items)) + .catch(ChromeSyncStorage.parseStorageErrors) + + remove: (keys) -> + if not keys? + return Promise.resolve(@storage.clear()) + if Array.isArray(keys) and keys.length == 0 + return Promise.resolve({}) + Promise.resolve(@storage.remove(keys)) + .catch(ChromeSyncStorage.parseStorageErrors) + flush: ({data}) -> + entries = [] + result = null + if data and data.schemaVersion + for own key, value of data + entries.push([key, value]) + result = idbKeyval.setMany(entries, @syncStore) + Promise.resolve(result) + + ## + # param(withRemoteData) retrive gist file content + ## + init: (args) -> + gistId = args.gistId + gistToken = args.gistToken + return new Promise((resolve, reject) -> + getLastCommit(gistId).then( (lastGistCommit) -> + if args.withRemoteData + getGist(gistId).then((gist) -> + try + optionsStr = gist.files[optionFilename].content + options = JSON.parse(optionsStr) + resolve({options, lastGistCommit}) + catch e + resolve({}) + ) + else + resolve({}) + ).catch((e) -> + reject(e) + ) + ) + + ## + # param (opts) opts.immediately , immediately update changed + # param (opts) opts.force, force get remote content + ## + checkChange: (opts = {}) -> + isPulling = true + processCheckCommit().then((isChanged) => + if isChanged or opts.force + processPull(@syncStore).then(({changes, remoteOptions}) => + @flush({data: remoteOptions}).then( => + isPulling = false + ChromeSyncStorage.onChangedListener(changes, @areaName, opts) + ) + ) + else + console.log('no changed') + isPulling = false + ) + + watch: (keys, callback) -> + chrome.alarms.create('omega.syncCheck', { + periodInMinutes: 5 + }) + ChromeSyncStorage.watchers[@areaName] ?= {} + area = ChromeSyncStorage.watchers[@areaName] + watcher = {keys: keys, callback: callback} + enableSync = true + id = Date.now().toString() + while area[id] + id = Date.now().toString() + + if Array.isArray(keys) + keyMap = {} + for key in keys + keyMap[key] = true + keys = keyMap + area[id] = {keys: keys, callback: callback} + if not onChangedListenerInstalled + # chrome alerm + @checkChange() + chrome.alarms.onAlarm.addListener (alarm) => + return unless enableSync + return if isPulling + switch alarm.name + when 'omega.syncCheck' + @checkChange() + #chrome.storage.onChanged.addListener(ChromeSyncStorage.onChangedListener) + onChangedListenerInstalled = true + return -> + enableSync = false + delete area[id] + + ## + # param (opts) opts.immediately , immediately update changed + ## + @onChangedListener: (changes, areaName, opts = {}) -> + map = null + for _, watcher of ChromeSyncStorage.watchers[areaName] + match = watcher.keys == null + if not match + for own key of changes + if watcher.keys[key] + match = true + break + if match + if not map? + map = {} + for own key, change of changes + map[key] = change.newValue + watcher.callback(map, opts) + + @watchers: {} + +module.exports = ChromeSyncStorage diff --git a/omega-target/src/browser_storage.coffee b/omega-target/src/browser_storage.coffee index c48071a..0db4937 100644 --- a/omega-target/src/browser_storage.coffee +++ b/omega-target/src/browser_storage.coffee @@ -59,9 +59,9 @@ class BrowserStorage extends Storage else index = 0 while true - key = @proto.key.call(index) + key = @proto.key.call(@storage, index) break if key == null - if @key.substr(0, @prefix.length) == @prefix + if key.substr(0, @prefix.length) == @prefix @proto.removeItem.call(@storage, @prefix + keys) else index++ diff --git a/omega-target/src/log.coffee b/omega-target/src/log.coffee index 5216d13..1bf1c84 100644 --- a/omega-target/src/log.coffee +++ b/omega-target/src/log.coffee @@ -4,7 +4,7 @@ Log = require './log' replacer = (key, value) -> switch key # Hide values for a few keys with privacy concerns. - when "username", "password", "host", "port" + when "username", "password", "host", "port", "token", "gistToken", "gistId" return "" else value diff --git a/omega-target/src/options.coffee b/omega-target/src/options.coffee index 3abdfac..834b16f 100644 --- a/omega-target/src/options.coffee +++ b/omega-target/src/options.coffee @@ -45,6 +45,8 @@ class Options # @returns {{}} The transformed value ### @transformValueForSync: (value, key) -> + if key is '-customCss' + return undefined if key[0] == '+' if OmegaPac.Profiles.updateUrl(value) profile = {} @@ -83,22 +85,36 @@ class Options @_watchStop = null loadRaw = if options? then Promise.resolve(options) else - if not @sync?.enabled - if not @sync? - @_state.set({'syncOptions': 'unsupported'}) + if not @sync? + @_state.set({'syncOptions': 'unsupported'}) @_storage.get(null) else - @_state.set({'syncOptions': 'sync'}) - @_syncWatchStop = @sync.watchAndPull(@_storage) - @sync.copyTo(@_storage).catch(Storage.StorageUnavailableError, => - console.error('Warning: Sync storage is not available in this ' + - 'browser! Disabling options sync.') - @_syncWatchStop?() - @_syncWatchStop = null - @sync = null - @_state.set({'syncOptions': 'unsupported'}) - ).then => - @_storage.get(null) + @_state.get({ + 'syncOptions': '' + 'gistId': '' + 'gistToken': '' + }).then(({syncOptions, gistId, gistToken}) => + unless gistId + syncOptions = 'pristine' + @_state.set({'syncOptions': 'pristine'}) + @sync.enabled = syncOptions is 'sync' + unless @sync.enabled + @_storage.get(null) + else + @sync.init({gistId, gistToken}).catch((e) -> + console.error('sync init fail::', e) + ) + @_syncWatchStop = @sync.watchAndPull(@_storage) + @sync.copyTo(@_storage).catch(Storage.StorageUnavailableError, => + console.error('Warning: Sync storage is not available in this ' + + 'browser! Disabling options sync.') + @_syncWatchStop?() + @_syncWatchStop = null + @sync = null + @_state.set({'syncOptions': 'unsupported'}) + ).then => + @_storage.get(null) + ) @optionsLoaded = loadRaw.then((options) => @upgrade(options) @@ -235,7 +251,7 @@ class Options # Current schemaVersion. Promise.resolve([options, changes]) else - Promise.reject new Error("Invalid schemaVerion #{version}!") + Promise.reject new Error("Invalid schemaVersion #{version}!") ###* # Parse options in various formats (including JSON & base64). @@ -1004,53 +1020,80 @@ class Options # @param {boolean=false} args.force If true, overwrite options when conflict # @returns {Promise} A promise which is fulfilled when the syncing is switched ### - setOptionsSync: (enabled, args) -> + setOptionsSync: (enabled, args = {}) -> @log.method('Options#setOptionsSync', this, arguments) if not @sync? return Promise.reject(new Error('Options syncing is unsupported.')) - @_state.get({'syncOptions': ''}).then ({syncOptions}) => + @_state.get({ + 'syncOptions': '', lastGistCommit: '' + }).then ({syncOptions, lastGistCommit}) => if not enabled if syncOptions == 'sync' - @_state.set({'syncOptions': 'conflict'}) + @_state.set({'syncOptions': 'pristine'}) @sync.enabled = false @_syncWatchStop?() @_syncWatchStop = null return if syncOptions == 'conflict' - if not args?.force + if not args.force return Promise.reject(new Error( 'Syncing not enabled due to conflict. Retry with force to overwrite local options and enable syncing.')) return if syncOptions == 'sync' - @_state.set({'syncOptions': 'sync'}).then => - if syncOptions == 'conflict' - # Try to re-init options from sync. - @sync.enabled = false - @_storage.remove().then => - @sync.enabled = true - @init() - else - @sync.enabled = true - @_syncWatchStop?() - @sync.requestPush(@_options) - @_syncWatchStop = @sync.watchAndPull(@_storage) - return + { gistId, gistToken } = args + @sync.init({ + gistId, gistToken, withRemoteData: true + }).then( ({ + options: remoteOptions, lastGistCommit: remoteLastGistCommit + }) => + @_state.set({ + 'syncOptions': 'sync' + 'gistId': gistId + 'gistToken': gistToken + }).then => + if syncOptions == 'conflict' + # Try to re-init options from sync. + @sync.enabled = false + @_storage.remove().then => + @sync.enabled = true + @init() + else + if remoteOptions.schemaVersion + @sync.flush({data: remoteOptions}).then( => + @sync.enabled = false + @_state.set({'syncOptions': 'conflict'}) + return + ) + else + @sync.enabled = true + @_syncWatchStop?() + @sync.requestPush(@_options) + @_syncWatchStop = @sync.watchAndPull(@_storage) + return + ) ###* # Clear the sync storage, resetting syncing state to pristine. # @returns {Promise} A promise which is fulfilled when the syncing is reset. ### - resetOptionsSync: -> + resetOptionsSync: (args) -> @log.method('Options#resetOptionsSync', this, arguments) if not @sync? return Promise.reject(new Error('Options syncing is unsupported.')) @sync.enabled = false @_syncWatchStop?() @_syncWatchStop = null - @_state.set({'syncOptions': 'conflict'}) - - return @sync.storage.remove().then => + @_state.set({'syncOptions': 'conflict'}).then( => + @sync.init(args) + ).then( => + @sync.storage.remove() + ).then( => @_state.set({'syncOptions': 'pristine'}) + ) + + checkOptionsSyncChange: -> + if @sync and @sync.enabled + @sync.checkChange() module.exports = Options diff --git a/omega-target/src/options_sync.coffee b/omega-target/src/options_sync.coffee index febd408..715dad4 100644 --- a/omega-target/src/options_sync.coffee +++ b/omega-target/src/options_sync.coffee @@ -205,10 +205,22 @@ class OptionsSync @_logOperations('OptionsSync::pull', operations) local.apply(operations) - @storage.watch null, (changes) => + @storage.watch null, (changes, opts = {}) => for own key, value of changes pull[key] = value return if pullScheduled? - pullScheduled = setTimeout(doPull, @pullThrottle) + if opts.immediately + doPull() + else + pullScheduled = setTimeout(doPull, @pullThrottle) + checkChange: -> + @storage.checkChange({ + immediately: true + force: true + }) + init: (args) -> + @storage.init(args) + flush: ({data}) -> + @storage.flush({data}) module.exports = OptionsSync diff --git a/omega-web/lib/themes/base.css b/omega-web/lib/themes/base.css index 76ff9bb..c7665d0 100644 --- a/omega-web/lib/themes/base.css +++ b/omega-web/lib/themes/base.css @@ -172,7 +172,7 @@ fieldset[disabled] .form-control { position: relative; } .alert-success { - color: var(--primaryColor); + color: var(--positiveColor); background-color: transparent; border-color: var(--lighterBackground); position: relative; @@ -185,7 +185,7 @@ fieldset[disabled] .form-control { position: absolute; inset: 0; opacity: 0.1; - background-color: var(--primaryColor); + background-color: var(--positiveColor); pointer-events: none; } @@ -429,3 +429,9 @@ main .page-header { .sp-palette-container { border-right-color: var(--selectionBackground); } + +.input-group-addon{ + color: var(--defaultForeground); + background-color: var(--lighterBackground); + border-color: var(--lighterBackground); +} diff --git a/omega-web/src/omega/controllers/about.coffee b/omega-web/src/omega/controllers/about.coffee index 4f8880b..e3b9797 100644 --- a/omega-web/src/omega/controllers/about.coffee +++ b/omega-web/src/omega/controllers/about.coffee @@ -1,12 +1,25 @@ -angular.module('omega').controller 'AboutCtrl', ($scope, $rootScope, - $modal, omegaDebug) -> - - $scope.downloadLog = omegaDebug.downloadLog - $scope.reportIssue = omegaDebug.reportIssue +angular.module('omega').controller 'AboutCtrl', ( + $scope, $rootScope,$modal, omegaDebug +) -> + $scope.downloadLog = -> + $scope.logDownloading = true + Promise.resolve(omegaDebug.downloadLog()).then( -> + $scope.logDownloading = false + ) + $scope.reportIssue = -> + $scope.issueReporting = true + omegaDebug.reportIssue().then( -> + $scope.issueReporting = false + ) $scope.showResetOptionsModal = -> - $modal.open(templateUrl: 'partials/reset_options_confirm.html').result - .then -> omegaDebug.resetOptions() + $modal + .open(templateUrl: 'partials/reset_options_confirm.html').result + .then -> + $scope.optionsReseting = true + omegaDebug.resetOptions().then( -> + $scope.optionsReseting = false + ) try $scope.version = omegaDebug.getProjectVersion() diff --git a/omega-web/src/omega/controllers/io.coffee b/omega-web/src/omega/controllers/io.coffee index 4c9e9b7..8a3843a 100644 --- a/omega-web/src/omega/controllers/io.coffee +++ b/omega-web/src/omega/controllers/io.coffee @@ -1,9 +1,22 @@ -angular.module('omega').controller 'IoCtrl', ($scope, $rootScope, - $window, $http, omegaTarget, downloadFile) -> +angular.module('omega').controller 'IoCtrl', ( + $scope, $rootScope, $window, $http, omegaTarget, downloadFile +) -> - omegaTarget.state('web.restoreOnlineUrl').then (url) -> + omegaTarget.state([ + 'web.restoreOnlineUrl', + 'gistId', + 'gistToken', + 'lastGistSync', + 'lastGistState' + ]).then ([url, gistId, gistToken, lastGistSync, lastGistState]) -> if url $scope.restoreOnlineUrl = url + if gistId + $scope.gistId = gistId + if gistToken + $scope.gistToken = gistToken + $scope.lastGistSync = new Date(lastGistSync or Date.now()) + $scope.lastGistState = lastGistState or '' $scope.exportOptions = -> $rootScope.applyOptionsConfirm().then -> @@ -57,21 +70,59 @@ angular.module('omega').controller 'IoCtrl', ($scope, $rootScope, ), $scope.downloadError).finally -> $scope.restoringOnline = false - $scope.enableOptionsSync = (args) -> + $scope.enableOptionsSync = (args = {}) -> enable = -> - omegaTarget.setOptionsSync(true, args).finally -> + if !$scope.gistId or !$scope.gistToken + $rootScope.showAlert( + type: 'error' + message: 'Gist Id or Gist Token is required' + ) + return + args.gistId = $scope.gistId + args.gistToken = $scope.gistToken + $scope.enableOptionsSyncing = true + omegaTarget.setOptionsSync(true, args).then( -> $window.location.reload() + ).catch((e) -> + $scope.enableOptionsSyncing = false + $rootScope.showAlert( + type: 'error' + message: e + '' + ) + console.log('error:::', e) + ) if args?.force enable() else $rootScope.applyOptionsConfirm().then enable + $scope.checkOptionsSyncChange = -> + $scope.enableOptionsSyncing = true + omegaTarget.checkOptionsSyncChange().then( -> + $window.location.reload() + ) $scope.disableOptionsSync = -> omegaTarget.setOptionsSync(false).then -> $rootScope.applyOptionsConfirm().then -> $window.location.reload() $scope.resetOptionsSync = -> - omegaTarget.resetOptionsSync().then -> + if !$scope.gistId or !$scope.gistToken + $rootScope.showAlert( + type: 'error' + message: 'Gist Id or Gist Token is required' + ) + return + omegaTarget.resetOptionsSync({ + gistId: $scope.gistId + gistToken: $scope.gistToken + }).then( -> $rootScope.applyOptionsConfirm().then -> $window.location.reload() + ).catch((e) -> + $rootScope.showAlert( + type: 'error' + message: e + '' + ) + console.log('error:::', e) + ) diff --git a/omega-web/src/omega/controllers/master.coffee b/omega-web/src/omega/controllers/master.coffee index c131991..7e3f8b3 100644 --- a/omega-web/src/omega/controllers/master.coffee +++ b/omega-web/src/omega/controllers/master.coffee @@ -90,9 +90,6 @@ angular.module('omega').controller 'MasterCtrl', ($scope, $rootScope, $window, plainOptions = angular.fromJson(angular.toJson($rootScope.options)) patch = diff.diff($rootScope.optionsOld, plainOptions) omegaTarget.optionsPatch(patch).then -> - - omegaTarget.state('customCss').then (customCss = '') -> - $scope.customCss = customCss $rootScope.showAlert( type: 'success' i18n: 'options_saveSuccess' diff --git a/omega-web/src/partials/about.jade b/omega-web/src/partials/about.jade index 5c8ea61..b3d3208 100644 --- a/omega-web/src/partials/about.jade +++ b/omega-web/src/partials/about.jade @@ -14,17 +14,17 @@ section p {{'about_app_description' | tr}} section p - button.btn.btn-info(ng-click='reportIssue()') + button.btn.btn-info(ng-click='reportIssue()' ladda='issueReporting') span.glyphicon.glyphicon-comment = ' ' | {{'popup_reportIssues' | tr}} = ' ' - button.btn.btn-default(ng-click='downloadLog()') + button.btn.btn-default(ng-click='downloadLog()' ladda='logDownloading' data-spinner-color="currentColor") span.glyphicon.glyphicon-download = ' ' | {{'popup_errorLog' | tr}} = ' ' - button.btn.btn-danger(ng-click='showResetOptionsModal()') + button.btn.btn-danger(ng-click='showResetOptionsModal()' ladda='optionsReseting') span.glyphicon.glyphicon-alert = ' ' | {{'options_reset' | tr}} diff --git a/omega-web/src/partials/io.jade b/omega-web/src/partials/io.jade index deeaee7..28f3d69 100644 --- a/omega-web/src/partials/io.jade +++ b/omega-web/src/partials/io.jade @@ -38,15 +38,47 @@ section.settings-group | {{'options_restoreOnlineSubmit' | tr}} section.settings-group h3 {{'options_group_syncing' | tr}} + div + form + div.form-group + label {{'Gist Id'}} + .input-group.width-limit + span.input-group-addon {{'ID'}} + input.form-control(type='text' ng-model='gistId' ng-readonly='syncOptions == "sync"' placeholder="Gist Id e.g. https://gist.github.com/{username}/{Gist Id}") + span.help-block + a(href="https://gist.github.com/" role="button" target="_blank") + | {{'Create a secret Gist. '}} + strong + | {{" Note: If it's a public Gist, your options can be searched by others。"}} + div.form-group + label {{'Gist Token'}} + .input-group.width-limit + span.input-group-addon {{'TOKEN'}} + input.form-control(type='text' ng-model='gistToken' ng-readonly='syncOptions == "sync"' placeholder="Gist Token") + span.help-block + a(href="https://github.com/settings/tokens/new" role="button" target="_blank") + | {{ 'Create a token that manages the Gist.'}} div(ng-show='syncOptions == "pristine" || syncOptions == "disabled"') p.help-block(omega-html='"options_syncPristineHelp" | tr') p - button.btn.btn-default(ng-click='enableOptionsSync()') + button.btn.btn-default(ng-click='enableOptionsSync()' ladda='enableOptionsSyncing' data-spinner-color="currentColor") span.glyphicon.glyphicon-cloud-upload = ' ' | {{'options_syncEnable' | tr}} div(ng-show='syncOptions == "sync"') p.alert.alert-success.width-limit + button.btn.btn-sm.btn-success(ng-click='checkOptionsSyncChange()' ladda='enableOptionsSyncing') + span.glyphicon.glyphicon-refresh + span {{' last sync date: '}} + | {{ lastGistSync | date:'medium'}} + | {{'('}} + | {{lastGistState}} + | {{')'}} + a(href="https://gist.github.com/{{gistId}}" role="button" target="_blank") + | {{' '}} + span.glyphicon.glyphicon-link + br + br span.glyphicon.glyphicon-ok = ' ' | {{"options_syncSyncAlert" | tr}} @@ -57,13 +89,13 @@ section.settings-group = ' ' | {{'options_syncDisable' | tr}} div(ng-show='syncOptions == "conflict"') - p.alert.alert-info.width-limit + p.alert.alert-danger.width-limit span.glyphicon.glyphicon-info-sign = ' ' | {{"options_syncConflictAlert" | tr}} p.help-block(omega-html='"options_syncConflictHelp" | tr') p - button.btn.btn-danger(ng-click='enableOptionsSync({force: true})') + button.btn.btn-danger(ng-click='enableOptionsSync({force: true})' ladda='enableOptionsSyncing' ) span.glyphicon.glyphicon-cloud-download = ' ' | {{'options_syncEnableForce' | tr}}