boot.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. const api = window.yaoyaoDesktop
  2. const $ = id => document.getElementById(id)
  3. let state, initialized = false, pending = false, serverDirty = false, generation = 0, actionError = ''
  4. function display(next) {
  5. state = next
  6. if (!initialized) {
  7. $('server').value = state.serverURL || ''
  8. $('remember').checked = state.remember
  9. initialized = true
  10. }
  11. const busy = pending || state.busy
  12. const local = state.mode === 'local'
  13. const ready = ['ready', 'authenticating', 'entering'].includes(state.phase) && !serverDirty
  14. const setup = ready && state.setupRequired
  15. const accountTitle = setup ? '创建管理员' : '登录账号'
  16. $('mode-options').disabled = busy
  17. for (const input of document.querySelectorAll('[name=mode]')) input.checked = input.value === state.mode
  18. $('remember').disabled = busy
  19. $('prepare-section').classList.toggle('pending', !state.mode)
  20. $('prepare-section').classList.toggle('local-ready', local && ready)
  21. $('prepare-heading').textContent = local ? '准备本机服务' : state.mode ? '连接服务器' : '准备服务'
  22. $('choose-hint').hidden = !!state.mode
  23. $('local-panel').hidden = !local
  24. $('connection-form').hidden = state.mode !== 'remote'
  25. $('server').disabled = busy
  26. $('detect').disabled = busy || !$('server').value.trim()
  27. $('detect').textContent = busy && !ready ? '正在检测…' : ready ? '重新检测' : '检测连接'
  28. $('prepare-local').hidden = !local || state.phase !== 'idle'
  29. $('prepare-local').disabled = busy
  30. $('actions').hidden = !local || state.phase !== 'error'
  31. $('retry').disabled = busy
  32. $('force-sync').hidden = !state.canForceSync
  33. $('force-sync').disabled = busy
  34. const preparationError = !ready && !serverDirty && (actionError || state.error)
  35. $('status').textContent = preparationError || (serverDirty ? '服务器地址已更改,请重新检测。' : state.message)
  36. $('status').setAttribute('role', preparationError ? 'alert' : 'status')
  37. $('status').classList.toggle('success', ready && !preparationError)
  38. $('prepare-section').setAttribute('aria-busy', String(busy && !ready))
  39. $('account-heading').textContent = accountTitle
  40. $('account-step-title').textContent = accountTitle
  41. $('account-section').classList.toggle('pending', !ready)
  42. $('account-section').setAttribute('aria-busy', String(busy && ready))
  43. $('account-hint').textContent = !ready ? '服务准备完成后,在此创建账号或登录。'
  44. : state.authenticated ? `已登录${state.username ? `:${state.username}` : ''},可以继续进入夭夭。`
  45. : setup ? '首次使用,请设置管理员账号和密码。密码至少 8 个字符。' : '登录所选服务器的夭夭账号。'
  46. $('login-form').hidden = !ready
  47. $('credentials').hidden = state.authenticated
  48. $('confirmation-field').hidden = !setup
  49. $('username').disabled = busy || state.authenticated || !ready
  50. $('password').disabled = busy || state.authenticated || !ready
  51. $('confirmation').disabled = busy || !setup || state.authenticated
  52. $('confirmation').required = setup && !state.authenticated
  53. $('password').minLength = setup ? 8 : 1
  54. $('password').autocomplete = setup ? 'new-password' : 'current-password'
  55. $('toggle-password').disabled = busy
  56. $('submit').disabled = busy
  57. $('submit').querySelector('span').textContent = state.phase === 'authenticating' ? (setup ? '正在创建…' : '正在登录…')
  58. : state.phase === 'entering' ? '正在进入…' : state.authenticated ? '进入夭夭' : setup ? '创建并进入夭夭' : '登录并进入夭夭'
  59. $('login-error').textContent = ready ? actionError || state.error : ''
  60. $('login-error').hidden = !$('login-error').textContent
  61. $('waiting').hidden = !busy || ready
  62. $('waiting').textContent = local ? '正在准备服务,请稍候' : '正在检测连接,请稍候'
  63. const activeStep = ready ? 2 : state.mode ? 1 : 0
  64. for (const [index, id] of ['step-choice', 'step-prepare', 'step-account'].entries()) {
  65. $(id).classList.toggle('complete', index < activeStep)
  66. if (index === activeStep) $(id).setAttribute('aria-current', 'step')
  67. else $(id).removeAttribute('aria-current')
  68. }
  69. const stages = ['checking', 'installing', 'starting']
  70. const current = stages.indexOf(state.stage)
  71. for (const [index, row] of [...document.querySelectorAll('[data-stage]')].entries()) {
  72. const complete = ready || (state.phase !== 'idle' && index < current)
  73. const active = !complete && state.phase === 'preparing' && index === current
  74. row.classList.toggle('complete', complete)
  75. row.classList.toggle('active', active)
  76. row.querySelector('use').setAttribute('href', `boot-icons.svg#${complete ? 'check' : active ? 'loader' : 'circle'}`)
  77. row.querySelector('small').textContent = complete ? '已完成' : active ? '进行中' : state.phase === 'error' && index === current ? '未完成' : '等待中'
  78. }
  79. $('install-progress').hidden = !local || state.phase !== 'preparing'
  80. document.querySelector('.install-steps').hidden = local && ready
  81. const history = state.history?.join('\n') || '准备开始'
  82. if ($('install-log').textContent !== history) $('install-log').textContent = history
  83. }
  84. async function action(run, { resetServer = false, focusAccount = false } = {}) {
  85. if (pending) return
  86. pending = true; actionError = ''; generation++
  87. if (state) display(state)
  88. try {
  89. const next = await run()
  90. if (resetServer) {
  91. if (next.phase === 'ready') $('server').value = next.serverURL || ''
  92. serverDirty = false
  93. }
  94. display(next)
  95. } catch (error) {
  96. actionError = error.message || '操作未完成,请重试。'
  97. } finally {
  98. pending = false
  99. if (state) display(state)
  100. if (focusAccount && state?.phase === 'ready' && !state.authenticated) $('username').focus()
  101. }
  102. }
  103. for (const input of document.querySelectorAll('[name=mode]')) input.addEventListener('change', () => {
  104. $('login-form').reset(); $('password').type = 'password'
  105. $('toggle-password').setAttribute('aria-pressed', 'false'); $('toggle-password').setAttribute('aria-label', '显示密码')
  106. $('password').removeAttribute('aria-invalid'); $('confirmation').removeAttribute('aria-invalid')
  107. serverDirty = false
  108. void action(() => api.selectServer(input.value))
  109. })
  110. $('server').addEventListener('input', () => {
  111. serverDirty = true; actionError = ''
  112. // Credentials belong to the checked server, never carry them to a new address.
  113. $('login-form').reset(); $('password').type = 'password'
  114. $('toggle-password').setAttribute('aria-pressed', 'false'); $('toggle-password').setAttribute('aria-label', '显示密码')
  115. if (state) display(state)
  116. })
  117. $('connection-form').addEventListener('submit', event => {
  118. event.preventDefault()
  119. void action(() => api.prepareServer({ serverURL: $('server').value, remember: $('remember').checked }), { resetServer: true, focusAccount: true })
  120. })
  121. $('prepare-local').onclick = () => action(() => api.prepareServer({ remember: $('remember').checked }), { focusAccount: true })
  122. $('retry').onclick = () => action(() => api.prepareServer({ remember: $('remember').checked }), { focusAccount: true })
  123. $('force-sync').onclick = () => action(() => api.forceSync(), { focusAccount: true })
  124. $('logs').onclick = async () => { try { await api.logs() } catch (error) { actionError = error.message; display(state) } }
  125. $('toggle-password').onclick = () => {
  126. const show = $('password').type === 'password'
  127. $('password').type = show ? 'text' : 'password'
  128. $('toggle-password').setAttribute('aria-label', show ? '隐藏密码' : '显示密码')
  129. $('toggle-password').setAttribute('aria-pressed', String(show))
  130. }
  131. $('confirmation').addEventListener('blur', () => {
  132. const mismatch = $('confirmation').value && $('confirmation').value !== $('password').value
  133. $('confirmation').setAttribute('aria-invalid', String(!!mismatch))
  134. actionError = mismatch ? '两次输入的密码不一致' : ''
  135. display(state)
  136. })
  137. $('login-form').addEventListener('submit', event => {
  138. event.preventDefault()
  139. if (serverDirty || pending) return
  140. if (state.setupRequired && !state.authenticated && $('confirmation').value !== $('password').value) {
  141. actionError = '两次输入的密码不一致'; $('confirmation').setAttribute('aria-invalid', 'true'); display(state); $('confirmation').focus(); return
  142. }
  143. void action(async () => {
  144. const next = await api.login({ username: $('username').value, password: $('password').value,
  145. confirmation: $('confirmation').value, remember: $('remember').checked })
  146. $('password').value = ''; $('confirmation').value = ''
  147. return next
  148. })
  149. })
  150. $('help-button').onclick = () => {
  151. $('help').hidden = !$('help').hidden
  152. $('help-button').setAttribute('aria-expanded', String(!$('help').hidden))
  153. }
  154. async function render() {
  155. const request = generation
  156. try { const next = await window.yaoyaoDesktop.status(); if (request === generation) display(next) }
  157. catch { if (!state) { $('status').textContent = '无法读取启动状态,请重新打开应用。'; $('status').setAttribute('role', 'alert') } }
  158. }
  159. void render()
  160. const poll = setInterval(() => { void render() }, 500)
  161. window.addEventListener('pagehide', () => clearInterval(poll), { once: true })