aboutsummaryrefslogtreecommitdiffstats
path: root/lib/settings.coffee
blob: 5bbc9719b2dd04f9e4b1e62b59f788c6ae00a745 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
#
# * Sync.set() and Sync.clear() propagate local changes to chrome.storage.sync.
# * Sync.handleStorageUpdate() listens for changes to chrome.storage.sync and propagates those
#   changes to localStorage and into vimium's internal state.
# * Sync.fetchAsync() polls chrome.storage.sync at startup, similarly propagating
#   changes to localStorage and into vimium's internal state.
#
# The effect is best-effort synchronization of vimium options/settings between
# chrome/vimium instances.
#
# NOTE:
#   Values handled within this module are ALWAYS already JSON.stringifed, so
#   they're always non-empty strings.
#

root = exports ? window
Sync =

  storage: chrome.storage.sync
  doNotSync: ["settingsVersion", "previousVersion"]

  # This is called in main.coffee.
  init: ->
    chrome.storage.onChanged.addListener (changes, area) -> Sync.handleStorageUpdate changes, area
    @fetchAsync()

  # Asynchronous fetch from synced storage, called only at startup.
  fetchAsync: ->
    @storage.get null, (items) =>
      unless chrome.runtime.lastError
        for own key, value of items
          Settings.storeAndPropagate key, value if @shouldSyncKey key
        Settings.isLoaded = true
        unless isPreloaded
          listener() while listener = Settings.eventListeners.load?.pop()

  # Asynchronous message from synced storage.
  handleStorageUpdate: (changes, area) ->
    for own key, change of changes
      Settings.storeAndPropagate key, change?.newValue if @shouldSyncKey key

  # Only called synchronously from within vimium, never on a callback.
  # No need to propagate updates to the rest of vimium, that's already been done.
  set: (key, value) ->
    if @shouldSyncKey key
      setting = {}; setting[key] = value
      @storage.set setting

  # Only called synchronously from within vimium, never on a callback.
  clear: (key) ->
    @storage.remove key if @shouldSyncKey key

  # Should we synchronize this key?
  shouldSyncKey: (key) -> key not in @doNotSync

#
# Used by all parts of Vimium to manipulate localStorage.
#

# Select the object to use as the cache for settings.
if Utils.isExtensionPage()
  if Utils.isBackgroundPage()
    settingsCache = localStorage
    isPreloaded = true
  else
    settingsCache = extend {}, localStorage # Make a copy of the cached settings from localStorage
    isPreloaded = true
else
  settingsCache = {}
  isPreloaded = false

root.Settings = Settings =
  isLoaded: isPreloaded
  cache: settingsCache
  eventListeners: {}

  init: ->
    Sync.init()
    if isPreloaded
      listener() while listener = Settings.eventListeners.load?.pop()

  get: (key) ->
    console.log "WARNING: Settings have not loaded yet; using the default value for #{key}." unless @isLoaded
    if key of @cache and @cache[key]? then JSON.parse(@cache[key]) else @defaults[key]

  set: (key, value) ->
    # 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
      jsonValue = JSON.stringify value
      @cache[key] = jsonValue
      Sync.set key, jsonValue

  clear: (key) ->
    if @has key
      delete @cache[key]
    Sync.clear key

  has: (key) -> key of @cache

  addEventListener: (eventName, callback) ->
    (@eventListeners[eventName] ||= []).push callback

  # For settings which require action when their value changes, add hooks to this object, to be called from
  # options/options.coffee (when the options page is saved), and by Settings.storeAndPropagate (when an
  # update propagates from chrome.storage.sync).
  postUpdateHooks: {}

  # postUpdateHooks convenience wrapper
  performPostUpdateHook: (key, value) ->
    @postUpdateHooks[key]? value

  # Only ever called from asynchronous synced-storage callbacks (fetchAsync and handleStorageUpdate).
  storeAndPropagate: (key, value) ->
    return unless key of @defaults
    return if value and key of @cache and @cache[key] is value
    defaultValue = @defaults[key]
    defaultValueJSON = JSON.stringify(defaultValue)

    if value and value != defaultValueJSON
      # Key/value has been changed to non-default value at remote instance.
      @cache[key] = value
      @performPostUpdateHook key, JSON.parse(value)
    else
      # Key has been reset to default value at remote instance.
      if key of @cache
        delete @cache[key]
      @performPostUpdateHook key, defaultValue

  # options.coffee and options.html only handle booleans and strings; therefore all defaults must be booleans
  # or strings
  defaults:
    scrollStepSize: 60
    smoothScroll: true
    keyMappings: "# Insert your preferred key mappings here."
    linkHintCharacters: "sadfjklewcmpgh"
    linkHintNumbers: "0123456789"
    filterLinkHints: false
    hideHud: false
    userDefinedLinkHintCss:
      """
      div > .vimiumHintMarker {
      /* linkhint boxes */
      background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#FFF785),
        color-stop(100%,#FFC542));
      border: 1px solid #E3BE23;
      }

      div > .vimiumHintMarker span {
      /* linkhint text */
      color: black;
      font-weight: bold;
      font-size: 12px;
      }

      div > .vimiumHintMarker > .matchingCharacter {
      }
      """
    # Default exclusion rules.
    exclusionRules:
      [
        # Disable Vimium on Gmail.
        { pattern: "http*://mail.google.com/*", passKeys: "" }
      ]

    # NOTE: If a page contains both a single angle-bracket link and a double angle-bracket link, then in
    # most cases the single bracket link will be "prev/next page" and the double bracket link will be
    # "first/last page", so we put the single bracket first in the pattern string so that it gets searched
    # for first.

    # "\bprev\b,\bprevious\b,\bback\b,<,←,«,≪,<<"
    previousPatterns: "prev,previous,back,<,\u2190,\xab,\u226a,<<"
    # "\bnext\b,\bmore\b,>,→,»,≫,>>"
    nextPatterns: "next,more,>,\u2192,\xbb,\u226b,>>"
    # default/fall back search engine
    searchUrl: "https://www.google.com/search?q="
    # put in an example search engine
    searchEngines: [
      "w: http://www.wikipedia.org/w/index.php?title=Special:Search&search=%s Wikipedia"
      ""
      "# More examples."
      "#"
      "# (Vimium has built-in completion for these.)"
      "#"
      "# g: http://www.google.com/search?q=%s Google"
      "# l: http://www.google.com/search?q=%s&btnI I'm feeling lucky..."
      "# y: http://www.youtube.com/results?search_query=%s Youtube"
      "# b: https://www.bing.com/search?q=%s Bing"
      "# d: https://duckduckgo.com/?q=%s DuckDuckGo"
      "# az: http://www.amazon.com/s/?field-keywords=%s Amazon"
      "#"
      "# Another example (for Vimium does not have completion)."
      "#"
      "# m: https://www.google.com/maps/search/%s Google Maps"
      ].join "\n"
    newTabUrl: "chrome://newtab"
    grabBackFocus: false

    settingsVersion: Utils.getCurrentVersion()
    helpDialog_showAdvancedCommands: false

# Export Sync via Settings for tests.
root.Settings.Sync = Sync

# Perform migration from old settings versions, if this is the background page.
if Utils.isBackgroundPage()

  # We use settingsVersion to coordinate any necessary schema changes.
  if Utils.compareVersions("1.42", Settings.get("settingsVersion")) != -1
    Settings.set("scrollStepSize", parseFloat Settings.get("scrollStepSize"))
  Settings.set("settingsVersion", Utils.getCurrentVersion())

  # Migration (after 1.49, 2015/2/1).
  # Legacy setting: findModeRawQuery (a string).
  # New setting: findModeRawQueryList (a list of strings), now stored in chrome.storage.local (not localStorage).
  chrome.storage.local.get "findModeRawQueryList", (items) ->
    unless chrome.runtime.lastError or items.findModeRawQueryList
      rawQuery = Settings.get "findModeRawQuery"
      chrome.storage.local.set findModeRawQueryList: (if rawQuery then [ rawQuery ] else [])