|
5 | 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
6 | 6 | <title>Compose Mac OS 26 UI</title> |
7 | 7 | <link type="text/css" rel="stylesheet" href="styles.css"> |
| 8 | + <style> |
| 9 | + #boot-screen { |
| 10 | + position: fixed; |
| 11 | + inset: 0; |
| 12 | + background: #000; |
| 13 | + display: flex; |
| 14 | + align-items: flex-end; |
| 15 | + justify-content: center; |
| 16 | + padding-bottom: 80px; |
| 17 | + z-index: 9999; |
| 18 | + opacity: 1; |
| 19 | + transition: opacity 0.7s ease; |
| 20 | + } |
| 21 | + #boot-screen.fade-out { |
| 22 | + opacity: 0; |
| 23 | + pointer-events: none; |
| 24 | + } |
| 25 | + .boot-progress-wrap { |
| 26 | + width: 180px; |
| 27 | + height: 4px; |
| 28 | + background: rgba(255, 255, 255, 0.15); |
| 29 | + border-radius: 2px; |
| 30 | + overflow: hidden; |
| 31 | + } |
| 32 | + .boot-progress-bar { |
| 33 | + height: 100%; |
| 34 | + background: #fff; |
| 35 | + border-radius: 2px; |
| 36 | + width: 0%; |
| 37 | + transition: width 0.2s ease-out; |
| 38 | + } |
| 39 | + </style> |
8 | 40 | </head> |
9 | | -<body style="text-align: center; align-content: center"> |
10 | | -<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 50 50" role="presentation"> |
11 | | - <circle cx="25" cy="25" r="20" stroke="#ccc" stroke-width="4" fill="none"/> |
12 | | - <circle cx="25" cy="25" r="20" stroke="#333" stroke-width="4" fill="none" stroke-linecap="round" |
13 | | - stroke-dasharray="90 125"> |
14 | | - <animateTransform attributeName="transform" type="rotate" from="0 25 25" to="360 25 25" dur="1s" |
15 | | - repeatCount="indefinite"/> |
16 | | - </circle> |
17 | | -</svg> |
| 41 | +<body> |
| 42 | +<div id="boot-screen"> |
| 43 | + <div class="boot-progress-wrap"> |
| 44 | + <div class="boot-progress-bar" id="boot-bar"></div> |
| 45 | + </div> |
| 46 | +</div> |
| 47 | + |
| 48 | +<script>(function () { |
| 49 | + 'use strict'; |
| 50 | + |
| 51 | + var bar = document.getElementById('boot-bar'); |
| 52 | + var screen = document.getElementById('boot-screen'); |
| 53 | + var wasmProgress = {}; // url -> { loaded, total } |
| 54 | + var dismissed = false; |
| 55 | + var barPct = 0; |
| 56 | + |
| 57 | + function setBar(pct) { |
| 58 | + barPct = Math.max(barPct, Math.min(100, pct)); |
| 59 | + bar.style.width = barPct + '%'; |
| 60 | + } |
| 61 | + |
| 62 | + function dismiss() { |
| 63 | + if (dismissed) return; |
| 64 | + dismissed = true; |
| 65 | + screen.classList.add('fade-out'); |
| 66 | + setTimeout(function () { if (screen.parentNode) screen.parentNode.removeChild(screen); }, 750); |
| 67 | + } |
| 68 | + |
| 69 | + // Rough expected total for Kotlin/WASM builds (2 files: ~4 MB + ~9 MB). |
| 70 | + // Used only when the server omits Content-Length. |
| 71 | + var WASM_ESTIMATED_BYTES = 14 * 1024 * 1024; |
| 72 | + |
| 73 | + function recalc() { |
| 74 | + var urls = Object.keys(wasmProgress); |
| 75 | + if (urls.length === 0) return; |
| 76 | + |
| 77 | + var knownTotal = 0; |
| 78 | + var loadedBytes = 0; |
| 79 | + var allDone = true; |
| 80 | + var hasTotal = true; |
| 81 | + |
| 82 | + urls.forEach(function (u) { |
| 83 | + var entry = wasmProgress[u]; |
| 84 | + if (!entry.total) hasTotal = false; |
| 85 | + if (!entry.done) allDone = false; |
| 86 | + knownTotal += entry.total || 0; |
| 87 | + loadedBytes += entry.loaded || 0; |
| 88 | + }); |
| 89 | + |
| 90 | + var pct; |
| 91 | + if (hasTotal && knownTotal > 0) { |
| 92 | + // Exact progress from Content-Length |
| 93 | + pct = (loadedBytes / knownTotal) * 100; |
| 94 | + } else { |
| 95 | + // Estimate: map received bytes against expected total, cap at 95% until done |
| 96 | + pct = Math.min(95, (loadedBytes / WASM_ESTIMATED_BYTES) * 100); |
| 97 | + } |
| 98 | + |
| 99 | + setBar(pct); |
| 100 | + |
| 101 | + if (allDone) { |
| 102 | + setBar(100); |
| 103 | + setTimeout(dismiss, 350); |
| 104 | + } |
| 105 | + } |
| 106 | + |
| 107 | + // Patch fetch to intercept WASM downloads and track progress via ReadableStream |
| 108 | + var _fetch = window.fetch; |
| 109 | + window.fetch = function (input, init) { |
| 110 | + var url = typeof input === 'string' ? input |
| 111 | + : (input && input.url) ? input.url |
| 112 | + : String(input); |
| 113 | + |
| 114 | + if (!/\.wasm(\?|$)/.test(url)) return _fetch.apply(this, arguments); |
| 115 | + |
| 116 | + return _fetch.apply(this, arguments).then(function (response) { |
| 117 | + var cl = response.headers.get('Content-Length'); |
| 118 | + wasmProgress[url] = { loaded: 0, total: cl ? parseInt(cl, 10) : 0 }; |
| 119 | + |
| 120 | + if (!response.body) { |
| 121 | + // No streaming support — mark as complete immediately |
| 122 | + wasmProgress[url].total = 1; |
| 123 | + wasmProgress[url].loaded = 1; |
| 124 | + wasmProgress[url].done = true; |
| 125 | + recalc(); |
| 126 | + return response; |
| 127 | + } |
| 128 | + |
| 129 | + var reader = response.body.getReader(); |
| 130 | + var stream = new ReadableStream({ |
| 131 | + start: function (ctrl) { |
| 132 | + (function pump() { |
| 133 | + reader.read().then(function (chunk) { |
| 134 | + if (chunk.done) { |
| 135 | + wasmProgress[url].done = true; |
| 136 | + if (!wasmProgress[url].total) { |
| 137 | + wasmProgress[url].total = wasmProgress[url].loaded; |
| 138 | + } |
| 139 | + recalc(); |
| 140 | + ctrl.close(); |
| 141 | + return; |
| 142 | + } |
| 143 | + wasmProgress[url].loaded += chunk.value.byteLength; |
| 144 | + recalc(); |
| 145 | + ctrl.enqueue(chunk.value); |
| 146 | + pump(); |
| 147 | + }).catch(function (e) { ctrl.error(e); }); |
| 148 | + }()); |
| 149 | + } |
| 150 | + }); |
| 151 | + |
| 152 | + return new Response(stream, { |
| 153 | + headers: response.headers, |
| 154 | + status: response.status, |
| 155 | + statusText: response.statusText |
| 156 | + }); |
| 157 | + }); |
| 158 | + }; |
| 159 | + |
| 160 | + // Fallback for the pure-JS build (no WASM): animate the bar with easing, |
| 161 | + // then watch for the Compose <canvas> to appear as the final signal. |
| 162 | + var fallbackHandle = null; |
| 163 | + var fallbackStarted = false; |
| 164 | + |
| 165 | + function startFallback() { |
| 166 | + if (fallbackStarted) return; |
| 167 | + fallbackStarted = true; |
| 168 | + var start = Date.now(); |
| 169 | + fallbackHandle = setInterval(function () { |
| 170 | + var t = Math.min(1, (Date.now() - start) / 9000); |
| 171 | + // Ease-out cubic, capped at 90% until canvas appears |
| 172 | + setBar((1 - Math.pow(1 - t, 3)) * 90); |
| 173 | + }, 60); |
| 174 | + } |
| 175 | + |
| 176 | + // Watch for the Compose canvas — signals that the app has rendered |
| 177 | + var canvasMo = new MutationObserver(function () { |
| 178 | + if (!document.querySelector('canvas')) return; |
| 179 | + canvasMo.disconnect(); |
| 180 | + clearInterval(fallbackHandle); |
| 181 | + if (!dismissed) { |
| 182 | + setBar(100); |
| 183 | + setTimeout(dismiss, 350); |
| 184 | + } |
| 185 | + }); |
| 186 | + canvasMo.observe(document.body, { childList: true, subtree: true }); |
| 187 | + |
| 188 | + // If no WASM fetch is detected within 1 s, assume JS-only target |
| 189 | + setTimeout(function () { |
| 190 | + if (Object.keys(wasmProgress).length === 0) startFallback(); |
| 191 | + }, 1000); |
| 192 | + |
| 193 | + // Absolute safety net — never leave the boot screen up forever |
| 194 | + setTimeout(function () { |
| 195 | + clearInterval(fallbackHandle); |
| 196 | + setBar(100); |
| 197 | + setTimeout(dismiss, 350); |
| 198 | + }, 45000); |
| 199 | +}()); |
| 200 | +</script> |
| 201 | + |
18 | 202 | <script type="application/javascript" src="sample.js"></script> |
19 | 203 | </body> |
20 | 204 | </html> |
0 commit comments