Closes #671.
Unified · Split
| @@ -1,9 +1,23 @@ | ||
| 1 | 1 | class app.Settings |
| 2 | − DOCS_KEY = 'docs' | |
| 3 | − DARK_KEY = 'dark' | |
| 4 | − LAYOUT_KEY = 'layout' | |
| 5 | − SIZE_KEY = 'size' | |
| 6 | − TIPS_KEY = 'tips' | |
| 2 | + PREFERENCE_KEYS = [ | |
| 3 | + 'hideDisabled' | |
| 4 | + 'hideIntro' | |
| 5 | + 'manualUpdate' | |
| 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 | 22 | @defaults: |
| 9 | 23 | count: 0 |
| @@ -32,24 +46,24 @@ class app.Settings · 22 unmodified lines | ||
| 32 | 46 | return |
| 33 | 47 | |
| 34 | 48 | hasDocs: -> |
| 35 | − try !!@store.get(DOCS_KEY) | |
| 49 | + try !!@store.get('docs') | |
| 36 | 50 | |
| 37 | 51 | getDocs: -> |
| 38 | − @store.get(DOCS_KEY)?.split('/') or app.config.default_docs | |
| 52 | + @store.get('docs')?.split('/') or app.config.default_docs | |
| 39 | 53 | |
| 40 | 54 | setDocs: (docs) -> |
| 41 | − @set DOCS_KEY, docs.join('/') | |
| 55 | + @set 'docs', docs.join('/') | |
| 42 | 56 | return |
| 43 | 57 | |
| 44 | 58 | getTips: -> |
| 45 | − @store.get(TIPS_KEY)?.split('/') or [] | |
| 59 | + @store.get('tips')?.split('/') or [] | |
| 46 | 60 | |
| 47 | 61 | setTips: (tips) -> |
| 48 | − @set TIPS_KEY, tips.join('/') | |
| 62 | + @set 'tips', tips.join('/') | |
| 49 | 63 | return |
| 50 | 64 | |
| 51 | 65 | setLayout: (name, enable) -> |
| 52 | − layout = (@store.get(LAYOUT_KEY) || '').split(' ') | |
| 66 | + layout = (@store.get('layout') || '').split(' ') | |
| 53 | 67 | $.arrayDelete(layout, '') |
| 54 | 68 | |
| 55 | 69 | if enable |
| @@ -58,22 +72,34 @@ class app.Settings · 2 unmodified lines | ||
| 58 | 72 | $.arrayDelete(layout, name) |
| 59 | 73 | |
| 60 | 74 | if layout.length > 0 |
| 61 | − @set LAYOUT_KEY, layout.join(' ') | |
| 75 | + @set 'layout', layout.join(' ') | |
| 62 | 76 | else |
| 63 | − @del LAYOUT_KEY | |
| 77 | + @del 'layout' | |
| 64 | 78 | return |
| 65 | 79 | |
| 66 | 80 | hasLayout: (name) -> |
| 67 | − layout = (@store.get(LAYOUT_KEY) || '').split(' ') | |
| 81 | + layout = (@store.get('layout') || '').split(' ') | |
| 68 | 82 | layout.indexOf(name) isnt -1 |
| 69 | 83 | |
| 70 | 84 | setSize: (value) -> |
| 71 | − @set SIZE_KEY, value | |
| 85 | + @set 'size', value | |
| 72 | 86 | return |
| 73 | 87 | |
| 74 | 88 | dump: -> |
| 75 | 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 | 103 | reset: -> |
| 78 | 104 | @store.reset() |
| 79 | 105 | @cache = {} |
| @@ -14,6 +14,7 @@ class @CookieStore · 13 unmodified lines | ||
| 14 | 14 | return |
| 15 | 15 | |
| 16 | 16 | value = 1 if value == true |
| 17 | + value = parseInt(value, 10) if value and INT.test?(value) | |
| 17 | 18 | Cookies.set(key, '' + value, path: '/', expires: 1e8) |
| 18 | 19 | @constructor.onBlocked(key, value, @get(key)) if @get(key) != value |
| 19 | 20 | return |
| @@ -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 | 6 | "2017-09-03", |
| 4 | 7 | "New documentations: <a href=\"/d/\">D</a>, <a href=\"/nim/\">Nim</a> and <a href=\"/vulkan/\">Vulkan</a>" |
| 5 | 8 | ], [ |
| @@ -30,6 +30,10 @@ app.templates.notifInvalidLocation = -> · 29 unmodified lines | ||
| 30 | 30 | textNotif """ DevDocs must be loaded from #{app.config.production_host} """, |
| 31 | 31 | """ Otherwise things are likely to break. """ |
| 32 | 32 | |
| 33 | +app.templates.notifImportInvalid = -> | |
| 34 | + textNotif """ Oops, an error occured. """, | |
| 35 | + """ The file you selected is invalid. """ | |
| 36 | + | |
| 33 | 37 | app.templates.notifNews = (news) -> |
| 34 | 38 | notif 'Changelog', """<div class="_notif-content _notif-news">#{app.templates.newsList(news, years: false)}</div>""" |
| 35 | 39 | |
| @@ -35,5 +35,10 @@ app.templates.settingsPage = (settings) -> """ · 34 unmodified lines | ||
| 35 | 35 | </div> |
| 36 | 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 | """ |
| @@ -5,6 +5,7 @@ class app.views.SettingsPage extends app.View · 4 unmodified lines | ||
| 5 | 5 | @className: '_static' |
| 6 | 6 | |
| 7 | 7 | @events: |
| 8 | + click: 'onClick' | |
| 8 | 9 | change: 'onChange' |
| 9 | 10 | |
| 10 | 11 | render: -> |
| @@ -46,6 +47,34 @@ class app.views.SettingsPage extends app.View · 35 unmodified lines | ||
| 46 | 47 | app.settings.set(name, enable) |
| 47 | 48 | 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 | 78 | onChange: (event) => |
| 50 | 79 | input = event.target |
| 51 | 80 | switch input.name |
| @@ -55,10 +84,20 @@ class app.views.SettingsPage extends app.View · 3 unmodified lines | ||
| 55 | 84 | @toggleLayout input.value, input.checked |
| 56 | 85 | when 'smoothScroll' |
| 57 | 86 | @toggleSmoothScroll input.checked |
| 87 | + when 'import' | |
| 88 | + @import input.files[0], input | |
| 58 | 89 | else |
| 59 | 90 | @toggle input.name, input.checked |
| 60 | 91 | 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 | 101 | onRoute: (context) -> |
| 63 | 102 | @render() |
| 64 | 103 | return |
| @@ -9,6 +9,7 @@ class app.views.Settings extends app.View · 8 unmodified lines | ||
| 9 | 9 | backBtn: 'button[data-back]' |
| 10 | 10 | |
| 11 | 11 | @events: |
| 12 | + import: 'onImport' | |
| 12 | 13 | change: 'onChange' |
| 13 | 14 | submit: 'onSubmit' |
| 14 | 15 | click: 'onClick' |
| @@ -41,11 +42,16 @@ class app.views.Settings extends app.View · 26 unmodified lines | ||
| 41 | 42 | @addClass '_in' |
| 42 | 43 | return |
| 43 | 44 | |
| 44 | − save: -> | |
| 45 | + save: (options = {}) -> | |
| 45 | 46 | unless @saving |
| 46 | 47 | @saving = true |
| 47 | − docs = @docPicker.getSelectedDocs() | |
| 48 | − app.settings.setDocs(docs) | |
| 48 | + | |
| 49 | + if options.import | |
| 50 | + docs = app.settings.getDocs() | |
| 51 | + else | |
| 52 | + docs = @docPicker.getSelectedDocs() | |
| 53 | + app.settings.setDocs(docs) | |
| 54 | + | |
| 49 | 55 | @saveBtn.textContent = if app.appCache then 'Downloading\u2026' else 'Saving\u2026' |
| 50 | 56 | disabledDocs = new app.collections.Docs(doc for doc in app.docs.all() when docs.indexOf(doc.slug) is -1) |
| 51 | 57 | disabledDocs.uninstall -> |
| @@ -66,6 +72,11 @@ class app.views.Settings extends app.View · 14 unmodified lines | ||
| 66 | 72 | @save() |
| 67 | 73 | return |
| 68 | 74 | |
| 75 | + onImport: => | |
| 76 | + @addClass('_dirty') | |
| 77 | + @save(import: true) | |
| 78 | + return | |
| 79 | + | |
| 69 | 80 | onClick: (event) => |
| 70 | 81 | return if event.which isnt 1 |
| 71 | 82 | if event.target is @backBtn |
| @@ -398,12 +398,16 @@ · 397 unmodified lines | ||
| 398 | 398 | } |
| 399 | 399 | |
| 400 | 400 | ._btn { |
| 401 | + display: inline-block; | |
| 402 | + vertical-align: top; | |
| 403 | + line-height: normal; | |
| 401 | 404 | white-space: nowrap; |
| 402 | 405 | padding: .375rem .675rem; |
| 403 | 406 | background-image: linear-gradient(lighten($boxBackground, 4%), darken($boxBackground, 2%)); |
| 404 | 407 | border: 1px solid $boxBorder; |
| 405 | 408 | border-radius: 3px; |
| 406 | 409 | box-shadow: 0 1px rgba($boxBorder, .08); |
| 410 | + cursor: pointer; | |
| 407 | 411 | |
| 408 | 412 | &:active { |
| 409 | 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 | 432 | ._btn-link { |
| 415 | 433 | line-height: inherit; |
| 416 | 434 | color: $linkColor; |