Skip to content

Commit 943d82c

Browse files
committed
chore: simpler splash window
1 parent 10a43e0 commit 943d82c

6 files changed

Lines changed: 225 additions & 118 deletions

File tree

packages/desktop-electron/electron.vite.config.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ export default defineConfig({
3333
rollupOptions: {
3434
input: {
3535
main: "src/renderer/index.html",
36-
loading: "src/renderer/loading.html",
3736
},
3837
},
3938
},

packages/desktop-electron/src/main/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,8 @@ async function initialize() {
198198
setInitStep({ phase: "done" })
199199

200200
if (splash) {
201-
await loadingComplete.promise
201+
const ok = await Promise.race([loadingComplete.promise.then(() => true), delay(2_000).then(() => false)])
202+
if (!ok) logger.warn("loading window complete timed out")
202203
splash.close()
203204
splash = null
204205
}

packages/desktop-electron/src/main/windows.ts

Lines changed: 222 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ export function getBackgroundColor(): string | undefined {
2121
return backgroundColor
2222
}
2323

24+
function back(mode = tone()) {
25+
return backgroundColor ?? (mode === "dark" ? "#101010" : "#f8f8f8")
26+
}
27+
2428
function iconsDir() {
2529
return app.isPackaged ? join(process.resourcesPath, "icons") : join(root, "../../resources/icons")
2630
}
@@ -69,7 +73,7 @@ export function createMainWindow(globals: Globals, opts: { show?: boolean } = {}
6973
show: opts.show ?? true,
7074
title: "OpenCode",
7175
icon: iconPath(),
72-
backgroundColor,
76+
backgroundColor: back(mode),
7377
...(process.platform === "darwin"
7478
? {
7579
titleBarStyle: "hidden" as const,
@@ -98,27 +102,241 @@ export function createMainWindow(globals: Globals, opts: { show?: boolean } = {}
98102
}
99103

100104
export function createLoadingWindow(globals: Globals) {
105+
const mode = tone()
101106
const win = new BrowserWindow({
102107
width: 640,
103108
height: 480,
104109
resizable: false,
105110
center: true,
106-
show: true,
111+
show: false,
107112
frame: false,
108113
icon: iconPath(),
109-
backgroundColor,
114+
backgroundColor: back(mode),
110115
webPreferences: {
111116
preload: join(root, "../preload/index.mjs"),
112117
sandbox: false,
113118
},
114119
})
115120

116-
loadWindow(win, "loading.html")
121+
win.once("ready-to-show", () => {
122+
if (!win.isDestroyed()) win.show()
123+
})
124+
125+
loadSplash(win, mode)
117126
injectGlobals(win, globals)
118127

119128
return win
120129
}
121130

131+
function loadSplash(win: BrowserWindow, mode: "dark" | "light") {
132+
void win.loadURL(`data:text/html;charset=UTF-8,${encodeURIComponent(page(mode))}`)
133+
}
134+
135+
function page(mode: "dark" | "light") {
136+
const dark = mode === "dark"
137+
const bg = back(mode)
138+
const base = dark ? "#7e7e7e" : "#8f8f8f"
139+
const weak = dark ? "#343434" : "#dbdbdb"
140+
const strong = dark ? "#ededed" : "#171717"
141+
const track = dark ? "rgba(255,255,255,0.078)" : "rgba(0,0,0,0.051)"
142+
const warn = dark ? "#fbb73c" : "#ebb76e"
143+
const pulse = mark(base, strong)
144+
const splash = mark(weak, strong)
145+
146+
return `<!doctype html>
147+
<html lang="en">
148+
<head>
149+
<meta charset="utf-8" />
150+
<meta name="viewport" content="width=device-width, initial-scale=1" />
151+
<title>OpenCode</title>
152+
<style>
153+
:root {
154+
color-scheme: ${mode};
155+
}
156+
157+
html,
158+
body {
159+
width: 100%;
160+
height: 100%;
161+
margin: 0;
162+
overflow: hidden;
163+
background: ${bg};
164+
}
165+
166+
body {
167+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
168+
}
169+
170+
#root {
171+
display: flex;
172+
width: 100%;
173+
height: 100%;
174+
align-items: center;
175+
justify-content: center;
176+
}
177+
178+
#pulse,
179+
#migrate {
180+
display: flex;
181+
align-items: center;
182+
justify-content: center;
183+
}
184+
185+
#pulse[hidden],
186+
#migrate[hidden] {
187+
display: none;
188+
}
189+
190+
#pulse svg {
191+
width: 64px;
192+
height: 80px;
193+
opacity: 0.5;
194+
animation: pulse 1.6s ease-in-out infinite;
195+
transform-origin: center;
196+
}
197+
198+
#migrate {
199+
flex-direction: column;
200+
gap: 44px;
201+
}
202+
203+
#migrate svg {
204+
width: 80px;
205+
height: 100px;
206+
opacity: 0.15;
207+
}
208+
209+
#copy {
210+
display: flex;
211+
width: 240px;
212+
flex-direction: column;
213+
align-items: center;
214+
gap: 16px;
215+
}
216+
217+
#status {
218+
width: 100%;
219+
overflow: hidden;
220+
color: ${strong};
221+
text-align: center;
222+
text-overflow: ellipsis;
223+
white-space: nowrap;
224+
font-size: 14px;
225+
line-height: 20px;
226+
}
227+
228+
#bar {
229+
width: 80px;
230+
height: 4px;
231+
overflow: hidden;
232+
background: ${track};
233+
}
234+
235+
#fill {
236+
width: 25%;
237+
height: 100%;
238+
background: ${warn};
239+
}
240+
241+
@keyframes pulse {
242+
0%,
243+
100% {
244+
opacity: 0.5;
245+
}
246+
247+
50% {
248+
opacity: 0.15;
249+
}
250+
}
251+
</style>
252+
</head>
253+
<body>
254+
<div id="root">
255+
<div id="pulse">${pulse}</div>
256+
<div id="migrate" hidden>
257+
${splash}
258+
<div id="copy" aria-live="polite">
259+
<span id="status">Just a moment...</span>
260+
<div id="bar"><div id="fill"></div></div>
261+
</div>
262+
</div>
263+
</div>
264+
<script>
265+
;(() => {
266+
const lines = ["Just a moment...", "Migrating your database", "This may take a couple of minutes"]
267+
const pulse = document.getElementById("pulse")
268+
const migrate = document.getElementById("migrate")
269+
const status = document.getElementById("status")
270+
const fill = document.getElementById("fill")
271+
let step = { phase: "server_waiting" }
272+
let line = 0
273+
let seen = false
274+
let value = 0
275+
let done = false
276+
277+
function render() {
278+
const sql = step.phase === "sqlite_waiting" || (seen && step.phase === "done")
279+
pulse.hidden = sql
280+
migrate.hidden = !sql
281+
if (!sql) return
282+
status.textContent = step.phase === "done" ? "All done" : lines[line]
283+
fill.style.width = String(step.phase === "done" ? 100 : Math.max(25, Math.min(100, value))) + "%"
284+
}
285+
286+
function finish() {
287+
if (done) return
288+
done = true
289+
window.api?.loadingWindowComplete?.()
290+
}
291+
292+
function set(step_) {
293+
step = step_ || step
294+
render()
295+
if (step.phase === "done") finish()
296+
}
297+
298+
const timers = [3000, 9000].map((ms, i) =>
299+
setTimeout(() => {
300+
line = i + 1
301+
render()
302+
}, ms),
303+
)
304+
305+
const off = window.api?.onInitStep?.((step_) => set(step_)) ?? (() => {})
306+
const progress =
307+
window.api?.onSqliteMigrationProgress?.((next) => {
308+
seen = true
309+
if (next.type === "InProgress") {
310+
value = Math.max(0, Math.min(100, next.value))
311+
step = { phase: "sqlite_waiting" }
312+
render()
313+
return
314+
}
315+
value = 100
316+
step = { phase: "done" }
317+
render()
318+
finish()
319+
}) ?? (() => {})
320+
321+
window.api?.awaitInitialization?.((step_) => set(step_))?.catch(() => undefined)
322+
323+
addEventListener("beforeunload", () => {
324+
off()
325+
progress()
326+
timers.forEach(clearTimeout)
327+
})
328+
329+
render()
330+
})()
331+
</script>
332+
</body>
333+
</html>`
334+
}
335+
336+
function mark(base: string, strong: string) {
337+
return `<svg viewBox="0 0 80 100" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M60 80H20V40H60V80Z" fill="${base}" /><path d="M60 20H20V80H60V20ZM80 100H0V0H80V100Z" fill="${strong}" /></svg>`
338+
}
339+
122340
function loadWindow(win: BrowserWindow, html: string) {
123341
const devUrl = process.env.ELECTRON_RENDERER_URL
124342
if (devUrl) {

packages/desktop-electron/src/renderer/html.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const html = async (name: string) => Bun.file(join(dir, name)).text()
1616
* All local resource references must use relative paths (`./`).
1717
*/
1818
describe("electron renderer html", () => {
19-
for (const name of ["index.html", "loading.html"]) {
19+
for (const name of ["index.html"]) {
2020
describe(name, () => {
2121
test("script src attributes use relative paths", async () => {
2222
const content = await html(name)

packages/desktop-electron/src/renderer/loading.html

Lines changed: 0 additions & 22 deletions
This file was deleted.

0 commit comments

Comments
 (0)