Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 20 additions & 30 deletions electron/main/updater.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,24 @@
import { app, BrowserWindow } from 'electron'
import { autoUpdater } from 'electron-updater'
import { existsSync, rmSync, writeFileSync, readFileSync } from 'fs'
import { join } from 'path'
import { logger } from './logger'

type WindowGetter = () => BrowserWindow | null

function pendingFilePath(): string {
return join(app.getPath('userData'), '.last-update-installer')
}

function cleanupLastInstaller(): void {
const marker = pendingFilePath()
if (!existsSync(marker)) return
try {
const installerPath = readFileSync(marker, 'utf-8').trim()
if (existsSync(installerPath)) {
rmSync(installerPath)
logger.info(`[updater] Cleaned up installer: ${installerPath}`)
}
rmSync(marker)
} catch {}
}

export function initAutoUpdater(getWindow: WindowGetter): void {
cleanupLastInstaller()

// eslint-disable-next-line @typescript-eslint/no-explicit-any
autoUpdater.logger = logger as any
autoUpdater.autoDownload = false
autoUpdater.autoInstallOnAppQuit = true
autoUpdater.autoDownload = false
autoUpdater.autoInstallOnAppQuit = false

autoUpdater.on('update-available', (info) => {
const running = app.getVersion()
const running = app.getVersion()
const incoming = info.version
const [rMaj, rMin] = running.split('.').map(Number)
const [iMaj, iMin] = incoming.split('.').map(Number)
const isPatch = rMaj === iMaj && rMin === iMin

if (isPatch) {
logger.info(`[updater] Patch update ${incoming} available — downloading silently`)
logger.info(`[updater] Patch ${incoming} available — downloading`)
autoUpdater.downloadUpdate().catch((err: Error) => {
logger.error(`[updater] Download failed: ${err.message}`)
})
Expand All @@ -50,11 +29,12 @@ export function initAutoUpdater(getWindow: WindowGetter): void {
})

autoUpdater.on('update-downloaded', (info) => {
logger.info(`[updater] Patch update ${info.version} downloaded — showing badge`)
try {
writeFileSync(pendingFilePath(), info.downloadedFile, 'utf-8')
} catch {}
getWindow()?.webContents.send('updater:patch-ready', { version: info.version })
logger.info(`[updater] Patch ${info.version} downloaded — applying now`)
getWindow()?.webContents.send('updater:applying', { version: info.version })
// Small delay so the renderer can render the "Applying…" panel before quit
setTimeout(() => {
autoUpdater.quitAndInstall(true, true)
}, 800)
})

autoUpdater.on('update-not-available', () => {
Expand All @@ -64,4 +44,14 @@ export function initAutoUpdater(getWindow: WindowGetter): void {
autoUpdater.on('error', (err: Error) => {
logger.error(`[updater] Error: ${err.message}`)
})

// Check immediately on startup so it can apply during the setup screen
autoUpdater.checkForUpdates().catch((err: Error) => {
logger.error(`[updater] Initial check failed: ${err.message}`)
})

// Re-check every 2 hours for long-running sessions
setInterval(() => {
autoUpdater.checkForUpdates().catch(() => {})
}, 2 * 60 * 60 * 1000)
}
6 changes: 3 additions & 3 deletions electron/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,10 +174,10 @@ contextBridge.exposeInMainWorld('electron', {
ipcRenderer.invoke('updater:check'),
quitAndInstall: (): Promise<void> =>
ipcRenderer.invoke('updater:quitAndInstall'),
onPatchReady: (cb: (data: { version: string }) => void) => {
ipcRenderer.on('updater:patch-ready', (_event, data) => cb(data))
onApplying: (cb: (data: { version: string }) => void) => {
ipcRenderer.on('updater:applying', (_event, data) => cb(data))
},
offPatchReady: () => ipcRenderer.removeAllListeners('updater:patch-ready'),
offApplying: () => ipcRenderer.removeAllListeners('updater:applying'),
onMajorMinorAvailable: (cb: (data: { version: string }) => void) => {
ipcRenderer.on('updater:major-minor-available', (_event, data) => cb(data))
},
Expand Down
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "modly",
"version": "0.2.1",
"version": "0.3.0",
"description": "Local AI-powered 3D mesh generation from images",
"main": "./out/main/index.js",
"author": "Modly",
Expand Down Expand Up @@ -75,8 +75,7 @@
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"disableWebInstaller": true
"allowToChangeInstallationDirectory": true
},
"win": {
"target": "nsis",
Expand Down
11 changes: 2 additions & 9 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,18 @@ import { UpdateModal } from '@shared/components/ui/UpdateModal'
import { ErrorModal } from '@shared/components/ui/ErrorModal'

export default function App(): JSX.Element {
const { checkSetup, setupStatus, initApp, backendStatus, showError, setPatchUpdateReady } = useAppStore()
const { checkSetup, setupStatus, initApp, backendStatus, showError } = useAppStore()
const [updateVersion, setUpdateVersion] = useState<string | null>(null)
const [currentVersion, setCurrentVersion] = useState<string>('')

useEffect(() => {
checkSetup()
window.electron.app.onError((message) => showError(message))
window.electron.updater.onPatchReady(() => {
setPatchUpdateReady(true)
})
window.electron.updater.onMajorMinorAvailable(({ version }) => {
setUpdateVersion(`v${version}`)
})
return () => {
window.electron.app.offError()
window.electron.updater.offPatchReady()
window.electron.updater.offMajorMinorAvailable()
}
}, [])
Expand All @@ -32,10 +28,7 @@ export default function App(): JSX.Element {

useEffect(() => {
if (backendStatus !== 'ready') return
window.electron.app.info().then(({ version }) => {
setCurrentVersion(version)
window.electron.updater.check()
})
window.electron.app.info().then(({ version }) => setCurrentVersion(version))
}, [backendStatus])

if (backendStatus === 'ready') return (
Expand Down
29 changes: 29 additions & 0 deletions src/areas/setup/FirstRunSetup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,28 @@ function StartingPanel(): JSX.Element {
)
}

function ApplyingUpdatePanel({ version }: { version: string }): JSX.Element {
return (
<div className="w-80 bg-surface-300 rounded-xl p-6">
<div className="flex items-center gap-3 mb-4">
<div className="w-8 h-8 rounded-lg bg-accent/15 border border-accent/25 flex items-center justify-center shrink-0">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-accent-light">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/>
</svg>
</div>
<div>
<p className="text-sm font-medium text-zinc-100">Applying update {version}</p>
<p className="text-xs text-zinc-500 mt-0.5">The app will restart automatically</p>
</div>
</div>
<div className="h-1 bg-zinc-800 rounded-full overflow-hidden">
<div className="h-full bg-accent rounded-full animate-pulse" style={{ width: '70%' }} />
</div>
</div>
)
}

function ErrorPanel({ message }: { message: string | null }): JSX.Element {
return (
<div className="w-80 bg-surface-300 rounded-xl p-6">
Expand All @@ -190,8 +212,15 @@ function ErrorPanel({ message }: { message: string | null }): JSX.Element {
export default function FirstRunSetup(): JSX.Element {
const { setupStatus, setupProgress, setupError, saveDataDir, defaultDataDir, backendStatus, backendError } =
useAppStore()
const [applyingVersion, setApplyingVersion] = useState<string | null>(null)

useEffect(() => {
window.electron.updater.onApplying(({ version }) => setApplyingVersion(`v${version}`))
return () => { window.electron.updater.offApplying() }
}, [])

const renderPanel = () => {
if (applyingVersion) return <ApplyingUpdatePanel version={applyingVersion} />
switch (setupStatus) {
case 'idle':
case 'checking':
Expand Down
Loading