inline-update.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  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 icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
  22. icon.id = 'desktop-update-icon'
  23. icon.setAttribute('viewBox', '0 0 24 24')
  24. icon.setAttribute('aria-hidden', 'true')
  25. button.prepend(icon)
  26. const value = document.getElementById('desktop-update-value')
  27. const dot = document.getElementById('desktop-update-dot')
  28. const error = document.getElementById('desktop-update-error')
  29. let state, pending = false, closed = false, requested = false, reading = false, localError = ''
  30. let observer
  31. function stop() {
  32. closed = true; clearInterval(timer); detach(); observer?.disconnect()
  33. if (fallback) entry.remove()
  34. }
  35. function render() {
  36. if (!state || closed) return
  37. const busy = pending || ['checking', 'downloading', 'verifying', 'preparing', 'installing'].includes(state.phase)
  38. const percent = state.phase === 'downloading' && state.total > 0 ? Math.max(0, Math.min(100, Math.floor(state.received / state.total * 100))) : undefined
  39. const hasUpdate = state.available || ['downloading', 'verifying', 'preparing', 'ready', 'installing'].includes(state.phase) || (state.phase === 'failed' && state.latestVersion)
  40. entry.hidden = entry.classList.contains('desktop-update-toolbar') ? !hasUpdate
  41. : !hasUpdate && !requested && !localError && state.phase !== 'failed'
  42. label.textContent = state.phase === 'ready' ? state.installMode === 'restart' ? '重新启动' : '打开安装包'
  43. : state.phase === 'installing' ? '正在重启…'
  44. : state.phase === 'downloading' ? percent === undefined ? '正在下载…' : `正在下载 · ${percent}%`
  45. : ['verifying', 'preparing'].includes(state.phase) ? '正在准备…'
  46. : busy ? '正在检测…' : state.retryable === false ? '更新未完成'
  47. : state.available ? '可更新' : state.phase === 'failed' || localError ? '重试更新'
  48. : state.phase === 'checked' ? '已是最新版本' : '检测更新'
  49. button.disabled = busy || state.retryable === false
  50. button.setAttribute('aria-label', label.textContent)
  51. button.setAttribute('aria-busy', String(busy))
  52. button.title = localError || state.error || (state.phase === 'ready'
  53. ? state.installMode === 'restart' ? '更新已下载,点击重新启动完成安装' : '更新已下载,点击打开安装包'
  54. : busy ? label.textContent : state.available ? `App ${state.latestVersion || ''} 可更新,点击下载` : state.message)
  55. entry.title = button.title
  56. entry.classList.toggle('desktop-update-ready', state.phase === 'ready')
  57. icon.toggleAttribute('hidden', busy)
  58. icon.innerHTML = state.phase === 'ready' && state.installMode === 'restart'
  59. ? '<path d="M20 7v5h-5M4 17v-5h5M5.1 9a8 8 0 0 1 13-3l2 2M18.9 15a8 8 0 0 1-13 3l-2-2"/>'
  60. : '<path d="M12 3v12m-5-5 5 5 5-5M5 21h14"/>'
  61. ring.toggleAttribute('hidden', !busy)
  62. ring.classList.toggle('update-spinning', busy && percent === undefined)
  63. ring.setAttribute('role', busy ? 'progressbar' : 'img')
  64. ring.setAttribute('aria-label', label.textContent)
  65. if (busy) ring.removeAttribute('aria-hidden')
  66. else ring.setAttribute('aria-hidden', 'true')
  67. ring.setAttribute('aria-valuemin', '0'); ring.setAttribute('aria-valuemax', '100')
  68. if (percent === undefined) ring.removeAttribute('aria-valuenow')
  69. else ring.setAttribute('aria-valuenow', String(percent))
  70. value.setAttribute('stroke-dasharray', `${busy ? percent ?? 25 : 100} 100`)
  71. dot.hidden = !(state.available || state.phase === 'ready') || busy
  72. error.textContent = localError || state.error || ''; error.hidden = !error.textContent
  73. }
  74. async function refresh() {
  75. if (reading || closed) return
  76. if (fallback && document.querySelector('[data-desktop-update-inline]')) {
  77. stop()
  78. return
  79. }
  80. if (fallback) {
  81. const bell = document.querySelector('.desktop-sidebar .unread-entry')
  82. if (bell && bell.nextElementSibling !== entry) {
  83. bell.after(entry)
  84. entry.classList.remove('desktop-update-fallback')
  85. entry.classList.add('desktop-update-toolbar')
  86. }
  87. }
  88. reading = true
  89. try { state = await desktop.updateState(); render() } catch { /* Poll again after navigation settles. */ }
  90. finally { reading = false }
  91. }
  92. button.addEventListener('click', async () => {
  93. if (pending || !state || button.disabled) return
  94. const action = state.phase === 'ready' ? state.installMode === 'restart' ? 'install' : 'open' : state.available ? 'download' : 'check'
  95. pending = true; localError = ''; render()
  96. try {
  97. const operation = desktop.updateAction(action)
  98. void refresh()
  99. const next = await operation
  100. if (next) state = next
  101. } catch (cause) { localError = cause.message || '更新未完成,请重试' }
  102. finally { pending = false; render() }
  103. })
  104. const detach = desktop.onUpdateRequested(() => { requested = true; void refresh() })
  105. const timer = setInterval(() => void refresh(), 500)
  106. if (fallback) {
  107. observer = new MutationObserver(() => {
  108. if (document.querySelector('[data-desktop-update-inline]')) stop()
  109. })
  110. observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['data-desktop-update-inline'] })
  111. }
  112. window.addEventListener('unload', stop)
  113. void refresh()
  114. })()