Use Gist as a new synchronization method

This commit is contained in:
suziwen 2024-08-03 15:15:20 +08:00
parent 3994518c00
commit 6f4a579c18
17 changed files with 679 additions and 112 deletions

View File

@ -42,8 +42,7 @@ class LocalStorage {
} }
const instance = new LocalStorage() const instance = new LocalStorage()
if (!globalThis.localStorage) { globalThis.zeroLocalStorage = new Proxy(instance, {
globalThis.localStorage = new Proxy(instance, {
set: function (obj, prop, value) { set: function (obj, prop, value) {
if (LocalStorage.prototype.hasOwnProperty(prop)) { if (LocalStorage.prototype.hasOwnProperty(prop)) {
instance[prop] = value instance[prop] = value
@ -61,4 +60,6 @@ if (!globalThis.localStorage) {
} }
} }
}) })
if (!globalThis.localStorage) {
globalThis.localStorage = globalThis.zeroLocalStorage;
} }

View File

@ -1,7 +1,5 @@
const logStore = idbKeyval.createStore('log-store', 'log-store'); 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 = [] const logSequence = []
let isRunning = false let isRunning = false
let splitStr = '\n------------------\n' let splitStr = '\n------------------\n'
@ -12,15 +10,21 @@ const originConsoleError = console.error
const _logFn = async function(){ const _logFn = async function(){
if (isRunning) return if (isRunning) return
isRunning = true 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) { while (logSequence.length > 0) {
const str = logSequence.join('\n'); const str = logSequence.join('\n');
logSequence.length = 0; logSequence.length = 0;
let logInfo = await idbKeyval.get(logKey, logStore) let logInfo = await idbKeyval.get(logKey, logStore)
let date = _moment.format('YYYY-MM-DD')
if (!logInfo || !logInfo.date) { if (!logInfo || !logInfo.date) {
logInfo = { date: moment().format('YYYY-MM-DD'), val: ''} logInfo = { date: date, val: ''}
} }
let { date, val } = logInfo let { val } = logInfo
if ( !date.endsWith(dayOfWeek)) { if ( logInfo.date != date) {
val = '' val = ''
} }
val += splitStr val += splitStr
@ -43,6 +47,9 @@ const replacerFn = (key, value)=>{
case 'password': case 'password':
case 'host': case 'host':
case 'port': case 'port':
case 'token':
case 'gistToken':
case 'gistId':
return '<secret>' return '<secret>'
default: default:
return value return value
@ -84,7 +91,7 @@ const _lastErrorLogFn = async ()=>{
_lastErrorLogFn.isRunning = false _lastErrorLogFn.isRunning = false
} }
const lastErrorLogFn = async ()=>{ const lastErrorLogFn = async function (){
const val = getStr.apply(null, arguments) const val = getStr.apply(null, arguments)
_lastErrorLogFn.val = val _lastErrorLogFn.val = val
_lastErrorLogFn() _lastErrorLogFn()

View File

@ -154,13 +154,11 @@ actionForUrl = (url) ->
storage = new OmegaTargetCurrent.Storage('local') 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 if chrome?.storage?.sync or browser?.storage?.sync
syncStorage = new OmegaTargetCurrent.Storage('sync') syncStorage = new OmegaTargetCurrent.SyncStorage('sync', state)
sync = new OmegaTargetCurrent.OptionsSync(syncStorage) sync = new OmegaTargetCurrent.OptionsSync(syncStorage)
if localStorage['omega.local.syncOptions'] != '"sync"'
sync.enabled = false
sync.transformValue = OmegaTargetCurrent.Options.transformValueForSync sync.transformValue = OmegaTargetCurrent.Options.transformValueForSync
proxyImpl = OmegaTargetCurrent.proxy.getProxyImpl(Log) proxyImpl = OmegaTargetCurrent.proxy.getProxyImpl(Log)
@ -315,10 +313,23 @@ refreshActivePageIfEnabled = ->
else else
chrome.tabs.reload(tabs[0].id, {bypassCache: true}) 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) -> chrome.runtime.onMessage.addListener (request, sender, respond) ->
return unless request and request.method return unless request and request.method
options.ready.then -> options.ready.then ->
if request.method == 'getState' if request.method == 'resetAllOptions'
target = globalThis
method = resetAllOptions
else if request.method == 'getState'
target = state target = state
method = state.get method = state.get
else if request.method == 'setState' else if request.method == 'setState'

View File

@ -1,7 +1,13 @@
logStore = idbKeyval.createStore('log-store', 'log-store') 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 = window.OmegaDebug =
getProjectVersion: -> getProjectVersion: ->
@ -9,8 +15,6 @@ window.OmegaDebug =
getExtensionVersion: -> getExtensionVersion: ->
chrome.runtime.getManifest().version chrome.runtime.getManifest().version
downloadLog: -> downloadLog: ->
return if isProcessing
isProcessing = true
idbKeyval.entries(logStore).then((entries) -> idbKeyval.entries(logStore).then((entries) ->
zip = new JSZip() zip = new JSZip()
zipFolder = zip.folder('ZeroOmega') zipFolder = zip.folder('ZeroOmega')
@ -28,26 +32,30 @@ window.OmegaDebug =
).then((blob) -> ).then((blob) ->
filename = "ZeroOmegaLog_#{Date.now()}.zip" filename = "ZeroOmegaLog_#{Date.now()}.zip"
saveAs(blob, filename) saveAs(blob, filename)
isProcessing = false
) )
resetOptions: -> resetOptions: ->
return if isProcessing chrome.runtime.sendMessage({
isProcessing = true method: 'resetAllOptions'
}, (response) ->
# firefox still use localStorage
localStorage.clear()
# as storage watch value changed
# and background localStorage state delay saved
# this must after storage and wait 2 seconds
Promise.all([ Promise.all([
idbKeyval.clear(logStore), idbKeyval.clear(logStore),
chrome.storage.local.clear() idbKeyval.clear(syncStore),
waitTimeFn(2000)
]).then( -> ]).then( ->
localStorage.clear() idbKeyval.clear()
).then( ->
# Prevent options loading from sync storage after reload. # Prevent options loading from sync storage after reload.
localStorage['omega.local.syncOptions'] = '"conflict"' #localStorage['omega.local.syncOptions'] = '"conflict"'
isProcessing = false
chrome.runtime.reload() chrome.runtime.reload()
) )
)
reportIssue: -> reportIssue: ->
return if isProcessing
isProcessing = true
idbKeyval.get('lastError', logStore).then((lastError) -> idbKeyval.get('lastError', logStore).then((lastError) ->
isProcessing = false
url = 'https://github.com/suziwen/ZeroOmega/issues/new?title=&body=' url = 'https://github.com/suziwen/ZeroOmega/issues/new?title=&body='
finalUrl = url finalUrl = url
try try

View File

@ -158,7 +158,8 @@ angular.module('omegaTarget', []).factory 'omegaTarget', ($q) ->
chrome.tabs.create url: 'chrome://extensions/configureCommands' chrome.tabs.create url: 'chrome://extensions/configureCommands'
setOptionsSync: (enabled, args) -> setOptionsSync: (enabled, args) ->
callBackground('setOptionsSync', enabled, args) callBackground('setOptionsSync', enabled, args)
resetOptionsSync: (enabled, args) -> callBackground('resetOptionsSync') resetOptionsSync: (args) -> callBackground('resetOptionsSync', args)
checkOptionsSyncChange: -> callBackground('checkOptionsSyncChange')
setRequestInfoCallback: (callback) -> setRequestInfoCallback: (callback) ->
requestInfoCallback = callback requestInfoCallback = callback

View File

@ -1,5 +1,6 @@
module.exports = module.exports =
Storage: require('./storage') Storage: require('./storage')
SyncStorage: require('./sync_storage')
Options: require('./options') Options: require('./options')
ChromeTabs: require('./tabs') ChromeTabs: require('./tabs')
SwitchySharp: require('./switchysharp') SwitchySharp: require('./switchysharp')

View 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

View File

@ -59,9 +59,9 @@ class BrowserStorage extends Storage
else else
index = 0 index = 0
while true while true
key = @proto.key.call(index) key = @proto.key.call(@storage, index)
break if key == null break if key == null
if @key.substr(0, @prefix.length) == @prefix if key.substr(0, @prefix.length) == @prefix
@proto.removeItem.call(@storage, @prefix + keys) @proto.removeItem.call(@storage, @prefix + keys)
else else
index++ index++

View File

@ -4,7 +4,7 @@ Log = require './log'
replacer = (key, value) -> replacer = (key, value) ->
switch key switch key
# Hide values for a few keys with privacy concerns. # Hide values for a few keys with privacy concerns.
when "username", "password", "host", "port" when "username", "password", "host", "port", "token", "gistToken", "gistId"
return "<secret>" return "<secret>"
else else
value value

View File

@ -45,6 +45,8 @@ class Options
# @returns {{}} The transformed value # @returns {{}} The transformed value
### ###
@transformValueForSync: (value, key) -> @transformValueForSync: (value, key) ->
if key is '-customCss'
return undefined
if key[0] == '+' if key[0] == '+'
if OmegaPac.Profiles.updateUrl(value) if OmegaPac.Profiles.updateUrl(value)
profile = {} profile = {}
@ -83,12 +85,25 @@ class Options
@_watchStop = null @_watchStop = null
loadRaw = if options? then Promise.resolve(options) else loadRaw = if options? then Promise.resolve(options) else
if not @sync?.enabled
if not @sync? if not @sync?
@_state.set({'syncOptions': 'unsupported'}) @_state.set({'syncOptions': 'unsupported'})
@_storage.get(null) @_storage.get(null)
else else
@_state.set({'syncOptions': 'sync'}) @_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) @_syncWatchStop = @sync.watchAndPull(@_storage)
@sync.copyTo(@_storage).catch(Storage.StorageUnavailableError, => @sync.copyTo(@_storage).catch(Storage.StorageUnavailableError, =>
console.error('Warning: Sync storage is not available in this ' + console.error('Warning: Sync storage is not available in this ' +
@ -99,6 +114,7 @@ class Options
@_state.set({'syncOptions': 'unsupported'}) @_state.set({'syncOptions': 'unsupported'})
).then => ).then =>
@_storage.get(null) @_storage.get(null)
)
@optionsLoaded = loadRaw.then((options) => @optionsLoaded = loadRaw.then((options) =>
@upgrade(options) @upgrade(options)
@ -235,7 +251,7 @@ class Options
# Current schemaVersion. # Current schemaVersion.
Promise.resolve([options, changes]) Promise.resolve([options, changes])
else else
Promise.reject new Error("Invalid schemaVerion #{version}!") Promise.reject new Error("Invalid schemaVersion #{version}!")
###* ###*
# Parse options in various formats (including JSON & base64). # 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 # @param {boolean=false} args.force If true, overwrite options when conflict
# @returns {Promise} A promise which is fulfilled when the syncing is switched # @returns {Promise} A promise which is fulfilled when the syncing is switched
### ###
setOptionsSync: (enabled, args) -> setOptionsSync: (enabled, args = {}) ->
@log.method('Options#setOptionsSync', this, arguments) @log.method('Options#setOptionsSync', this, arguments)
if not @sync? if not @sync?
return Promise.reject(new Error('Options syncing is unsupported.')) 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 not enabled
if syncOptions == 'sync' if syncOptions == 'sync'
@_state.set({'syncOptions': 'conflict'}) @_state.set({'syncOptions': 'pristine'})
@sync.enabled = false @sync.enabled = false
@_syncWatchStop?() @_syncWatchStop?()
@_syncWatchStop = null @_syncWatchStop = null
return return
if syncOptions == 'conflict' if syncOptions == 'conflict'
if not args?.force if not args.force
return Promise.reject(new Error( return Promise.reject(new Error(
'Syncing not enabled due to conflict. Retry with force to overwrite 'Syncing not enabled due to conflict. Retry with force to overwrite
local options and enable syncing.')) local options and enable syncing.'))
return if syncOptions == 'sync' return if syncOptions == 'sync'
@_state.set({'syncOptions': 'sync'}).then => { 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' if syncOptions == 'conflict'
# Try to re-init options from sync. # Try to re-init options from sync.
@sync.enabled = false @sync.enabled = false
@_storage.remove().then => @_storage.remove().then =>
@sync.enabled = true @sync.enabled = true
@init() @init()
else
if remoteOptions.schemaVersion
@sync.flush({data: remoteOptions}).then( =>
@sync.enabled = false
@_state.set({'syncOptions': 'conflict'})
return
)
else else
@sync.enabled = true @sync.enabled = true
@_syncWatchStop?() @_syncWatchStop?()
@sync.requestPush(@_options) @sync.requestPush(@_options)
@_syncWatchStop = @sync.watchAndPull(@_storage) @_syncWatchStop = @sync.watchAndPull(@_storage)
return return
)
###* ###*
# Clear the sync storage, resetting syncing state to pristine. # Clear the sync storage, resetting syncing state to pristine.
# @returns {Promise} A promise which is fulfilled when the syncing is reset. # @returns {Promise} A promise which is fulfilled when the syncing is reset.
### ###
resetOptionsSync: -> resetOptionsSync: (args) ->
@log.method('Options#resetOptionsSync', this, arguments) @log.method('Options#resetOptionsSync', this, arguments)
if not @sync? if not @sync?
return Promise.reject(new Error('Options syncing is unsupported.')) return Promise.reject(new Error('Options syncing is unsupported.'))
@sync.enabled = false @sync.enabled = false
@_syncWatchStop?() @_syncWatchStop?()
@_syncWatchStop = null @_syncWatchStop = null
@_state.set({'syncOptions': 'conflict'}) @_state.set({'syncOptions': 'conflict'}).then( =>
@sync.init(args)
return @sync.storage.remove().then => ).then( =>
@sync.storage.remove()
).then( =>
@_state.set({'syncOptions': 'pristine'}) @_state.set({'syncOptions': 'pristine'})
)
checkOptionsSyncChange: ->
if @sync and @sync.enabled
@sync.checkChange()
module.exports = Options module.exports = Options

View File

@ -205,10 +205,22 @@ class OptionsSync
@_logOperations('OptionsSync::pull', operations) @_logOperations('OptionsSync::pull', operations)
local.apply(operations) local.apply(operations)
@storage.watch null, (changes) => @storage.watch null, (changes, opts = {}) =>
for own key, value of changes for own key, value of changes
pull[key] = value pull[key] = value
return if pullScheduled? return if pullScheduled?
if opts.immediately
doPull()
else
pullScheduled = setTimeout(doPull, @pullThrottle) pullScheduled = setTimeout(doPull, @pullThrottle)
checkChange: ->
@storage.checkChange({
immediately: true
force: true
})
init: (args) ->
@storage.init(args)
flush: ({data}) ->
@storage.flush({data})
module.exports = OptionsSync module.exports = OptionsSync

View File

@ -172,7 +172,7 @@ fieldset[disabled] .form-control {
position: relative; position: relative;
} }
.alert-success { .alert-success {
color: var(--primaryColor); color: var(--positiveColor);
background-color: transparent; background-color: transparent;
border-color: var(--lighterBackground); border-color: var(--lighterBackground);
position: relative; position: relative;
@ -185,7 +185,7 @@ fieldset[disabled] .form-control {
position: absolute; position: absolute;
inset: 0; inset: 0;
opacity: 0.1; opacity: 0.1;
background-color: var(--primaryColor); background-color: var(--positiveColor);
pointer-events: none; pointer-events: none;
} }
@ -429,3 +429,9 @@ main .page-header {
.sp-palette-container { .sp-palette-container {
border-right-color: var(--selectionBackground); border-right-color: var(--selectionBackground);
} }
.input-group-addon{
color: var(--defaultForeground);
background-color: var(--lighterBackground);
border-color: var(--lighterBackground);
}

View File

@ -1,12 +1,25 @@
angular.module('omega').controller 'AboutCtrl', ($scope, $rootScope, angular.module('omega').controller 'AboutCtrl', (
$modal, omegaDebug) -> $scope, $rootScope,$modal, omegaDebug
) ->
$scope.downloadLog = omegaDebug.downloadLog $scope.downloadLog = ->
$scope.reportIssue = omegaDebug.reportIssue $scope.logDownloading = true
Promise.resolve(omegaDebug.downloadLog()).then( ->
$scope.logDownloading = false
)
$scope.reportIssue = ->
$scope.issueReporting = true
omegaDebug.reportIssue().then( ->
$scope.issueReporting = false
)
$scope.showResetOptionsModal = -> $scope.showResetOptionsModal = ->
$modal.open(templateUrl: 'partials/reset_options_confirm.html').result $modal
.then -> omegaDebug.resetOptions() .open(templateUrl: 'partials/reset_options_confirm.html').result
.then ->
$scope.optionsReseting = true
omegaDebug.resetOptions().then( ->
$scope.optionsReseting = false
)
try try
$scope.version = omegaDebug.getProjectVersion() $scope.version = omegaDebug.getProjectVersion()

View File

@ -1,9 +1,22 @@
angular.module('omega').controller 'IoCtrl', ($scope, $rootScope, angular.module('omega').controller 'IoCtrl', (
$window, $http, omegaTarget, downloadFile) -> $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 if url
$scope.restoreOnlineUrl = 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 = -> $scope.exportOptions = ->
$rootScope.applyOptionsConfirm().then -> $rootScope.applyOptionsConfirm().then ->
@ -57,21 +70,59 @@ angular.module('omega').controller 'IoCtrl', ($scope, $rootScope,
), $scope.downloadError).finally -> ), $scope.downloadError).finally ->
$scope.restoringOnline = false $scope.restoringOnline = false
$scope.enableOptionsSync = (args) -> $scope.enableOptionsSync = (args = {}) ->
enable = -> 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() $window.location.reload()
).catch((e) ->
$scope.enableOptionsSyncing = false
$rootScope.showAlert(
type: 'error'
message: e + ''
)
console.log('error:::', e)
)
if args?.force if args?.force
enable() enable()
else else
$rootScope.applyOptionsConfirm().then enable $rootScope.applyOptionsConfirm().then enable
$scope.checkOptionsSyncChange = ->
$scope.enableOptionsSyncing = true
omegaTarget.checkOptionsSyncChange().then( ->
$window.location.reload()
)
$scope.disableOptionsSync = -> $scope.disableOptionsSync = ->
omegaTarget.setOptionsSync(false).then -> omegaTarget.setOptionsSync(false).then ->
$rootScope.applyOptionsConfirm().then -> $rootScope.applyOptionsConfirm().then ->
$window.location.reload() $window.location.reload()
$scope.resetOptionsSync = -> $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 -> $rootScope.applyOptionsConfirm().then ->
$window.location.reload() $window.location.reload()
).catch((e) ->
$rootScope.showAlert(
type: 'error'
message: e + ''
)
console.log('error:::', e)
)

View File

@ -90,9 +90,6 @@ angular.module('omega').controller 'MasterCtrl', ($scope, $rootScope, $window,
plainOptions = angular.fromJson(angular.toJson($rootScope.options)) plainOptions = angular.fromJson(angular.toJson($rootScope.options))
patch = diff.diff($rootScope.optionsOld, plainOptions) patch = diff.diff($rootScope.optionsOld, plainOptions)
omegaTarget.optionsPatch(patch).then -> omegaTarget.optionsPatch(patch).then ->
omegaTarget.state('customCss').then (customCss = '') ->
$scope.customCss = customCss
$rootScope.showAlert( $rootScope.showAlert(
type: 'success' type: 'success'
i18n: 'options_saveSuccess' i18n: 'options_saveSuccess'

View File

@ -14,17 +14,17 @@ section
p {{'about_app_description' | tr}} p {{'about_app_description' | tr}}
section section
p p
button.btn.btn-info(ng-click='reportIssue()') button.btn.btn-info(ng-click='reportIssue()' ladda='issueReporting')
span.glyphicon.glyphicon-comment span.glyphicon.glyphicon-comment
= ' ' = ' '
| {{'popup_reportIssues' | tr}} | {{'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 span.glyphicon.glyphicon-download
= ' ' = ' '
| {{'popup_errorLog' | tr}} | {{'popup_errorLog' | tr}}
= ' ' = ' '
button.btn.btn-danger(ng-click='showResetOptionsModal()') button.btn.btn-danger(ng-click='showResetOptionsModal()' ladda='optionsReseting')
span.glyphicon.glyphicon-alert span.glyphicon.glyphicon-alert
= ' ' = ' '
| {{'options_reset' | tr}} | {{'options_reset' | tr}}

View File

@ -38,15 +38,47 @@ section.settings-group
| {{'options_restoreOnlineSubmit' | tr}} | {{'options_restoreOnlineSubmit' | tr}}
section.settings-group section.settings-group
h3 {{'options_group_syncing' | tr}} 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"') div(ng-show='syncOptions == "pristine" || syncOptions == "disabled"')
p.help-block(omega-html='"options_syncPristineHelp" | tr') p.help-block(omega-html='"options_syncPristineHelp" | tr')
p 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 span.glyphicon.glyphicon-cloud-upload
= ' ' = ' '
| {{'options_syncEnable' | tr}} | {{'options_syncEnable' | tr}}
div(ng-show='syncOptions == "sync"') div(ng-show='syncOptions == "sync"')
p.alert.alert-success.width-limit 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 span.glyphicon.glyphicon-ok
= ' ' = ' '
| {{"options_syncSyncAlert" | tr}} | {{"options_syncSyncAlert" | tr}}
@ -57,13 +89,13 @@ section.settings-group
= ' ' = ' '
| {{'options_syncDisable' | tr}} | {{'options_syncDisable' | tr}}
div(ng-show='syncOptions == "conflict"') div(ng-show='syncOptions == "conflict"')
p.alert.alert-info.width-limit p.alert.alert-danger.width-limit
span.glyphicon.glyphicon-info-sign span.glyphicon.glyphicon-info-sign
= ' ' = ' '
| {{"options_syncConflictAlert" | tr}} | {{"options_syncConflictAlert" | tr}}
p.help-block(omega-html='"options_syncConflictHelp" | tr') p.help-block(omega-html='"options_syncConflictHelp" | tr')
p 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 span.glyphicon.glyphicon-cloud-download
= ' ' = ' '
| {{'options_syncEnableForce' | tr}} | {{'options_syncEnableForce' | tr}}