Przeglądaj źródła

chore: simpler splash window

Adam 1 tydzień temu
rodzic
commit
943d82ce0d

+ 0 - 1
packages/desktop-electron/electron.vite.config.ts

@@ -33,7 +33,6 @@ export default defineConfig({
       rollupOptions: {
         input: {
           main: "src/renderer/index.html",
-          loading: "src/renderer/loading.html",
         },
       },
     },

+ 2 - 1
packages/desktop-electron/src/main/index.ts

@@ -198,7 +198,8 @@ async function initialize() {
   setInitStep({ phase: "done" })
 
   if (splash) {
-    await loadingComplete.promise
+    const ok = await Promise.race([loadingComplete.promise.then(() => true), delay(2_000).then(() => false)])
+    if (!ok) logger.warn("loading window complete timed out")
     splash.close()
     splash = null
   }

+ 222 - 4
packages/desktop-electron/src/main/windows.ts

@@ -21,6 +21,10 @@ export function getBackgroundColor(): string | undefined {
   return backgroundColor
 }
 
+function back(mode = tone()) {
+  return backgroundColor ?? (mode === "dark" ? "#101010" : "#f8f8f8")
+}
+
 function iconsDir() {
   return app.isPackaged ? join(process.resourcesPath, "icons") : join(root, "../../resources/icons")
 }
@@ -69,7 +73,7 @@ export function createMainWindow(globals: Globals, opts: { show?: boolean } = {}
     show: opts.show ?? true,
     title: "OpenCode",
     icon: iconPath(),
-    backgroundColor,
+    backgroundColor: back(mode),
     ...(process.platform === "darwin"
       ? {
           titleBarStyle: "hidden" as const,
@@ -98,27 +102,241 @@ export function createMainWindow(globals: Globals, opts: { show?: boolean } = {}
 }
 
 export function createLoadingWindow(globals: Globals) {
+  const mode = tone()
   const win = new BrowserWindow({
     width: 640,
     height: 480,
     resizable: false,
     center: true,
-    show: true,
+    show: false,
     frame: false,
     icon: iconPath(),
-    backgroundColor,
+    backgroundColor: back(mode),
     webPreferences: {
       preload: join(root, "../preload/index.mjs"),
       sandbox: false,
     },
   })
 
-  loadWindow(win, "loading.html")
+  win.once("ready-to-show", () => {
+    if (!win.isDestroyed()) win.show()
+  })
+
+  loadSplash(win, mode)
   injectGlobals(win, globals)
 
   return win
 }
 
+function loadSplash(win: BrowserWindow, mode: "dark" | "light") {
+  void win.loadURL(`data:text/html;charset=UTF-8,${encodeURIComponent(page(mode))}`)
+}
+
+function page(mode: "dark" | "light") {
+  const dark = mode === "dark"
+  const bg = back(mode)
+  const base = dark ? "#7e7e7e" : "#8f8f8f"
+  const weak = dark ? "#343434" : "#dbdbdb"
+  const strong = dark ? "#ededed" : "#171717"
+  const track = dark ? "rgba(255,255,255,0.078)" : "rgba(0,0,0,0.051)"
+  const warn = dark ? "#fbb73c" : "#ebb76e"
+  const pulse = mark(base, strong)
+  const splash = mark(weak, strong)
+
+  return `<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <title>OpenCode</title>
+    <style>
+      :root {
+        color-scheme: ${mode};
+      }
+
+      html,
+      body {
+        width: 100%;
+        height: 100%;
+        margin: 0;
+        overflow: hidden;
+        background: ${bg};
+      }
+
+      body {
+        font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+      }
+
+      #root {
+        display: flex;
+        width: 100%;
+        height: 100%;
+        align-items: center;
+        justify-content: center;
+      }
+
+      #pulse,
+      #migrate {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+      }
+
+      #pulse[hidden],
+      #migrate[hidden] {
+        display: none;
+      }
+
+      #pulse svg {
+        width: 64px;
+        height: 80px;
+        opacity: 0.5;
+        animation: pulse 1.6s ease-in-out infinite;
+        transform-origin: center;
+      }
+
+      #migrate {
+        flex-direction: column;
+        gap: 44px;
+      }
+
+      #migrate svg {
+        width: 80px;
+        height: 100px;
+        opacity: 0.15;
+      }
+
+      #copy {
+        display: flex;
+        width: 240px;
+        flex-direction: column;
+        align-items: center;
+        gap: 16px;
+      }
+
+      #status {
+        width: 100%;
+        overflow: hidden;
+        color: ${strong};
+        text-align: center;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+        font-size: 14px;
+        line-height: 20px;
+      }
+
+      #bar {
+        width: 80px;
+        height: 4px;
+        overflow: hidden;
+        background: ${track};
+      }
+
+      #fill {
+        width: 25%;
+        height: 100%;
+        background: ${warn};
+      }
+
+      @keyframes pulse {
+        0%,
+        100% {
+          opacity: 0.5;
+        }
+
+        50% {
+          opacity: 0.15;
+        }
+      }
+    </style>
+  </head>
+  <body>
+    <div id="root">
+      <div id="pulse">${pulse}</div>
+      <div id="migrate" hidden>
+        ${splash}
+        <div id="copy" aria-live="polite">
+          <span id="status">Just a moment...</span>
+          <div id="bar"><div id="fill"></div></div>
+        </div>
+      </div>
+    </div>
+    <script>
+      ;(() => {
+        const lines = ["Just a moment...", "Migrating your database", "This may take a couple of minutes"]
+        const pulse = document.getElementById("pulse")
+        const migrate = document.getElementById("migrate")
+        const status = document.getElementById("status")
+        const fill = document.getElementById("fill")
+        let step = { phase: "server_waiting" }
+        let line = 0
+        let seen = false
+        let value = 0
+        let done = false
+
+        function render() {
+          const sql = step.phase === "sqlite_waiting" || (seen && step.phase === "done")
+          pulse.hidden = sql
+          migrate.hidden = !sql
+          if (!sql) return
+          status.textContent = step.phase === "done" ? "All done" : lines[line]
+          fill.style.width = String(step.phase === "done" ? 100 : Math.max(25, Math.min(100, value))) + "%"
+        }
+
+        function finish() {
+          if (done) return
+          done = true
+          window.api?.loadingWindowComplete?.()
+        }
+
+        function set(step_) {
+          step = step_ || step
+          render()
+          if (step.phase === "done") finish()
+        }
+
+        const timers = [3000, 9000].map((ms, i) =>
+          setTimeout(() => {
+            line = i + 1
+            render()
+          }, ms),
+        )
+
+        const off = window.api?.onInitStep?.((step_) => set(step_)) ?? (() => {})
+        const progress =
+          window.api?.onSqliteMigrationProgress?.((next) => {
+            seen = true
+            if (next.type === "InProgress") {
+              value = Math.max(0, Math.min(100, next.value))
+              step = { phase: "sqlite_waiting" }
+              render()
+              return
+            }
+            value = 100
+            step = { phase: "done" }
+            render()
+            finish()
+          }) ?? (() => {})
+
+        window.api?.awaitInitialization?.((step_) => set(step_))?.catch(() => undefined)
+
+        addEventListener("beforeunload", () => {
+          off()
+          progress()
+          timers.forEach(clearTimeout)
+        })
+
+        render()
+      })()
+    </script>
+  </body>
+</html>`
+}
+
+function mark(base: string, strong: string) {
+  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>`
+}
+
 function loadWindow(win: BrowserWindow, html: string) {
   const devUrl = process.env.ELECTRON_RENDERER_URL
   if (devUrl) {

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

@@ -16,7 +16,7 @@ const html = async (name: string) => Bun.file(join(dir, name)).text()
  * All local resource references must use relative paths (`./`).
  */
 describe("electron renderer html", () => {
-  for (const name of ["index.html", "loading.html"]) {
+  for (const name of ["index.html"]) {
     describe(name, () => {
       test("script src attributes use relative paths", async () => {
         const content = await html(name)

+ 0 - 22
packages/desktop-electron/src/renderer/loading.html

@@ -1,22 +0,0 @@
-<!doctype html>
-<html lang="en" style="background-color: var(--background-base)">
-  <head>
-    <meta charset="utf-8" />
-    <meta name="viewport" content="width=device-width, initial-scale=1" />
-    <title>OpenCode</title>
-    <link rel="icon" type="image/png" href="./favicon-96x96-v3.png" sizes="96x96" />
-    <link rel="icon" type="image/svg+xml" href="./favicon-v3.svg" />
-    <link rel="shortcut icon" href="./favicon-v3.ico" />
-    <link rel="apple-touch-icon" sizes="180x180" href="./apple-touch-icon-v3.png" />
-    <meta name="theme-color" content="#F8F7F7" />
-    <meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
-    <meta property="og:image" content="./social-share.png" />
-    <meta property="twitter:image" content="./social-share.png" />
-    <script id="oc-theme-preload-script" src="./oc-theme-preload.js"></script>
-  </head>
-  <body class="antialiased overscroll-none text-12-regular overflow-hidden">
-    <noscript>You need to enable JavaScript to run this app.</noscript>
-    <div id="root" class="flex flex-col h-dvh"></div>
-    <script src="./loading.tsx" type="module"></script>
-  </body>
-</html>

+ 0 - 89
packages/desktop-electron/src/renderer/loading.tsx

@@ -1,89 +0,0 @@
-import { MetaProvider } from "@solidjs/meta"
-import { render } from "solid-js/web"
-import "@opencode-ai/app/index.css"
-import { Font } from "@opencode-ai/ui/font"
-import { Splash } from "@opencode-ai/ui/logo"
-import { Progress } from "@opencode-ai/ui/progress"
-import "./styles.css"
-import { createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js"
-import type { InitStep, SqliteMigrationProgress } from "../preload/types"
-
-const root = document.getElementById("root")!
-const lines = ["Just a moment...", "Migrating your database", "This may take a couple of minutes"]
-const delays = [3000, 9000]
-
-render(() => {
-  const [step, setStep] = createSignal<InitStep | null>(null)
-  const [line, setLine] = createSignal(0)
-  const [percent, setPercent] = createSignal(0)
-
-  const phase = createMemo(() => step()?.phase)
-
-  const value = createMemo(() => {
-    if (phase() === "done") return 100
-    return Math.max(25, Math.min(100, percent()))
-  })
-
-  window.api.awaitInitialization((next) => setStep(next as InitStep)).catch(() => undefined)
-  const off = window.api.onInitStep((next) => setStep(next))
-
-  onMount(() => {
-    setLine(0)
-    setPercent(0)
-
-    const timers = delays.map((ms, i) => setTimeout(() => setLine(i + 1), ms))
-
-    const listener = window.api.onSqliteMigrationProgress((progress: SqliteMigrationProgress) => {
-      if (progress.type === "InProgress") setPercent(Math.max(0, Math.min(100, progress.value)))
-      if (progress.type === "Done") {
-        setPercent(100)
-        setStep({ phase: "done" })
-      }
-    })
-
-    onCleanup(() => {
-      off()
-      listener()
-      timers.forEach(clearTimeout)
-    })
-  })
-
-  createEffect(() => {
-    if (phase() !== "done") return
-
-    const timer = setTimeout(() => window.api.loadingWindowComplete(), 1000)
-    onCleanup(() => clearTimeout(timer))
-  })
-
-  const status = createMemo(() => {
-    if (phase() === "done") return "All done"
-    if (phase() === "sqlite_waiting") return lines[line()]
-    return "Just a moment..."
-  })
-
-  return (
-    <MetaProvider>
-      <div class="w-screen h-screen bg-background-base flex items-center justify-center">
-        <Font />
-        {phase() === "sqlite_waiting" ? (
-          <div class="flex flex-col items-center gap-11">
-            <Splash class="w-20 h-25 opacity-15" />
-            <div class="w-60 flex flex-col items-center gap-4" aria-live="polite">
-              <span class="w-full overflow-hidden text-center text-ellipsis whitespace-nowrap text-text-strong text-14-normal">
-                {status()}
-              </span>
-              <Progress
-                value={value()}
-                class="w-20 [&_[data-slot='progress-track']]:h-1 [&_[data-slot='progress-track']]:border-0 [&_[data-slot='progress-track']]:rounded-none [&_[data-slot='progress-track']]:bg-surface-weak [&_[data-slot='progress-fill']]:rounded-none [&_[data-slot='progress-fill']]:bg-icon-warning-base"
-                aria-label="Database migration progress"
-                getValueLabel={({ value }) => `${Math.round(value)}%`}
-              />
-            </div>
-          </div>
-        ) : (
-          <Splash class="w-16 h-20 opacity-50 animate-pulse" />
-        )}
-      </div>
-    </MetaProvider>
-  )
-}, root)