History

Add ability export/import preferences

Closes #671.

Thibaut Courouble committed 9 years ago

Commit 44e63161 parent a16059a8 files · +126 −19Browse files

Unified · Split

8 files
  1. assets/javascripts/app/settings.coffee +41 −15
  2. assets/javascripts/lib/cookie_store.coffee +1 −0
  3. assets/javascripts/news.json +3 −0
  4. assets/javascripts/templates/notif_tmpl.coffee +4 −0
  5. assets/javascripts/templates/pages/settings_tmpl.coffee +6 −1
  6. assets/javascripts/views/content/settings_page.coffee +39 −0
  7. assets/javascripts/views/layout/settings.coffee +14 −3
  8. assets/stylesheets/components/_content.scss +18 −0
assets/javascripts/app/settings.coffee+41−15
@@ -1,9 +1,23 @@
1class app.Settings1class app.Settings
2 DOCS_KEY = 'docs'2 PREFERENCE_KEYS = [
3 DARK_KEY = 'dark'3 'hideDisabled'
4 LAYOUT_KEY = 'layout'4 'hideIntro'
5 SIZE_KEY = 'size'5 'manualUpdate'
6 TIPS_KEY = 'tips'6 'fastScroll'
7 'arrowScroll'
8 'docs'
9 'dark'
10 'layout'
11 'size'
12 'tips'
13 ]
14
15 INTERNAL_KEYS = [
16 'count'
17 'schema'
18 'version'
19 'news'
20 ]
7 21
8 @defaults:22 @defaults:
9 count: 023 count: 0
@@ -32,24 +46,24 @@ class app.Settings · 22 unmodified lines
32 return46 return
33 47
34 hasDocs: ->48 hasDocs: ->
35 try !!@store.get(DOCS_KEY)49 try !!@store.get('docs')
36 50
37 getDocs: ->51 getDocs: ->
38 @store.get(DOCS_KEY)?.split('/') or app.config.default_docs52 @store.get('docs')?.split('/') or app.config.default_docs
39 53
40 setDocs: (docs) ->54 setDocs: (docs) ->
41 @set DOCS_KEY, docs.join('/')55 @set 'docs', docs.join('/')
42 return56 return
43 57
44 getTips: ->58 getTips: ->
45 @store.get(TIPS_KEY)?.split('/') or []59 @store.get('tips')?.split('/') or []
46 60
47 setTips: (tips) ->61 setTips: (tips) ->
48 @set TIPS_KEY, tips.join('/')62 @set 'tips', tips.join('/')
49 return63 return
50 64
51 setLayout: (name, enable) ->65 setLayout: (name, enable) ->
52 layout = (@store.get(LAYOUT_KEY) || '').split(' ')66 layout = (@store.get('layout') || '').split(' ')
53 $.arrayDelete(layout, '')67 $.arrayDelete(layout, '')
54 68
55 if enable69 if enable
@@ -58,22 +72,34 @@ class app.Settings · 2 unmodified lines
58 $.arrayDelete(layout, name)72 $.arrayDelete(layout, name)
59 73
60 if layout.length > 074 if layout.length > 0
61 @set LAYOUT_KEY, layout.join(' ')75 @set 'layout', layout.join(' ')
62 else76 else
63 @del LAYOUT_KEY77 @del 'layout'
64 return78 return
65 79
66 hasLayout: (name) ->80 hasLayout: (name) ->
67 layout = (@store.get(LAYOUT_KEY) || '').split(' ')81 layout = (@store.get('layout') || '').split(' ')
68 layout.indexOf(name) isnt -182 layout.indexOf(name) isnt -1
69 83
70 setSize: (value) ->84 setSize: (value) ->
71 @set SIZE_KEY, value85 @set 'size', value
72 return86 return
73 87
74 dump: ->88 dump: ->
75 @store.dump()89 @store.dump()
76 90
91 export: ->
92 data = @dump()
93 delete data[key] for key in INTERNAL_KEYS
94 data
95
96 import: (data) ->
97 for key, value of @export()
98 @del key unless data.hasOwnProperty(key)
99 for key, value of data
100 @set key, value if PREFERENCE_KEYS.indexOf(key) isnt -1
101 return
102
77 reset: ->103 reset: ->
78 @store.reset()104 @store.reset()
79 @cache = {}105 @cache = {}
assets/javascripts/lib/cookie_store.coffee+1−0
@@ -14,6 +14,7 @@ class @CookieStore · 13 unmodified lines
14 return14 return
15 15
16 value = 1 if value == true16 value = 1 if value == true
17 value = parseInt(value, 10) if value and INT.test?(value)
17 Cookies.set(key, '' + value, path: '/', expires: 1e8)18 Cookies.set(key, '' + value, path: '/', expires: 1e8)
18 @constructor.onBlocked(key, value, @get(key)) if @get(key) != value19 @constructor.onBlocked(key, value, @get(key)) if @get(key) != value
19 return20 return
assets/javascripts/news.json+3−0
@@ -1,5 +1,8 @@
1[1[
2 [2 [
3 "2017-09-10",
4 "<a href=\"/settings\">Preferences</a> can now be exported and imported."
5 ], [
3 "2017-09-03",6 "2017-09-03",
4 "New documentations: <a href=\"/d/\">D</a>, <a href=\"/nim/\">Nim</a> and <a href=\"/vulkan/\">Vulkan</a>"7 "New documentations: <a href=\"/d/\">D</a>, <a href=\"/nim/\">Nim</a> and <a href=\"/vulkan/\">Vulkan</a>"
5 ], [8 ], [
assets/javascripts/templates/notif_tmpl.coffee+4−0
@@ -30,6 +30,10 @@ app.templates.notifInvalidLocation = -> · 29 unmodified lines
30 textNotif """ DevDocs must be loaded from #{app.config.production_host} """,30 textNotif """ DevDocs must be loaded from #{app.config.production_host} """,
31 """ Otherwise things are likely to break. """31 """ Otherwise things are likely to break. """
32 32
33app.templates.notifImportInvalid = ->
34 textNotif """ Oops, an error occured. """,
35 """ The file you selected is invalid. """
36
33app.templates.notifNews = (news) ->37app.templates.notifNews = (news) ->
34 notif 'Changelog', """<div class="_notif-content _notif-news">#{app.templates.newsList(news, years: false)}</div>"""38 notif 'Changelog', """<div class="_notif-content _notif-news">#{app.templates.newsList(news, years: false)}</div>"""
35 39
assets/javascripts/templates/pages/settings_tmpl.coffee+6−1
@@ -35,5 +35,10 @@ app.templates.settingsPage = (settings) -> """ · 34 unmodified lines
35 </div>35 </div>
36 </div>36 </div>
37 37
38 <button type="button" class="_btn-link _reset-btn" data-behavior="reset">Reset all preferences and data</button>38 <p class="_hide-on-mobile">
39 <button type="button" class="_btn" data-action="export">Export</button>
40 <label class="_btn _file-btn"><input type="file" form="settings" name="import" accept=".json">Import</label>
41
42 <p>
43 <button type="button" class="_btn-link _reset-btn" data-behavior="reset">Reset all preferences and data</button>
39"""44"""
assets/javascripts/views/content/settings_page.coffee+39−0
@@ -5,6 +5,7 @@ class app.views.SettingsPage extends app.View · 4 unmodified lines
5 @className: '_static'5 @className: '_static'
6 6
7 @events:7 @events:
8 click: 'onClick'
8 change: 'onChange'9 change: 'onChange'
9 10
10 render: ->11 render: ->
@@ -46,6 +47,34 @@ class app.views.SettingsPage extends app.View · 35 unmodified lines
46 app.settings.set(name, enable)47 app.settings.set(name, enable)
47 return48 return
48 49
50 export: ->
51 data = new Blob([JSON.stringify(app.settings.export())], type: 'application/json')
52 link = document.createElement('a')
53 link.href = URL.createObjectURL(data)
54 link.download = 'devdocs.json'
55 link.style.display = 'none'
56 document.body.appendChild(link)
57 link.click()
58 document.body.removeChild(link)
59 return
60
61 import: (file, input) ->
62 unless file and file.type is 'application/json'
63 new app.views.Notif 'ImportInvalid', autoHide: false
64 return
65
66 reader = new FileReader()
67 reader.onloadend = ->
68 data = try JSON.parse(reader.result)
69 unless data and data.constructor is Object
70 new app.views.Notif 'ImportInvalid', autoHide: false
71 return
72 app.settings.import(data)
73 $.trigger input.form, 'import'
74 return
75 reader.readAsText(file)
76 return
77
49 onChange: (event) =>78 onChange: (event) =>
50 input = event.target79 input = event.target
51 switch input.name80 switch input.name
@@ -55,10 +84,20 @@ class app.views.SettingsPage extends app.View · 3 unmodified lines
55 @toggleLayout input.value, input.checked84 @toggleLayout input.value, input.checked
56 when 'smoothScroll'85 when 'smoothScroll'
57 @toggleSmoothScroll input.checked86 @toggleSmoothScroll input.checked
87 when 'import'
88 @import input.files[0], input
58 else89 else
59 @toggle input.name, input.checked90 @toggle input.name, input.checked
60 return91 return
61 92
93 onClick: (event) =>
94 target = $.eventTarget(event)
95 switch target.getAttribute('data-action')
96 when 'export'
97 $.stopEvent(event)
98 @export()
99 return
100
62 onRoute: (context) ->101 onRoute: (context) ->
63 @render()102 @render()
64 return103 return
assets/javascripts/views/layout/settings.coffee+14−3
@@ -9,6 +9,7 @@ class app.views.Settings extends app.View · 8 unmodified lines
9 backBtn: 'button[data-back]'9 backBtn: 'button[data-back]'
10 10
11 @events:11 @events:
12 import: 'onImport'
12 change: 'onChange'13 change: 'onChange'
13 submit: 'onSubmit'14 submit: 'onSubmit'
14 click: 'onClick'15 click: 'onClick'
@@ -41,11 +42,16 @@ class app.views.Settings extends app.View · 26 unmodified lines
41 @addClass '_in'42 @addClass '_in'
42 return43 return
43 44
44 save: ->45 save: (options = {}) ->
45 unless @saving46 unless @saving
46 @saving = true47 @saving = true
47 docs = @docPicker.getSelectedDocs()48
48 app.settings.setDocs(docs)49 if options.import
50 docs = app.settings.getDocs()
51 else
52 docs = @docPicker.getSelectedDocs()
53 app.settings.setDocs(docs)
54
49 @saveBtn.textContent = if app.appCache then 'Downloading\u2026' else 'Saving\u2026'55 @saveBtn.textContent = if app.appCache then 'Downloading\u2026' else 'Saving\u2026'
50 disabledDocs = new app.collections.Docs(doc for doc in app.docs.all() when docs.indexOf(doc.slug) is -1)56 disabledDocs = new app.collections.Docs(doc for doc in app.docs.all() when docs.indexOf(doc.slug) is -1)
51 disabledDocs.uninstall ->57 disabledDocs.uninstall ->
@@ -66,6 +72,11 @@ class app.views.Settings extends app.View · 14 unmodified lines
66 @save()72 @save()
67 return73 return
68 74
75 onImport: =>
76 @addClass('_dirty')
77 @save(import: true)
78 return
79
69 onClick: (event) =>80 onClick: (event) =>
70 return if event.which isnt 181 return if event.which isnt 1
71 if event.target is @backBtn82 if event.target is @backBtn
assets/stylesheets/components/_content.scss+18−0
@@ -398,12 +398,16 @@ · 397 unmodified lines
398}398}
399 399
400._btn {400._btn {
401 display: inline-block;
402 vertical-align: top;
403 line-height: normal;
401 white-space: nowrap;404 white-space: nowrap;
402 padding: .375rem .675rem;405 padding: .375rem .675rem;
403 background-image: linear-gradient(lighten($boxBackground, 4%), darken($boxBackground, 2%));406 background-image: linear-gradient(lighten($boxBackground, 4%), darken($boxBackground, 2%));
404 border: 1px solid $boxBorder;407 border: 1px solid $boxBorder;
405 border-radius: 3px;408 border-radius: 3px;
406 box-shadow: 0 1px rgba($boxBorder, .08);409 box-shadow: 0 1px rgba($boxBorder, .08);
410 cursor: pointer;
407 411
408 &:active {412 &:active {
409 background-color: $boxBackground;413 background-color: $boxBackground;
@@ -411,6 +415,20 @@ · 1 unmodified line
411 }415 }
412}416}
413 417
418._file-btn {
419 position: relative;
420 overflow: hidden;
421
422 > input {
423 position: absolute;
424 top: 0;
425 left: 0;
426 width: 100%;
427 height: 100%;
428 visibility: hidden;
429 }
430}
431
414._btn-link {432._btn-link {
415 line-height: inherit;433 line-height: inherit;
416 color: $linkColor;434 color: $linkColor;