inline-update.js 5.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. // The startup/login page uses the same process-wide updater as the workspace.
  2. // It remains usable even when no server is reachable.
  3. (() => {
  4. const desktop = window.yaoyaoDesktop
  5. if (!desktop?.updateState || document.querySelector('[data-desktop-update-inline]') || window.__yaoyaoInlineUpdate) return
  6. window.__yaoyaoInlineUpdate = true
  7. let entry = document.getElementById('desktop-update-entry')
  8. const fallback = !entry
  9. if (fallback) {
  10. entry = document.createElement('section')
  11. entry.id = 'desktop-update-entry'
  12. entry.className = 'desktop-update-entry desktop-update-fallback'
  13. entry.hidden = true
  14. entry.setAttribute('aria-label', 'App 更新')
  15. entry.innerHTML = '<button id="desktop-update-action" type="button"><svg id="desktop-update-ring" viewBox="0 0 24 24" aria-hidden="true"><circle class="update-track" cx="12" cy="12" r="9"/><circle id="desktop-update-value" cx="12" cy="12" r="9" pathLength="100"/></svg><span id="desktop-update-label" aria-live="polite">检测更新</span><span id="desktop-update-dot" hidden aria-hidden="true"></span></button><p id="desktop-update-error" role="alert" hidden></p>'
  16. document.body.append(entry)
  17. }
  18. const button = document.getElementById('desktop-update-action')
  19. const label = document.getElementById('desktop-update-label')
  20. const ring = document.getElementById('desktop-update-ring')
  21. const value = document.getElementById('desktop-update-value')
  22. const dot = document.getElementById('desktop-update-dot')
  23. const error = document.getElementById('desktop-update-error')
  24. let state, pending = false, closed = false, requested = false, reading = false, localError = ''
  25. function render() {
  26. if (!state || closed) return
  27. const busy = pending || ['checking', 'downloading', 'verifying', 'preparing', 'installing'].includes(state.phase)
  28. const percent = state.phase === 'downloading' && state.total > 0 ? Math.max(0, Math.min(100, Math.floor(state.received / state.total * 100))) : undefined
  29. entry.hidden = !requested && !localError && !state.available && !['downloading', 'verifying', 'preparing', 'ready', 'installing', 'failed'].includes(state.phase)
  30. label.textContent = state.phase === 'ready' ? state.installMode === 'restart' ? '重新启动' : '打开安装包'
  31. : state.phase === 'installing' ? '正在重启…'
  32. : state.phase === 'downloading' ? percent === undefined ? '正在下载…' : `正在下载 · ${percent}%`
  33. : ['verifying', 'preparing'].includes(state.phase) ? '正在准备…'
  34. : busy ? '正在检测…' : state.retryable === false ? '更新未完成'
  35. : state.available ? '可更新' : state.phase === 'failed' || localError ? '重试更新'
  36. : state.phase === 'checked' ? '已是最新版本' : '检测更新'
  37. button.disabled = busy || state.retryable === false
  38. button.setAttribute('aria-label', label.textContent)
  39. button.setAttribute('aria-busy', String(busy))
  40. button.title = localError || state.error || state.message
  41. ring.classList.toggle('update-spinning', busy && percent === undefined)
  42. ring.setAttribute('role', busy ? 'progressbar' : 'img')
  43. ring.setAttribute('aria-label', label.textContent)
  44. if (busy) ring.removeAttribute('aria-hidden')
  45. else ring.setAttribute('aria-hidden', 'true')
  46. ring.setAttribute('aria-valuemin', '0'); ring.setAttribute('aria-valuemax', '100')
  47. if (percent === undefined) ring.removeAttribute('aria-valuenow')
  48. else ring.setAttribute('aria-valuenow', String(percent))
  49. value.setAttribute('stroke-dasharray', `${busy ? percent ?? 25 : 100} 100`)
  50. dot.hidden = !state.available || busy
  51. error.textContent = localError || state.error || ''; error.hidden = !error.textContent
  52. }
  53. async function refresh() {
  54. if (reading || closed) return
  55. if (fallback && document.querySelector('[data-desktop-update-inline]')) {
  56. closed = true; clearInterval(timer); detach(); entry.remove()
  57. return
  58. }
  59. if (fallback) {
  60. const sidebar = document.querySelector('.desktop-sidebar .sidebar-footer')
  61. if (sidebar && entry.parentElement !== sidebar) {
  62. sidebar.prepend(entry)
  63. entry.classList.replace('desktop-update-fallback', 'desktop-update-sidebar')
  64. }
  65. }
  66. reading = true
  67. try { state = await desktop.updateState(); render() } catch { /* Poll again after navigation settles. */ }
  68. finally { reading = false }
  69. }
  70. button.addEventListener('click', async () => {
  71. if (pending || !state || button.disabled) return
  72. const action = state.phase === 'ready' ? state.installMode === 'restart' ? 'install' : 'open' : state.available ? 'download' : 'check'
  73. pending = true; localError = ''; render()
  74. try {
  75. const operation = desktop.updateAction(action)
  76. void refresh()
  77. const next = await operation
  78. if (next) state = next
  79. } catch (cause) { localError = cause.message || '更新未完成,请重试' }
  80. finally { pending = false; render() }
  81. })
  82. const detach = desktop.onUpdateRequested(() => { requested = true; void refresh() })
  83. const timer = setInterval(() => void refresh(), 500)
  84. window.addEventListener('unload', () => { closed = true; clearInterval(timer); detach() })
  85. void refresh()
  86. })()