mirror of
https://github.com/zero-peak/ZeroOmega.git
synced 2025-01-22 15:08:12 -05:00
Use Gist as a new synchronization method
This commit is contained in:
parent
3994518c00
commit
6f4a579c18
@ -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;
|
||||
}
|
||||
|
@ -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 '<secret>'
|
||||
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()
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
module.exports =
|
||||
Storage: require('./storage')
|
||||
SyncStorage: require('./sync_storage')
|
||||
Options: require('./options')
|
||||
ChromeTabs: require('./tabs')
|
||||
SwitchySharp: require('./switchysharp')
|
||||
|
384
omega-target-chromium-extension/src/module/sync_storage.coffee
Normal file
384
omega-target-chromium-extension/src/module/sync_storage.coffee
Normal file
@ -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 <YOUR-TOKEN>" \
|
||||
# -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
|
@ -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++
|
||||
|
@ -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 "<secret>"
|
||||
else
|
||||
value
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
)
|
||||
|
@ -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'
|
||||
|
@ -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}}
|
||||
|
@ -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}}
|
||||
|
Loading…
Reference in New Issue
Block a user