mirror of
https://github.com/anomalyco/opencode.git
synced 2026-04-23 22:34:53 +00:00
chore: simpler splash window
This commit is contained in:
@@ -33,7 +33,6 @@ export default defineConfig({
|
||||
rollupOptions: {
|
||||
input: {
|
||||
main: "src/renderer/index.html",
|
||||
loading: "src/renderer/loading.html",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user