V0.0.3 (#12)
* feat(editor): add github releases auto update flow Enable desktop auto-update checks and install prompt so packaged apps can update from published GitHub Releases. * fix(canvas): keep frame labels aligned during zoom Draw labels in viewport space with zoom-aware positioning so frame titles stay anchored while panning and zooming. * fix(ai): disable chat interactions when no models connected Prevent sending and shortcut actions when provider models are unavailable, and show a concise model picker hint to avoid false-available UI states. * docs: refresh readme community and demo section Embed the product demo video inline, add a styled Discord community entry, and update media assets for clearer README presentation. * chore: update nitro external dependency settings Align SSR bundling with current runtime usage by removing the unnecessary @opencode-ai external entries. * feat(electron): enhance macOS build process and update auto-updater - Added a new build script for macOS that allows building for both arm64 and x64 architectures. - Updated GitHub Actions workflow to support separate build arguments for macOS arm64 and x64. - Implemented a custom GitHub update provider for macOS to handle versioning based on architecture. - Introduced a new icon for the electron app and removed the old logo image. * feat(editor): enhance update ready banner with additional states and UI improvements - Added new state management for checking updates and dismissed status. - Improved visibility logic for the update banner based on update states. - Enhanced UI to display current and latest version information, download progress, and error messages. - Updated layout and styling for better user experience and accessibility. * docs: replace stripped video embed with clickable cover link GitHub sanitizes README HTML video tags, so the embedded demo player does not render on repository pages. Use a clickable poster image and explicit watch link to preserve the demo entry point across GitHub README rendering. --------- Co-authored-by: Fini <fini.yang@gmail.com>
This commit is contained in:
parent
198a2685a6
commit
f3f3bf19b3
|
|
@ -18,9 +18,12 @@ Open-source vector design tool with a **Design-as-Code** philosophy. An alternat
|
|||
Design visually on an infinite canvas, generate code from designs, and let AI build entire screens from a single prompt — all in one tool that runs in your browser or as a native desktop app.
|
||||
|
||||
<p align="center">
|
||||
<video controls playsinline preload="metadata" poster="./screenshot/op-cover.png" width="100%">
|
||||
<source src="https://i.imgur.com/yiu2GTt_lq.mp4" type="video/mp4" />
|
||||
</video>
|
||||
<a href="https://oss.ioa.tech/zseven/openpencil/a46e24733239ce24de36702342201033.mp4">
|
||||
<img src="./screenshot/op-cover.png" alt="OpenPencil demo video (click to play)" width="100%" />
|
||||
</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://oss.ioa.tech/zseven/openpencil/a46e24733239ce24de36702342201033.mp4"><strong>▶ Watch demo video</strong></a>
|
||||
</p>
|
||||
|
||||
## Highlights
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import { AlertCircle, CheckCircle2, Download, Loader2, RefreshCw, Sparkles } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
export default function UpdateReadyBanner() {
|
||||
const [updateState, setUpdateState] = useState<UpdaterState | null>(null)
|
||||
const [isInstalling, setIsInstalling] = useState(false)
|
||||
const [isChecking, setIsChecking] = useState(false)
|
||||
const [dismissed, setDismissed] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const updater = window.electronAPI?.updater
|
||||
|
|
@ -29,6 +31,13 @@ export default function UpdateReadyBanner() {
|
|||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!updateState) return
|
||||
if (updateState.status === 'available' || updateState.status === 'downloading' || updateState.status === 'downloaded' || updateState.status === 'error') {
|
||||
setDismissed(false)
|
||||
}
|
||||
}, [updateState])
|
||||
|
||||
const handleInstall = async () => {
|
||||
if (!window.electronAPI?.updater) return
|
||||
|
||||
|
|
@ -39,39 +48,173 @@ export default function UpdateReadyBanner() {
|
|||
}
|
||||
}
|
||||
|
||||
if (!updateState || updateState.status !== 'downloaded') {
|
||||
const handleCheckUpdates = async () => {
|
||||
if (!window.electronAPI?.updater) return
|
||||
setIsChecking(true)
|
||||
try {
|
||||
const next = await window.electronAPI.updater.checkForUpdates()
|
||||
setUpdateState(next)
|
||||
} finally {
|
||||
setIsChecking(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!updateState) {
|
||||
return null
|
||||
}
|
||||
|
||||
const visible =
|
||||
!dismissed
|
||||
&& (updateState.status === 'checking'
|
||||
|| updateState.status === 'available'
|
||||
|| updateState.status === 'downloading'
|
||||
|| updateState.status === 'downloaded'
|
||||
|| updateState.status === 'error')
|
||||
|
||||
if (!visible) {
|
||||
return null
|
||||
}
|
||||
|
||||
const progress = Math.max(0, Math.min(100, Math.round(updateState.downloadProgress ?? 0)))
|
||||
const releaseDate = updateState.releaseDate
|
||||
? new Date(updateState.releaseDate).toLocaleDateString()
|
||||
: null
|
||||
|
||||
const titleByStatus: Partial<Record<UpdaterStatus, string>> = {
|
||||
checking: 'Checking for updates',
|
||||
available: 'Update found',
|
||||
downloading: 'Downloading update',
|
||||
downloaded: 'Ready to install',
|
||||
error: 'Update failed',
|
||||
}
|
||||
|
||||
const subtitleByStatus: Partial<Record<UpdaterStatus, string>> = {
|
||||
checking: 'Looking for the latest release...',
|
||||
available: updateState.latestVersion ? `Version ${updateState.latestVersion} is available.` : 'A new version is available.',
|
||||
downloading: updateState.latestVersion
|
||||
? `Version ${updateState.latestVersion} is downloading in the background.`
|
||||
: 'Downloading update package in the background.',
|
||||
downloaded: updateState.latestVersion
|
||||
? `Version ${updateState.latestVersion} has been downloaded.`
|
||||
: 'The update has been downloaded.',
|
||||
error: updateState.error || 'Unable to check or download the update.',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed top-6 left-1/2 -translate-x-1/2 z-50 app-region-no-drag">
|
||||
<div className="w-[760px] max-w-[calc(100vw-32px)] rounded-2xl border border-border bg-card/95 backdrop-blur-md shadow-2xl px-6 py-5 sm:px-8 sm:py-6 flex items-center justify-between gap-5 sm:gap-8">
|
||||
<div className="min-w-0">
|
||||
<p className="text-2xl sm:text-3xl font-semibold text-card-foreground leading-tight">
|
||||
Update Ready To Install
|
||||
</p>
|
||||
<p className="mt-2 text-sm sm:text-lg text-muted-foreground">
|
||||
{updateState.latestVersion ? `Version ${updateState.latestVersion} has been downloaded.` : 'A new version has been downloaded.'}
|
||||
</p>
|
||||
<p className="text-sm sm:text-lg text-muted-foreground">
|
||||
Can take 10-15 seconds for the app to relaunch automatically.
|
||||
</p>
|
||||
<div className="fixed top-5 right-5 z-50 app-region-no-drag">
|
||||
<div className="w-[460px] max-w-[calc(100vw-24px)] rounded-2xl border border-border/80 bg-card/95 backdrop-blur-md shadow-2xl overflow-hidden">
|
||||
<div className="px-5 py-4 border-b border-border/70 bg-gradient-to-r from-foreground/5 to-transparent">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="inline-flex items-center gap-2 text-[11px] uppercase tracking-[0.16em] text-muted-foreground">
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
Software Update
|
||||
</div>
|
||||
<p className="mt-1 text-base font-semibold text-card-foreground">
|
||||
{titleByStatus[updateState.status] || 'Software Update'}
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground leading-relaxed break-words">
|
||||
{subtitleByStatus[updateState.status]}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDismissed(true)}
|
||||
className="h-7 px-2 rounded-md text-xs text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleInstall}
|
||||
disabled={isInstalling}
|
||||
className="h-11 sm:h-12 px-4 sm:px-6 text-sm sm:text-lg font-semibold whitespace-nowrap bg-foreground text-background hover:bg-foreground/90"
|
||||
>
|
||||
{isInstalling
|
||||
? (
|
||||
<>
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
Installing...
|
||||
</>
|
||||
)
|
||||
: 'Restart & Install'}
|
||||
</Button>
|
||||
<div className="px-5 py-4 space-y-3">
|
||||
<div className="grid grid-cols-2 gap-2 text-xs text-muted-foreground">
|
||||
<div className="rounded-lg border border-border/70 px-3 py-2">
|
||||
<span className="block text-[10px] uppercase tracking-wider mb-1">Current</span>
|
||||
<span className="text-foreground font-medium">{updateState.currentVersion || 'Unknown'}</span>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border/70 px-3 py-2">
|
||||
<span className="block text-[10px] uppercase tracking-wider mb-1">Latest</span>
|
||||
<span className="text-foreground font-medium">{updateState.latestVersion || 'Checking...'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(updateState.status === 'downloading' || updateState.status === 'available') && (
|
||||
<div className="rounded-lg border border-border/70 p-3">
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground mb-2">
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<Download className="h-3.5 w-3.5" />
|
||||
Download Progress
|
||||
</span>
|
||||
<span className="font-medium text-foreground">{progress}%</span>
|
||||
</div>
|
||||
<div className="h-2 rounded-full bg-muted overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-foreground/90 transition-all duration-300 ease-out"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{updateState.status === 'error' && (
|
||||
<div className="rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-2 text-xs text-red-700 dark:text-red-300 inline-flex items-start gap-2">
|
||||
<AlertCircle className="h-4 w-4 mt-0.5 shrink-0" />
|
||||
<span className="leading-relaxed break-words">{updateState.error || 'Unknown updater error.'}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{updateState.status === 'downloaded' && (
|
||||
<div className="rounded-lg border border-emerald-500/25 bg-emerald-500/10 px-3 py-2 text-xs text-emerald-700 dark:text-emerald-300 inline-flex items-center gap-2">
|
||||
<CheckCircle2 className="h-4 w-4 shrink-0" />
|
||||
<span>Restart to apply update. Relaunch usually takes 10-15 seconds.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{releaseDate && (
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
Release date: {releaseDate}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-end gap-2 pt-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleCheckUpdates}
|
||||
disabled={isChecking || isInstalling}
|
||||
className="h-9"
|
||||
>
|
||||
{(isChecking || updateState.status === 'checking')
|
||||
? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Checking...
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
Check Again
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={handleInstall}
|
||||
disabled={isInstalling || updateState.status !== 'downloaded'}
|
||||
className="h-9 bg-foreground text-background hover:bg-foreground/90"
|
||||
>
|
||||
{isInstalling
|
||||
? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Installing...
|
||||
</>
|
||||
)
|
||||
: 'Restart & Install'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue