max / goingson
- Co-Authored-By
- Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 file changed,
+1 insertion,
-212 deletions
| @@ -1,212 +1,1 @@ | |||
| 1 | - | /** | |
| 2 | - | * Shared Tauri OTA Update UI | |
| 3 | - | * | |
| 4 | - | * Listens for update-available events from the Rust backend and shows | |
| 5 | - | * a notification banner with Install / Dismiss buttons. The user must | |
| 6 | - | * explicitly consent before any download or installation happens. | |
| 7 | - | * | |
| 8 | - | * Usage: | |
| 9 | - | * initUpdater({ ui, escapeHtml, namespace }) | |
| 10 | - | * | |
| 11 | - | * - ui: object with showToast(msg, level?) method | |
| 12 | - | * - escapeHtml: function(str) -> safe string (optional, falls back to identity) | |
| 13 | - | * - namespace: object to attach { showUpdateBanner } onto (e.g. GoingsOn.updater) | |
| 14 | - | * - bannerClass: CSS class name for the banner (optional, uses inline styles if omitted) | |
| 15 | - | * - cssVars: override CSS variable names (optional) | |
| 16 | - | * - renderMarkdown: function(body) -> safe HTML string (optional). When supplied, | |
| 17 | - | * release notes render as HTML and the 120-char preview truncation is dropped. | |
| 18 | - | * The caller is responsible for sanitizing the output. | |
| 19 | - | */ | |
| 20 | - | ||
| 21 | - | // eslint-disable-next-line no-unused-vars | |
| 22 | - | function initUpdater(opts) { | |
| 23 | - | 'use strict'; | |
| 24 | - | ||
| 25 | - | const ui = opts.ui; | |
| 26 | - | const esc = opts.escapeHtml || ((s) => s); | |
| 27 | - | const vars = Object.assign({ | |
| 28 | - | accent: 'var(--action)', | |
| 29 | - | border: 'var(--border)', | |
| 30 | - | textSecondary: 'var(--content-secondary)', | |
| 31 | - | bgSecondary: 'var(--surface-overlay)', | |
| 32 | - | fontBody: 'var(--font-body, sans-serif)', | |
| 33 | - | }, opts.cssVars || {}); | |
| 34 | - | ||
| 35 | - | let updateBannerShown = false; | |
| 36 | - | let pendingUpdate = null; | |
| 37 | - | ||
| 38 | - | const BTN_STYLE = [ | |
| 39 | - | 'padding: 0.25rem 0.5rem', | |
| 40 | - | 'border-radius: 4px', | |
| 41 | - | 'cursor: pointer', | |
| 42 | - | 'font-size: 0.8rem', | |
| 43 | - | ].join(';'); | |
| 44 | - | ||
| 45 | - | function showUpdateBanner(version, body, update) { | |
| 46 | - | if (updateBannerShown) return; | |
| 47 | - | updateBannerShown = true; | |
| 48 | - | pendingUpdate = update || null; | |
| 49 | - | ||
| 50 | - | const banner = document.createElement('div'); | |
| 51 | - | banner.id = 'update-banner'; | |
| 52 | - | ||
| 53 | - | if (opts.bannerClass) { | |
| 54 | - | banner.className = opts.bannerClass; | |
| 55 | - | } else { | |
| 56 | - | banner.style.cssText = [ | |
| 57 | - | 'position: fixed', | |
| 58 | - | 'bottom: 1rem', | |
| 59 | - | 'right: 1rem', | |
| 60 | - | 'background: ' + vars.bgSecondary, | |
| 61 | - | 'border: 1px solid ' + vars.border, | |
| 62 | - | 'border-radius: 8px', | |
| 63 | - | 'padding: 0.75rem 1rem', | |
| 64 | - | 'z-index: 9999', | |
| 65 | - | 'max-width: 320px', | |
| 66 | - | 'box-shadow: 0 4px 12px rgba(0,0,0,0.3)', | |
| 67 | - | 'font-family: ' + vars.fontBody, | |
| 68 | - | 'font-size: 0.875rem', | |
| 69 | - | ].join(';'); | |
| 70 | - | } | |
| 71 | - | ||
| 72 | - | const title = document.createElement('div'); | |
| 73 | - | title.style.cssText = 'font-weight: 600; margin-bottom: 0.25rem;'; | |
| 74 | - | title.textContent = 'Update Available: v' + esc(version); | |
| 75 | - | banner.appendChild(title); | |
| 76 | - | ||
| 77 | - | if (body) { | |
| 78 | - | const notes = document.createElement('div'); | |
| 79 | - | notes.style.cssText = 'color: ' + vars.textSecondary + '; margin-bottom: 0.5rem; max-height: 8rem; overflow-y: auto;'; | |
| 80 | - | if (typeof opts.renderMarkdown === 'function') { | |
| 81 | - | notes.innerHTML = opts.renderMarkdown(body); | |
| 82 | - | } else { | |
| 83 | - | notes.textContent = body.length > 120 ? body.substring(0, 120) + '...' : body; | |
| 84 | - | } | |
| 85 | - | banner.appendChild(notes); | |
| 86 | - | } | |
| 87 | - | ||
| 88 | - | const buttons = document.createElement('div'); | |
| 89 | - | buttons.style.cssText = 'display: flex; gap: 0.5rem; margin-top: 0.5rem;'; | |
| 90 | - | ||
| 91 | - | const install = document.createElement('button'); | |
| 92 | - | install.textContent = 'Install & Restart'; | |
| 93 | - | install.style.cssText = BTN_STYLE + ';background: ' + vars.accent + '; border: none; color: #fff; font-weight: 600;'; | |
| 94 | - | install.onclick = () => installUpdate(banner); | |
| 95 | - | buttons.appendChild(install); | |
| 96 | - | ||
| 97 | - | const dismiss = document.createElement('button'); | |
| 98 | - | dismiss.textContent = 'Not Now'; | |
| 99 | - | dismiss.style.cssText = BTN_STYLE + ';background: none; border: 1px solid ' + vars.border + '; color: ' + vars.textSecondary + ';'; | |
| 100 | - | dismiss.onclick = () => { | |
| 101 | - | banner.remove(); | |
| 102 | - | updateBannerShown = false; | |
| 103 | - | pendingUpdate = null; | |
| 104 | - | }; | |
| 105 | - | buttons.appendChild(dismiss); | |
| 106 | - | ||
| 107 | - | banner.appendChild(buttons); | |
| 108 | - | document.body.appendChild(banner); | |
| 109 | - | } | |
| 110 | - | ||
| 111 | - | async function installUpdate(banner) { | |
| 112 | - | if (!pendingUpdate) { | |
| 113 | - | try { | |
| 114 | - | const { check } = window.__TAURI_PLUGIN_UPDATER__; | |
| 115 | - | pendingUpdate = await check(); | |
| 116 | - | } catch (err) { | |
| 117 | - | ui.showToast('Update check failed: ' + err, 'error'); | |
| 118 | - | return; | |
| 119 | - | } | |
| 120 | - | } | |
| 121 | - | ||
| 122 | - | if (!pendingUpdate) { | |
| 123 | - | ui.showToast('No update available.', 'error'); | |
| 124 | - | return; | |
| 125 | - | } | |
| 126 | - | ||
| 127 | - | banner.innerHTML = ''; | |
| 128 | - | const status = document.createElement('div'); | |
| 129 | - | status.textContent = 'Starting download...'; | |
| 130 | - | status.style.cssText = 'margin-bottom: 0.5rem;'; | |
| 131 | - | banner.appendChild(status); | |
| 132 | - | ||
| 133 | - | const progressWrap = document.createElement('div'); | |
| 134 | - | progressWrap.style.cssText = 'height: 4px; background: ' + vars.border + '; border-radius: 2px; overflow: hidden;'; | |
| 135 | - | const progressBar = document.createElement('div'); | |
| 136 | - | progressBar.style.cssText = 'height: 100%; width: 0%; background: ' + vars.accent + '; transition: width 120ms linear;'; | |
| 137 | - | progressWrap.appendChild(progressBar); | |
| 138 | - | banner.appendChild(progressWrap); | |
| 139 | - | ||
| 140 | - | let total = 0; | |
| 141 | - | let received = 0; | |
| 142 | - | const onEvent = (e) => { | |
| 143 | - | if (!e || !e.event) return; | |
| 144 | - | if (e.event === 'Started') { | |
| 145 | - | total = (e.data && e.data.contentLength) || 0; | |
| 146 | - | received = 0; | |
| 147 | - | status.textContent = total > 0 | |
| 148 | - | ? 'Downloading update (0%)...' | |
| 149 | - | : 'Downloading update...'; | |
| 150 | - | } else if (e.event === 'Progress') { | |
| 151 | - | received += (e.data && e.data.chunkLength) || 0; | |
| 152 | - | if (total > 0) { | |
| 153 | - | const pct = Math.min(100, Math.round((received / total) * 100)); | |
| 154 | - | progressBar.style.width = pct + '%'; | |
| 155 | - | status.textContent = 'Downloading update (' + pct + '%)...'; | |
| 156 | - | } | |
| 157 | - | } else if (e.event === 'Finished') { | |
| 158 | - | progressBar.style.width = '100%'; | |
| 159 | - | status.textContent = 'Installing...'; | |
| 160 | - | } | |
| 161 | - | }; | |
| 162 | - | ||
| 163 | - | try { | |
| 164 | - | await pendingUpdate.downloadAndInstall(onEvent); | |
| 165 | - | status.textContent = 'Update installed. Restarting...'; | |
| 166 | - | // Relaunch into the new version. Best-effort: requires the host app | |
| 167 | - | // to register the process plugin; if it's absent the install still | |
| 168 | - | // succeeded and the user can restart manually. | |
| 169 | - | try { | |
| 170 | - | await window.__TAURI__.process.relaunch(); | |
| 171 | - | } catch (e) { | |
| 172 | - | status.textContent = 'Update installed. Please quit and reopen the app.'; | |
| 173 | - | } | |
| 174 | - | } catch (err) { | |
| 175 | - | status.textContent = 'Update failed: ' + err; | |
| 176 | - | ui.showToast('Update failed: ' + err, 'error'); | |
| 177 | - | updateBannerShown = false; | |
| 178 | - | } | |
| 179 | - | } | |
| 180 | - | ||
| 181 | - | // Listen for automatic update check results from Rust backend | |
| 182 | - | if (window.__TAURI__) { | |
| 183 | - | const { listen } = window.__TAURI__.event; | |
| 184 | - | ||
| 185 | - | listen('update-available', (event) => { | |
| 186 | - | const { version, body } = event.payload; | |
| 187 | - | showUpdateBanner(version, body, null); | |
| 188 | - | }); | |
| 189 | - | ||
| 190 | - | listen('menu:check_updates', async () => { | |
| 191 | - | ui.showToast('Checking for updates...'); | |
| 192 | - | try { | |
| 193 | - | const { check } = window.__TAURI_PLUGIN_UPDATER__; | |
| 194 | - | const update = await check(); | |
| 195 | - | if (update) { | |
| 196 | - | showUpdateBanner(update.version, update.body || '', update); | |
| 197 | - | } else { | |
| 198 | - | ui.showToast('You are running the latest version.'); | |
| 199 | - | } | |
| 200 | - | } catch (err) { | |
| 201 | - | ui.showToast('Update check failed: ' + err, 'error'); | |
| 202 | - | } | |
| 203 | - | }); | |
| 204 | - | } | |
| 205 | - | ||
| 206 | - | // Attach to caller's namespace | |
| 207 | - | if (opts.namespace) { | |
| 208 | - | opts.namespace.showUpdateBanner = showUpdateBanner; | |
| 209 | - | } | |
| 210 | - | ||
| 211 | - | return { showUpdateBanner }; | |
| 212 | - | } | |
| 1 | + | ../../../../../MNW/shared/tauri-updater-ui/updater.js |