Last active
January 26, 2025 23:16
-
-
Save tgrushka/ecfe2ec78ae2d5859887431021e4673d to your computer and use it in GitHub Desktop.
Flutter Web Loading Progress Bar
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// prettier-ignore | |
{{flutter_js}} | |
// prettier-ignore | |
{{flutter_build_config}} | |
// Place this file at web/flutter_bootstrap.js in your Flutter Web project. | |
// IMPORTANT! The two tokens at the top of this file must not contain any spaces. | |
const APP_NAME = document.title | |
document.body.style.backgroundColor = "#000" | |
const SPEED_3G_BPS = 1500000 // 1.5 Mbps for 3G | |
const SPEED_HOME_BPS = SPEED_3G_BPS * 10 | |
// If > 0, will throttle fetch to simulate slow network speeds. | |
const SIMULATE_SPEED = 0 | |
// Flutter Web Initialization Documentation: | |
// https://docs.flutter.dev/platform-integration/web/initialization#example-display-a-progress-indicator | |
const container = document.createElement("div") | |
container.style.cssText = ` | |
position: absolute; | |
inset: 0; | |
display: flex; | |
flex-direction: column; | |
justify-content: center; | |
align-items: stretch; | |
gap: 2rem; | |
overflow: hidden; | |
` | |
const progressDescription = document.createElement("div") | |
progressDescription.style.cssText = ` | |
color: #fff; | |
text-align: center; | |
align-self: center; | |
font-family: Arial, Helvetica, sans-serif; | |
font-size: 2em; | |
white-space: nowrap; | |
` | |
progressDescription.textContent = `Loading ${ | |
typeof APP_NAME !== "undefined" ? APP_NAME : "application" | |
}...` | |
const progressContainer = document.createElement("div") | |
progressContainer.style.cssText = ` | |
margin: 0 20%; | |
` | |
const progressRow = document.createElement("div") | |
progressRow.style.cssText = ` | |
display: flex; | |
align-items: center; | |
gap: 1em; | |
` | |
const progressBg = document.createElement("div") | |
progressBg.style.cssText = ` | |
flex-grow: 1; | |
padding: 5px; | |
border-radius: 16px; | |
height: 1em; | |
background: #333; | |
` | |
const progress = document.createElement("div") | |
progress.style.cssText = ` | |
width: 0%; | |
height: 100%; | |
border-radius: 9px; | |
background: #0175C2; | |
transition: width 0.3s; | |
` | |
const percentage = document.createElement("div") | |
percentage.style.cssText = ` | |
color: #fff; | |
font-family: Arial, Helvetica, sans-serif; | |
font-size: 1em; | |
white-space: nowrap; | |
` | |
percentage.textContent = "0%" | |
const timeRemaining = document.createElement("div") | |
timeRemaining.style.cssText = ` | |
color: #fff; | |
text-align: center; | |
margin-top: 1em; | |
font-family: Arial, Helvetica, sans-serif; | |
font-size: 1em; | |
` | |
timeRemaining.textContent = "Estimating time remaining..." | |
progressBg.appendChild(progress) | |
progressRow.appendChild(progressBg) | |
progressRow.appendChild(percentage) | |
progressContainer.appendChild(progressRow) | |
progressContainer.appendChild(timeRemaining) | |
container.appendChild(progressDescription) | |
container.appendChild(progressContainer) | |
document.body.appendChild(container) | |
const originalFetch = window.fetch | |
const stats = new Map() | |
const startTime = Date.now() | |
window.fetch = async (...args) => { | |
// Get content length from HEAD request first | |
const [url] = args | |
const headResponse = await originalFetch.apply(window, [ | |
url, | |
{ method: "HEAD" }, | |
]) | |
if (headResponse.status >= 400) { | |
return await originalFetch.apply(window, args) | |
} | |
// Content-Encoding header not available in browser, so estimate compression ratio: | |
const ratio = 3.5 | |
const contentLengthHeader = headResponse.headers.get("Content-Length") | |
if (contentLengthHeader) { | |
const contentLength = parseInt(contentLengthHeader) | |
if (contentLength > 0) { | |
const total = contentLength * ratio | |
stats.set(url, { | |
total, | |
loaded: 0, | |
}) | |
} else { | |
console.error("Content-Length = 0 or NaN for", url) | |
} | |
} else { | |
console.error("Content-Length header not found for", url) | |
} | |
// Proceed with original fetch | |
const response = await originalFetch.apply(window, args) | |
const reader = response.body.getReader() | |
return new Response( | |
new ReadableStream({ | |
async start(controller) { | |
while (true) { | |
const { done, value } = await reader.read() | |
if ( | |
!done && | |
typeof SIMULATE_SPEED === "number" && | |
SIMULATE_SPEED > 0 | |
) { | |
await new Promise((resolve) => | |
setTimeout( | |
resolve, | |
((value.length * 8) / SIMULATE_SPEED) * 1000 | |
) | |
) | |
} | |
if (stats.has(url)) { | |
const stat = stats.get(url) | |
if (done) { | |
// Update total for this URL from estimated to actual downloaded bytes. | |
stat.total = stat.loaded | |
} else { | |
stat.loaded += value.length | |
} | |
const totalBytes = Array.from(stats.values()).reduce( | |
(sum, s) => sum + s.total, | |
0 | |
) | |
const loadedBytes = Array.from(stats.values()).reduce( | |
(sum, s) => sum + s.loaded, | |
0 | |
) | |
const percent = Math.floor( | |
(loadedBytes / totalBytes) * 100 | |
) | |
progress.style.width = `${percent}%` | |
percentage.textContent = `${percent}%` | |
// Calculate time remaining | |
const elapsed = (Date.now() - startTime) / 1000 | |
const rate = loadedBytes / elapsed // bytes per second | |
const remaining = Math.ceil( | |
(totalBytes - loadedBytes) / rate | |
) | |
timeRemaining.textContent = | |
remaining > 0 | |
? `about ${remaining} seconds remaining` | |
: "" | |
} | |
if (done) { | |
break | |
} | |
controller.enqueue(value) | |
} | |
controller.close() | |
}, | |
}), | |
response | |
) | |
} | |
_flutter.loader.load({ | |
onEntrypointLoaded: async function (engineInitializer) { | |
// This is the long step that downloads the WASM binary. | |
const appRunner = await engineInitializer.initializeEngine() | |
progressDescription.textContent = "Running app..." | |
// Restore original fetch so it doesn't interfere with your app. | |
window.fetch = originalFetch | |
await appRunner.runApp() | |
}, | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment