@@ -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+
2428function 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
100104export 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+
122340function loadWindow ( win : BrowserWindow , html : string ) {
123341 const devUrl = process . env . ELECTRON_RENDERER_URL
124342 if ( devUrl ) {
0 commit comments