Unified · Split
| @@ -61,7 +61,7 @@ · 60 unmodified lines | |||
| 61 | bootOne: -> | 61 | bootOne: -> |
| 62 | @doc = new app.models.Doc @DOC | 62 | @doc = new app.models.Doc @DOC |
| 63 | @docs.reset [@doc] | 63 | @docs.reset [@doc] |
| 64 | @doc.load @start.bind(@), @onBootError.bind(@), readCache: true | 64 | @doc.load @bootDB.bind(@), @onBootError.bind(@), readCache: true |
| 65 | new app.views.Notice 'singleDoc', @doc | 65 | new app.views.Notice 'singleDoc', @doc |
| 66 | delete @DOC | 66 | delete @DOC |
| 67 | return | 67 | return |
| @@ -72,20 +72,25 @@ · 4 unmodified lines | |||
| 72 | (if docs.indexOf(doc.slug) >= 0 then @docs else @disabledDocs).add(doc) | 72 | (if docs.indexOf(doc.slug) >= 0 then @docs else @disabledDocs).add(doc) |
| 73 | @docs.sort() | 73 | @docs.sort() |
| 74 | @disabledDocs.sort() | 74 | @disabledDocs.sort() |
| 75 | @docs.load @start.bind(@), @onBootError.bind(@), readCache: true, writeCache: true | 75 | @docs.load @bootDB.bind(@), @onBootError.bind(@), readCache: true, writeCache: true |
| 76 | delete @DOCS | 76 | delete @DOCS |
| 77 | return | 77 | return |
| 78 | 78 | ||
| 79 | start: -> | 79 | bootDB: -> |
| 80 | for doc in @docs.all() | 80 | for doc in @docs.all() |
| 81 | @entries.add doc.toEntry() | 81 | @entries.add doc.toEntry() |
| 82 | @entries.add type.toEntry() for type in doc.types.all() | 82 | @entries.add type.toEntry() for type in doc.types.all() |
| 83 | @entries.add doc.entries.all() | 83 | @entries.add doc.entries.all() |
| 84 | |||
| 85 | @db = new app.DB() | ||
| 86 | @db.init(@start.bind(@)) | ||
| 87 | return | ||
| 88 | |||
| 89 | start: -> | ||
| 84 | @trigger 'ready' | 90 | @trigger 'ready' |
| 85 | @router.start() | 91 | @router.start() |
| 86 | @hideLoading() | 92 | @hideLoading() |
| 87 | @welcomeBack() unless @doc | 93 | @welcomeBack() unless @doc |
| 88 | |||
| 89 | @removeEvent 'ready bootError' | 94 | @removeEvent 'ready bootError' |
| 90 | return | 95 | return |
| 91 | 96 | ||
| @@ -8,3 +8,4 @@ app.config = · 7 unmodified lines | |||
| 8 | production_host: 'devdocs.io' | 8 | production_host: 'devdocs.io' |
| 9 | search_param: 'q' | 9 | search_param: 'q' |
| 10 | sentry_dsn: '<%= App.sentry_dsn %>' | 10 | sentry_dsn: '<%= App.sentry_dsn %>' |
| 11 | version: '<%= Time.now.to_i %>' | ||
| @@ -0,0 +1,115 @@ | |||
| 1 | class app.DB | ||
| 2 | NAME = 'docs' | ||
| 3 | |||
| 4 | constructor: -> | ||
| 5 | @useIndexedDB = @useIndexedDB() | ||
| 6 | |||
| 7 | init: (@_callback) -> | ||
| 8 | if @useIndexedDB | ||
| 9 | @initIndexedDB() | ||
| 10 | else | ||
| 11 | @callback() | ||
| 12 | return | ||
| 13 | |||
| 14 | initIndexedDB: -> | ||
| 15 | try | ||
| 16 | req = indexedDB.open(NAME, @indexedDBVersion()) | ||
| 17 | req.onerror = @callback | ||
| 18 | req.onsuccess = @onOpenSuccess | ||
| 19 | req.onupgradeneeded = @onUpgradeNeeded | ||
| 20 | catch | ||
| 21 | @callback() | ||
| 22 | return | ||
| 23 | |||
| 24 | isEnabled: -> | ||
| 25 | !!@db | ||
| 26 | |||
| 27 | callback: => | ||
| 28 | @_callback?() | ||
| 29 | @_callback = null | ||
| 30 | return | ||
| 31 | |||
| 32 | onOpenSuccess: (event) => | ||
| 33 | try | ||
| 34 | @db = event.target.result | ||
| 35 | @db.transaction(['docs', app.docs.all()[0].slug], 'readwrite').abort() # https://bugs.webkit.org/show_bug.cgi?id=136937 | ||
| 36 | catch | ||
| 37 | @db = null | ||
| 38 | |||
| 39 | @callback() | ||
| 40 | return | ||
| 41 | |||
| 42 | onUpgradeNeeded: (event) => | ||
| 43 | db = event.target.result | ||
| 44 | |||
| 45 | unless db.objectStoreNames.contains('docs') | ||
| 46 | db.createObjectStore('docs') | ||
| 47 | |||
| 48 | for doc in app.docs.all() when not db.objectStoreNames.contains(doc.slug) | ||
| 49 | db.createObjectStore(doc.slug) | ||
| 50 | |||
| 51 | for doc in app.disabledDocs.all() when not db.objectStoreNames.contains(doc.slug) | ||
| 52 | db.createObjectStore(doc.slug) | ||
| 53 | return | ||
| 54 | |||
| 55 | store: (doc, data, onSuccess, onError) -> | ||
| 56 | txn = @db.transaction ['docs', doc.slug], 'readwrite' | ||
| 57 | txn.oncomplete = -> if txn.error then onError() else onSuccess() | ||
| 58 | |||
| 59 | store = txn.objectStore(doc.slug) | ||
| 60 | store.clear() | ||
| 61 | store.add(content, path) for path, content of data | ||
| 62 | |||
| 63 | store = txn.objectStore('docs') | ||
| 64 | store.put(doc.mtime, doc.slug) | ||
| 65 | return | ||
| 66 | |||
| 67 | unstore: (doc, onSuccess, onError) -> | ||
| 68 | txn = @db.transaction ['docs', doc.slug], 'readwrite' | ||
| 69 | txn.oncomplete = -> if txn.error then onError() else onSuccess() | ||
| 70 | |||
| 71 | store = txn.objectStore(doc.slug) | ||
| 72 | store.clear() | ||
| 73 | |||
| 74 | store = txn.objectStore('docs') | ||
| 75 | store.delete(doc.slug) | ||
| 76 | return | ||
| 77 | |||
| 78 | version: (doc, callback) -> | ||
| 79 | txn = @db.transaction ['docs'], 'readonly' | ||
| 80 | store = txn.objectStore('docs') | ||
| 81 | |||
| 82 | req = store.get(doc.slug) | ||
| 83 | req.onsuccess = -> callback(!!req.result) | ||
| 84 | req.onerror = -> callback(false) | ||
| 85 | return | ||
| 86 | |||
| 87 | load: (entry, onSuccess, onError) -> | ||
| 88 | if @isEnabled() | ||
| 89 | onError = @loadWithXHR.bind(@, entry, onSuccess, onError) | ||
| 90 | @loadWithIDB(entry, onSuccess, onError) | ||
| 91 | else | ||
| 92 | @loadWithXHR(entry, onSuccess, onError) | ||
| 93 | |||
| 94 | loadWithXHR: (entry, onSuccess, onError) -> | ||
| 95 | ajax | ||
| 96 | url: entry.fileUrl() | ||
| 97 | dataType: 'html' | ||
| 98 | success: onSuccess | ||
| 99 | error: onError | ||
| 100 | |||
| 101 | loadWithIDB: (entry, onSuccess, onError) -> | ||
| 102 | txn = @db.transaction [entry.doc.slug], 'readonly' | ||
| 103 | store = txn.objectStore(entry.doc.slug) | ||
| 104 | |||
| 105 | req = store.get(entry.path) | ||
| 106 | req.onsuccess = -> if req.result then onSuccess(req.result) else onError() | ||
| 107 | req.onerror = onError | ||
| 108 | |||
| 109 | txn | ||
| 110 | |||
| 111 | useIndexedDB: -> | ||
| 112 | !app.isSingleDoc() and !!window.indexedDB | ||
| 113 | |||
| 114 | indexedDBVersion: -> | ||
| 115 | if app.config.env is 'production' then app.config.version else Date.now() / 1000 | ||
| @@ -4,6 +4,7 @@ class app.Router · 3 unmodified lines | |||
| 4 | @routes: [ | 4 | @routes: [ |
| 5 | ['*', 'before' ] | 5 | ['*', 'before' ] |
| 6 | ['/', 'root' ] | 6 | ['/', 'root' ] |
| 7 | ['/offline', 'offline' ] | ||
| 7 | ['/about', 'about' ] | 8 | ['/about', 'about' ] |
| 8 | ['/news', 'news' ] | 9 | ['/news', 'news' ] |
| 9 | ['/help', 'help' ] | 10 | ['/help', 'help' ] |
| @@ -75,6 +76,10 @@ class app.Router · 65 unmodified lines | |||
| 75 | @triggerRoute 'root' | 76 | @triggerRoute 'root' |
| 76 | return | 77 | return |
| 77 | 78 | ||
| 79 | offline: -> | ||
| 80 | @triggerRoute 'offline' | ||
| 81 | return | ||
| 82 | |||
| 78 | about: (context) -> | 83 | about: (context) -> |
| 79 | context.page = 'about' | 84 | context.page = 'about' |
| 80 | @triggerRoute 'page' | 85 | @triggerRoute 'page' |
| @@ -27,6 +27,9 @@ class app.models.Doc extends app.Model · 26 unmodified lines | |||
| 27 | fileUrl: (path) -> | 27 | fileUrl: (path) -> |
| 28 | "#{app.config.docs_host}#{@fullPath(path)}" | 28 | "#{app.config.docs_host}#{@fullPath(path)}" |
| 29 | 29 | ||
| 30 | dbUrl: -> | ||
| 31 | "#{app.config.docs_host}/#{@db_path}?#{@mtime}" | ||
| 32 | |||
| 30 | indexUrl: -> | 33 | indexUrl: -> |
| 31 | "#{app.indexHost()}/#{@index_path}?#{@mtime}" | 34 | "#{app.indexHost()}/#{@index_path}?#{@mtime}" |
| 32 | 35 | ||
| @@ -83,3 +86,39 @@ class app.models.Doc extends app.Model · 50 unmodified lines | |||
| 83 | _setCache: (data) -> | 86 | _setCache: (data) -> |
| 84 | app.store.set @slug, [@mtime, data] | 87 | app.store.set @slug, [@mtime, data] |
| 85 | return | 88 | return |
| 89 | |||
| 90 | download: (onSuccess, onError) -> | ||
| 91 | return if @downloading | ||
| 92 | @downloading = true | ||
| 93 | |||
| 94 | error = => | ||
| 95 | @downloading = null | ||
| 96 | onError() | ||
| 97 | |||
| 98 | success = (data) => | ||
| 99 | @downloading = null | ||
| 100 | app.db.store @, data, onSuccess, error | ||
| 101 | |||
| 102 | ajax | ||
| 103 | url: @dbUrl() | ||
| 104 | success: success | ||
| 105 | error: error | ||
| 106 | return | ||
| 107 | |||
| 108 | undownload: (onSuccess, onError) -> | ||
| 109 | return if @downloading | ||
| 110 | @downloading = true | ||
| 111 | |||
| 112 | success = => | ||
| 113 | @downloading = null | ||
| 114 | onSuccess() | ||
| 115 | |||
| 116 | error = => | ||
| 117 | @downloading = null | ||
| 118 | onError() | ||
| 119 | |||
| 120 | app.db.unstore @, success, error | ||
| 121 | |||
| 122 | getDownloadStatus: (callback) -> | ||
| 123 | app.db.version @, (value) -> | ||
| 124 | callback downloaded: !!value, version: value | ||
| @@ -41,8 +41,4 @@ class app.models.Entry extends app.Model · 40 unmodified lines | |||
| 41 | @doc.types.findBy 'name', @type | 41 | @doc.types.findBy 'name', @type |
| 42 | 42 | ||
| 43 | loadFile: (onSuccess, onError) -> | 43 | loadFile: (onSuccess, onError) -> |
| 44 | ajax | 44 | app.db.load(@, onSuccess, onError) |
| 45 | url: @fileUrl() | ||
| 46 | dataType: 'html' | ||
| 47 | success: onSuccess | ||
| 48 | error: onError | ||
| @@ -0,0 +1,16 @@ | |||
| 1 | app.templates.offlinePage = -> | ||
| 2 | """ <h1 class="_lined-heading">Offline</h1> | ||
| 3 | <table class="_docs"> | ||
| 4 | #{app.templates.render 'offlineDoc', app.docs.all()} | ||
| 5 | </table> """ | ||
| 6 | |||
| 7 | app.templates.offlineDoc = (doc) -> | ||
| 8 | """<tr data-slug="#{doc.slug}"></tr>""" | ||
| 9 | |||
| 10 | app.templates.offlineDocContent = (doc, status) -> | ||
| 11 | html = """<th class="_icon-#{doc.slug}">#{doc.name}</th>""" | ||
| 12 | html += if status.downloaded | ||
| 13 | """<td><a data-del>Delete</a></td>""" | ||
| 14 | else | ||
| 15 | """<td><a data-dl>Download</a></td>""" | ||
| 16 | html | ||
| @@ -23,10 +23,11 @@ class app.views.Content extends app.View · 22 unmodified lines | |||
| 23 | @scrollMap = {} | 23 | @scrollMap = {} |
| 24 | @scrollStack = [] | 24 | @scrollStack = [] |
| 25 | 25 | ||
| 26 | @rootPage = new app.views.RootPage | 26 | @rootPage = new app.views.RootPage |
| 27 | @staticPage = new app.views.StaticPage | 27 | @staticPage = new app.views.StaticPage |
| 28 | @typePage = new app.views.TypePage | 28 | @offlinePage = new app.views.OfflinePage |
| 29 | @entryPage = new app.views.EntryPage | 29 | @typePage = new app.views.TypePage |
| 30 | @entryPage = new app.views.EntryPage | ||
| 30 | 31 | ||
| 31 | @entryPage | 32 | @entryPage |
| 32 | .on 'loading', @onEntryLoading | 33 | .on 'loading', @onEntryLoading |
| @@ -137,6 +138,8 @@ class app.views.Content extends app.View · 104 unmodified lines | |||
| 137 | @show @entryPage | 138 | @show @entryPage |
| 138 | when 'type' | 139 | when 'type' |
| 139 | @show @typePage | 140 | @show @typePage |
| 141 | when 'offline' | ||
| 142 | @show @offlinePage | ||
| 140 | else | 143 | else |
| 141 | @show @staticPage | 144 | @show @staticPage |
| 142 | 145 | ||
| @@ -0,0 +1,63 @@ | |||
| 1 | class app.views.OfflinePage extends app.View | ||
| 2 | @className: '_static' | ||
| 3 | |||
| 4 | @events: | ||
| 5 | click: 'onClick' | ||
| 6 | |||
| 7 | @elements: | ||
| 8 | list: '_._docs' | ||
| 9 | |||
| 10 | deactivate: -> | ||
| 11 | if super | ||
| 12 | @empty() | ||
| 13 | return | ||
| 14 | |||
| 15 | render: -> | ||
| 16 | @html @tmpl('offlinePage') | ||
| 17 | @refreshElements() | ||
| 18 | app.docs.each(@renderDoc) | ||
| 19 | return | ||
| 20 | |||
| 21 | renderDoc: (doc) => | ||
| 22 | doc.getDownloadStatus (status) => | ||
| 23 | html = app.templates.render('offlineDocContent', doc, status) | ||
| 24 | el = @docEl(doc) | ||
| 25 | el.className = '' | ||
| 26 | el.innerHTML = html | ||
| 27 | return | ||
| 28 | |||
| 29 | getTitle: -> | ||
| 30 | 'Offline' | ||
| 31 | |||
| 32 | getDoc: (el) -> | ||
| 33 | el = el.parentNode until slug = el.getAttribute('data-slug') | ||
| 34 | app.docs.findBy('slug', slug) | ||
| 35 | |||
| 36 | docEl: (doc) -> | ||
| 37 | @find("[data-slug='#{doc.slug}']") | ||
| 38 | |||
| 39 | onRoute: -> | ||
| 40 | @render() | ||
| 41 | return | ||
| 42 | |||
| 43 | onClick: (event) => | ||
| 44 | if event.target.hasAttribute('data-dl') | ||
| 45 | action = 'download' | ||
| 46 | else if event.target.hasAttribute('data-del') | ||
| 47 | action = 'undownload' | ||
| 48 | |||
| 49 | if action | ||
| 50 | $.stopEvent(event) | ||
| 51 | doc = @getDoc(event.target) | ||
| 52 | doc[action](@onDownloadSuccess.bind(@, doc), @onDownloadError.bind(@, doc)) | ||
| 53 | @docEl(doc).classList.add("#{action}ing") | ||
| 54 | return | ||
| 55 | |||
| 56 | onDownloadSuccess: (doc) -> | ||
| 57 | @renderDoc(doc) | ||
| 58 | return | ||
| 59 | |||
| 60 | onDownloadError: (doc) -> | ||
| 61 | el = @docEl(doc) | ||
| 62 | el.className = '' | ||
| 63 | el.classList.add('error') | ||
| @@ -242,6 +242,51 @@ · 241 unmodified lines | |||
| 242 | td:first-child, td:last-child { white-space: nowrap; } | 242 | td:first-child, td:last-child { white-space: nowrap; } |
| 243 | } | 243 | } |
| 244 | 244 | ||
| 245 | // | ||
| 246 | // Doc table | ||
| 247 | // | ||
| 248 | |||
| 249 | ._docs { | ||
| 250 | width: 100%; | ||
| 251 | line-height: 1.5rem; | ||
| 252 | |||
| 253 | th { | ||
| 254 | max-width: 0; | ||
| 255 | padding-left: .5rem; | ||
| 256 | padding-right: .5rem; | ||
| 257 | white-space: nowrap; | ||
| 258 | font-weight: normal; | ||
| 259 | |||
| 260 | &:before { | ||
| 261 | float: left; | ||
| 262 | margin: .25rem .5rem .25rem 0; | ||
| 263 | @extend %icon; | ||
| 264 | } | ||
| 265 | } | ||
| 266 | |||
| 267 | td:last-child { text-align: right; } | ||
| 268 | td > a { cursor: pointer; } | ||
| 269 | |||
| 270 | tr.downloading > td:last-child { | ||
| 271 | > a { display: none; } | ||
| 272 | &:before { content: 'Downloading…' } | ||
| 273 | } | ||
| 274 | |||
| 275 | tr.undownloading > td:last-child { | ||
| 276 | > a { display: none; } | ||
| 277 | &:before { content: 'Deleting…' } | ||
| 278 | } | ||
| 279 | |||
| 280 | tr.error > td:last-child { | ||
| 281 | > a { display: none; } | ||
| 282 | |||
| 283 | &:before { | ||
| 284 | content: 'Error'; | ||
| 285 | color: red; | ||
| 286 | } | ||
| 287 | } | ||
| 288 | } | ||
| 289 | |||
| 245 | // | 290 | // |
| 246 | // News | 291 | // News |
| 247 | // | 292 | // |
| @@ -122,7 +122,7 @@ class App < Sinatra::Application · 121 unmodified lines | |||
| 122 | erb :index | 122 | erb :index |
| 123 | end | 123 | end |
| 124 | 124 | ||
| 125 | %w(about news help).each do |page| | 125 | %w(offline about news help).each do |page| |
| 126 | get "/#{page}" do | 126 | get "/#{page}" do |
| 127 | redirect "/#/#{page}", 302 | 127 | redirect "/#/#{page}", 302 |
| 128 | end | 128 | end |