Browse Source

Add ability export/import preferences

Closes #671.
Thibaut Courouble 8 years ago
parent
commit
44e6316100

+ 41 - 15
assets/javascripts/app/settings.coffee

@@ -1,9 +1,23 @@
 class app.Settings
-  DOCS_KEY = 'docs'
-  DARK_KEY = 'dark'
-  LAYOUT_KEY = 'layout'
-  SIZE_KEY = 'size'
-  TIPS_KEY = 'tips'
+  PREFERENCE_KEYS = [
+    'hideDisabled'
+    'hideIntro'
+    'manualUpdate'
+    'fastScroll'
+    'arrowScroll'
+    'docs'
+    'dark'
+    'layout'
+    'size'
+    'tips'
+  ]
+
+  INTERNAL_KEYS = [
+    'count'
+    'schema'
+    'version'
+    'news'
+  ]
 
   @defaults:
     count: 0
@@ -32,24 +46,24 @@ class app.Settings
     return
 
   hasDocs: ->
-    try !!@store.get(DOCS_KEY)
+    try !!@store.get('docs')
 
   getDocs: ->
-    @store.get(DOCS_KEY)?.split('/') or app.config.default_docs
+    @store.get('docs')?.split('/') or app.config.default_docs
 
   setDocs: (docs) ->
-    @set DOCS_KEY, docs.join('/')
+    @set 'docs', docs.join('/')
     return
 
   getTips: ->
-    @store.get(TIPS_KEY)?.split('/') or []
+    @store.get('tips')?.split('/') or []
 
   setTips: (tips) ->
-    @set TIPS_KEY, tips.join('/')
+    @set 'tips', tips.join('/')
     return
 
   setLayout: (name, enable) ->
-    layout = (@store.get(LAYOUT_KEY) || '').split(' ')
+    layout = (@store.get('layout') || '').split(' ')
     $.arrayDelete(layout, '')
 
     if enable
@@ -58,22 +72,34 @@ class app.Settings
       $.arrayDelete(layout, name)
 
     if layout.length > 0
-      @set LAYOUT_KEY, layout.join(' ')
+      @set 'layout', layout.join(' ')
     else
-      @del LAYOUT_KEY
+      @del 'layout'
     return
 
   hasLayout: (name) ->
-    layout = (@store.get(LAYOUT_KEY) || '').split(' ')
+    layout = (@store.get('layout') || '').split(' ')
     layout.indexOf(name) isnt -1
 
   setSize: (value) ->
-    @set SIZE_KEY, value
+    @set 'size', value
     return
 
   dump: ->
     @store.dump()
 
+  export: ->
+    data = @dump()
+    delete data[key] for key in INTERNAL_KEYS
+    data
+
+  import: (data) ->
+    for key, value of @export()
+      @del key unless data.hasOwnProperty(key)
+    for key, value of data
+      @set key, value if PREFERENCE_KEYS.indexOf(key) isnt -1
+    return
+
   reset: ->
     @store.reset()
     @cache = {}

+ 1 - 0
assets/javascripts/lib/cookie_store.coffee

@@ -14,6 +14,7 @@ class @CookieStore
       return
 
     value = 1 if value == true
+    value = parseInt(value, 10) if value and INT.test?(value)
     Cookies.set(key, '' + value, path: '/', expires: 1e8)
     @constructor.onBlocked(key, value, @get(key)) if @get(key) != value
     return

+ 3 - 0
assets/javascripts/news.json

@@ -1,5 +1,8 @@
 [
   [
+    "2017-09-10",
+    "<a href=\"/settings\">Preferences</a> can now be exported and imported."
+  ], [
     "2017-09-03",
     "New documentations: <a href=\"/d/\">D</a>, <a href=\"/nim/\">Nim</a> and <a href=\"/vulkan/\">Vulkan</a>"
   ], [

+ 4 - 0
assets/javascripts/templates/notif_tmpl.coffee

@@ -30,6 +30,10 @@ app.templates.notifInvalidLocation = ->
   textNotif """ DevDocs must be loaded from #{app.config.production_host} """,
             """ Otherwise things are likely to break. """
 
+app.templates.notifImportInvalid = ->
+  textNotif """ Oops, an error occured. """,
+            """ The file you selected is invalid. """
+
 app.templates.notifNews = (news) ->
   notif 'Changelog', """<div class="_notif-content _notif-news">#{app.templates.newsList(news, years: false)}</div>"""
 

+ 6 - 1
assets/javascripts/templates/pages/settings_tmpl.coffee

@@ -35,5 +35,10 @@ app.templates.settingsPage = (settings) -> """
     </div>
   </div>
 
-  <button type="button" class="_btn-link _reset-btn" data-behavior="reset">Reset all preferences and data</button>
+  <p class="_hide-on-mobile">
+    <button type="button" class="_btn" data-action="export">Export</button>
+    <label class="_btn _file-btn"><input type="file" form="settings" name="import" accept=".json">Import</label>
+
+  <p>
+    <button type="button" class="_btn-link _reset-btn" data-behavior="reset">Reset all preferences and data</button>
 """

+ 39 - 0
assets/javascripts/views/content/settings_page.coffee

@@ -5,6 +5,7 @@ class app.views.SettingsPage extends app.View
   @className: '_static'
 
   @events:
+    click: 'onClick'
     change: 'onChange'
 
   render: ->
@@ -46,6 +47,34 @@ class app.views.SettingsPage extends app.View
     app.settings.set(name, enable)
     return
 
+  export: ->
+    data = new Blob([JSON.stringify(app.settings.export())], type: 'application/json')
+    link = document.createElement('a')
+    link.href = URL.createObjectURL(data)
+    link.download = 'devdocs.json'
+    link.style.display = 'none'
+    document.body.appendChild(link)
+    link.click()
+    document.body.removeChild(link)
+    return
+
+  import: (file, input) ->
+    unless file and file.type is 'application/json'
+      new app.views.Notif 'ImportInvalid', autoHide: false
+      return
+
+    reader = new FileReader()
+    reader.onloadend = ->
+      data = try JSON.parse(reader.result)
+      unless data and data.constructor is Object
+        new app.views.Notif 'ImportInvalid', autoHide: false
+        return
+      app.settings.import(data)
+      $.trigger input.form, 'import'
+      return
+    reader.readAsText(file)
+    return
+
   onChange: (event) =>
     input = event.target
     switch input.name
@@ -55,10 +84,20 @@ class app.views.SettingsPage extends app.View
         @toggleLayout input.value, input.checked
       when 'smoothScroll'
         @toggleSmoothScroll input.checked
+      when 'import'
+        @import input.files[0], input
       else
         @toggle input.name, input.checked
     return
 
+  onClick: (event) =>
+    target = $.eventTarget(event)
+    switch target.getAttribute('data-action')
+      when 'export'
+        $.stopEvent(event)
+        @export()
+    return
+
   onRoute: (context) ->
     @render()
     return

+ 14 - 3
assets/javascripts/views/layout/settings.coffee

@@ -9,6 +9,7 @@ class app.views.Settings extends app.View
     backBtn: 'button[data-back]'
 
   @events:
+    import: 'onImport'
     change: 'onChange'
     submit: 'onSubmit'
     click: 'onClick'
@@ -41,11 +42,16 @@ class app.views.Settings extends app.View
     @addClass '_in'
     return
 
-  save: ->
+  save: (options = {}) ->
     unless @saving
       @saving = true
-      docs = @docPicker.getSelectedDocs()
-      app.settings.setDocs(docs)
+
+      if options.import
+        docs = app.settings.getDocs()
+      else
+        docs = @docPicker.getSelectedDocs()
+        app.settings.setDocs(docs)
+
       @saveBtn.textContent = if app.appCache then 'Downloading\u2026' else 'Saving\u2026'
       disabledDocs = new app.collections.Docs(doc for doc in app.docs.all() when docs.indexOf(doc.slug) is -1)
       disabledDocs.uninstall ->
@@ -66,6 +72,11 @@ class app.views.Settings extends app.View
     @save()
     return
 
+  onImport: =>
+    @addClass('_dirty')
+    @save(import: true)
+    return
+
   onClick: (event) =>
     return if event.which isnt 1
     if event.target is @backBtn

+ 18 - 0
assets/stylesheets/components/_content.scss

@@ -398,12 +398,16 @@
 }
 
 ._btn {
+  display: inline-block;
+  vertical-align: top;
+  line-height: normal;
   white-space: nowrap;
   padding: .375rem .675rem;
   background-image: linear-gradient(lighten($boxBackground, 4%), darken($boxBackground, 2%));
   border: 1px solid $boxBorder;
   border-radius: 3px;
   box-shadow: 0 1px rgba($boxBorder, .08);
+  cursor: pointer;
 
   &:active {
     background-color: $boxBackground;
@@ -411,6 +415,20 @@
   }
 }
 
+._file-btn {
+  position: relative;
+  overflow: hidden;
+
+  > input {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    visibility: hidden;
+  }
+}
+
 ._btn-link {
   line-height: inherit;
   color: $linkColor;