aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--background_scripts/settings.coffee44
-rw-r--r--background_scripts/sync.coffee136
-rw-r--r--manifest.json2
-rw-r--r--pages/options.coffee8
4 files changed, 177 insertions, 13 deletions
diff --git a/background_scripts/settings.coffee b/background_scripts/settings.coffee
index 0fe1e1bb..64fce308 100644
--- a/background_scripts/settings.coffee
+++ b/background_scripts/settings.coffee
@@ -7,17 +7,49 @@ root.Settings = Settings =
get: (key) ->
if (key of localStorage) then JSON.parse(localStorage[key]) else @defaults[key]
- set: (key, value) ->
+ # The doNotSync argument suppresses calls to chrome.storage.sync.* while running unit tests
+ set: (key, value, doNotSync) ->
# don't store the value if it is equal to the default, so we can change the defaults in the future
- if (value == @defaults[key])
- @clear(key)
- else
- localStorage[key] = JSON.stringify(value)
+ # warning: this test is always false for settings with numeric default values (such as scrollStepSize)
+ if ( value == @defaults[key] )
+ return @clear(key,doNotSync)
+ # don't update the key/value if it's unchanged; thereby suppressing unnecessary calls to chrome.storage
+ valueJSON = JSON.stringify value
+ if localStorage[key] == valueJSON
+ return localStorage[key]
+ # we have a new value: so update chrome.storage and localStorage
+ root.Sync.set key, valueJSON if root?.Sync?.set
+ localStorage[key] = valueJSON
- clear: (key) -> delete localStorage[key]
+ # The doNotSync argument suppresses calls to chrome.storage.sync.* while running unit tests
+ clear: (key, doNotSync) ->
+ if @has key
+ root.Sync.clear key if root?.Sync?.clear
+ delete localStorage[key]
has: (key) -> key of localStorage
+ # the postUpdateHooks handler below is called each time an option changes:
+ # either from options/options.coffee (when the options page is saved)
+ # or from background_scripts/sync.coffee (when an update propagates from chrome.storage)
+ #
+ # NOTE:
+ # this has been refactored and renamed from options.coffee(postSaveHooks):
+ # - refactored because it is now also called from sync.coffee
+ # - renamed because it is no longer associated only with "Save" operations
+ #
+ postUpdateHooks:
+ keyMappings: (value) ->
+ root.Commands.clearKeyMappingsAndSetDefaults()
+ root.Commands.parseCustomKeyMappings value
+ root.refreshCompletionKeysAfterMappingSave()
+
+ # postUpdateHooks convenience wrapper
+ doPostUpdateHook: (key, value) ->
+ if @postUpdateHooks[key]
+ @postUpdateHooks[key] value
+
+
# options/options.(coffee|html) only handle booleans and strings; therefore
# all defaults must be booleans or strings
defaults:
diff --git a/background_scripts/sync.coffee b/background_scripts/sync.coffee
new file mode 100644
index 00000000..d3a18b85
--- /dev/null
+++ b/background_scripts/sync.coffee
@@ -0,0 +1,136 @@
+
+#
+# * Sync.set() and Sync.clear() propagate local changes to chrome.storage.
+# * Sync.listener() listens for changes to chrome.storage and propagates those
+# changes to localStorage and into vimium's internal state.
+# * Sync.pull() polls chrome.storage at startup, similarly propagating changes
+# to localStorage and into vimium's internal state.
+#
+# Changes are propagated into vimium's state using the same mechanism that is
+# used when options are changed on the options page.
+#
+# The effect is best-effort synchronization of vimium options/settings between
+# chrome/vimium instances, whenever:
+# - chrome is logged in to the user's Google account, and
+# - chrome synchronization is enabled.
+#
+# NOTE:
+# Values handled within this module are ALWAYS already JSON.stringifed, so
+# they're always non-empty strings.
+#
+
+console.log ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"
+root = exports ? window
+root.Sync = Sync =
+
+ # ##################
+ # constants
+
+ debug: true
+ storage: chrome.storage.sync
+ doNotSync: [ "settingsVersion", "previousVersion" ]
+
+ init: ->
+ chrome.storage.onChanged.addListener (changes, area) -> Sync.listener changes, area
+ @pull()
+ @log "Sync.init()"
+
+ # asynchronous fetch from synced storage, called at startup
+ pull: ->
+ @storage.get null, (items) ->
+ Sync.log "pull callback: #{Sync.callbackStatus()}"
+ if not chrome.runtime.lastError
+ for own key, value of items
+ Sync.storeAndPropagate key, value
+
+ # asynchronous message from synced storage
+ listener: (changes, area) ->
+ @log "listener: #{area}"
+ for own key, change of changes
+ @storeAndPropagate key, change.newValue
+
+ # only ever called from asynchronous synced-storage callbacks (pull and listener)
+ storeAndPropagate: (key, value) ->
+ # must be JSON.stringifed or undefined
+ @checkHaveStringOrUndefined value
+ # ignore, if not accepting this key
+ if not @syncKey key
+ @log "callback ignoring: #{key}"
+ return
+ # ignore, if unchanged
+ if localStorage[key] == value
+ @log "callback unchanged: #{key}"
+ return
+
+ # ok: accept, store and propagate update
+ defaultValue = root.Settings.defaults[key]
+ defaultValueJSON = JSON.stringify(defaultValue) # could cache this to avoid repeated recalculation
+
+ if value && value != defaultValueJSON
+ # key/value has been changed to non-default value at remote instance
+ @log "callback set: #{key}=#{value}"
+ localStorage[key] = value
+ root.Settings.doPostUpdateHook key, JSON.parse(value)
+ else
+ # key has been reset to default value at remote instance
+ @log "callback clear: #{key}=#{value}"
+ delete localStorage[key]
+ root.Settings.doPostUpdateHook key, defaultValue
+
+ # only called synchronously from within vimium, never on a callback
+ # no need to propagate updates into the rest of vimium (because that will already have been handled externally)
+ set: (key, value) ->
+ # value must be JSON.stringifed
+ @checkHaveString value
+ if value
+ if @syncKey key
+ @storage.set @mkKeyValue(key,value), -> Sync.logCallback "DONE set", key
+ @log "set scheduled: #{key}=#{value}"
+ else
+ # unreachable? (because value is a JSON string)
+ @log "UNREACHABLE in Sync.set(): #{key}"
+ @clear key
+
+ # only called synchronously from within vimium, never on a callback
+ # no need to propagate updates into the rest of vimium (because that will already have been handled by externally)
+ clear: (key) ->
+ if @syncKey key
+ @storage.remove key, -> Sync.logCallback "DONE clear", key
+ @log "clear scheduled: #{key}"
+
+ # ##################
+ # utilities
+
+ syncKey: (key) ->
+ key not in @doNotSync
+
+ # there has to be a more elegant way to do this!
+ mkKeyValue: (key, value) ->
+ obj = {}
+ obj[key] = value
+ obj
+
+ # debugging messages
+ # disable these by setting root.Sync.debug to anything falsy
+ log: (msg) ->
+ console.log "sync debug: #{msg}" if @debug
+
+ logCallback: (where, key) ->
+ @log "#{where} callback: #{key} #{@callbackStatus()}"
+
+ callbackStatus: ->
+ if chrome.runtime.lastError then "ERROR: #{chrome.runtime.lastError.message}" else "(OK)"
+
+ checkHaveString: (thing) ->
+ if typeof(thing) != "string" or not thing
+ @log "sync.coffee: Yikes! this should be a non-empty string: #{typeof(thing)} #{thing}"
+
+ checkHaveStringOrUndefined: (thing) ->
+ if ( typeof(thing) != "string" and typeof(thing) != "undefined" ) or ( typeof(thing) == "string" and not thing )
+ @log "sync.coffee: Yikes! this should be a non-empty string or undefined: #{typeof(thing)} #{thing}"
+
+ # end of Sync object
+ # ##################
+
+Sync.init()
+
diff --git a/manifest.json b/manifest.json
index 8de7f009..5692fe75 100644
--- a/manifest.json
+++ b/manifest.json
@@ -11,6 +11,7 @@
"lib/utils.js",
"background_scripts/commands.js",
"lib/clipboard.js",
+ "background_scripts/sync.js",
"background_scripts/settings.js",
"background_scripts/completion.js",
"background_scripts/marks.js",
@@ -23,6 +24,7 @@
"bookmarks",
"history",
"clipboardRead",
+ "storage",
"<all_urls>"
],
"content_scripts": [
diff --git a/pages/options.coffee b/pages/options.coffee
index 117ce4a6..0aeb8e2d 100644
--- a/pages/options.coffee
+++ b/pages/options.coffee
@@ -8,12 +8,6 @@ editableFields = [ "scrollStepSize", "excludedUrls", "linkHintCharacters", "link
canBeEmptyFields = ["excludedUrls", "keyMappings", "userDefinedLinkHintCss"]
-postSaveHooks = keyMappings: (value) ->
- commands = chrome.extension.getBackgroundPage().Commands
- commands.clearKeyMappingsAndSetDefaults()
- commands.parseCustomKeyMappings value
- chrome.extension.getBackgroundPage().refreshCompletionKeysAfterMappingSave()
-
document.addEventListener "DOMContentLoaded", ->
populateOptions()
@@ -73,7 +67,7 @@ saveOptions = ->
bgSettings.set fieldName, fieldValue
$(fieldName).value = fieldValue
$(fieldName).setAttribute "savedValue", fieldValue
- postSaveHooks[fieldName] fieldValue if postSaveHooks[fieldName]
+ bgSettings.doPostUpdateHook fieldName, fieldValue
$("saveOptions").disabled = true