Create standalone browser port of LTX Athena with authenticated native API transport
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import type { UpdateStatePayload } from '../../shared/electron-api-schema'
|
||||
import { useIsGenerationActive } from '../lib/generation-active'
|
||||
|
||||
export type AppUpdate = {
|
||||
state: UpdateStatePayload
|
||||
checkForUpdates: () => Promise<void>
|
||||
startDownload: () => Promise<void>
|
||||
installAndRestart: () => Promise<{ success: true } | { success: false; error: string }>
|
||||
skipVersion: (version: string) => Promise<void>
|
||||
}
|
||||
|
||||
const INITIAL: UpdateStatePayload = { status: 'idle', currentVersion: '' }
|
||||
|
||||
const MODAL_STATUSES: ReadonlySet<UpdateStatePayload['status']> = new Set([
|
||||
'available',
|
||||
'downloading',
|
||||
'downloaded',
|
||||
])
|
||||
|
||||
export function useAppUpdate(): AppUpdate {
|
||||
const [state, setState] = useState<UpdateStatePayload>(INITIAL)
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true
|
||||
let fromEvent = false
|
||||
// Subscribe first so a check that starts during getUpdateState cannot be missed,
|
||||
// then ignore the snapshot if an event already applied a newer value.
|
||||
const unsubscribe = window.electronAPI.onUpdateEvent((data) => {
|
||||
fromEvent = true
|
||||
if (alive) setState(data)
|
||||
})
|
||||
void window.electronAPI.getUpdateState()
|
||||
.then((s) => { if (alive && !fromEvent) setState(s) })
|
||||
.catch(() => {})
|
||||
return () => { alive = false; unsubscribe() }
|
||||
}, [])
|
||||
|
||||
const checkForUpdates = useCallback(async () => {
|
||||
await window.electronAPI.checkForUpdatesNow()
|
||||
}, [])
|
||||
const startDownload = useCallback(async () => {
|
||||
await window.electronAPI.startUpdateDownload()
|
||||
}, [])
|
||||
const installAndRestart = useCallback(async () => {
|
||||
return window.electronAPI.installUpdateAndRestart()
|
||||
}, [])
|
||||
const skipVersion = useCallback(async (version: string) => {
|
||||
await window.electronAPI.skipUpdateVersion({ version })
|
||||
}, [])
|
||||
|
||||
return { state, checkForUpdates, startDownload, installAndRestart, skipVersion }
|
||||
}
|
||||
|
||||
/** Session Later / skip / manual-check intent. App only mounts the modal. */
|
||||
export function useAppUpdateModal() {
|
||||
const update = useAppUpdate()
|
||||
const isGenerationActive = useIsGenerationActive()
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
const [manualCheckPending, setManualCheckPending] = useState(false)
|
||||
const [laterVersion, setLaterVersion] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
// Mac has no modal: silent download + install-on-quit, gated only by the About toggle.
|
||||
if (window.electronAPI.platform === 'darwin') return
|
||||
const s = update.state
|
||||
if (s.status === 'available') {
|
||||
if (manualCheckPending || s.version !== laterVersion) {
|
||||
setModalOpen(true)
|
||||
}
|
||||
if (manualCheckPending) setManualCheckPending(false)
|
||||
} else if (s.status === 'downloaded') {
|
||||
// Bits are on disk: the modal is the only install path and must stay up,
|
||||
// including after Hide-during-download.
|
||||
setModalOpen(true)
|
||||
} else if (s.status === 'not-available') {
|
||||
if (manualCheckPending) setManualCheckPending(false)
|
||||
}
|
||||
}, [update.state.status, update.state.version, manualCheckPending, laterVersion])
|
||||
|
||||
const requestCheck = update.checkForUpdates
|
||||
const skipVersion = update.skipVersion
|
||||
const version = update.state.version
|
||||
const status = update.state.status
|
||||
|
||||
const checkForUpdates = useCallback(() => {
|
||||
setManualCheckPending(true)
|
||||
void requestCheck()
|
||||
}, [requestCheck])
|
||||
|
||||
const closeModal = useCallback((skipThisVersion: boolean) => {
|
||||
if (status === 'downloaded') return
|
||||
if (skipThisVersion && version) void skipVersion(version)
|
||||
// Hide during download is not Later — keep the session prompt so a failed
|
||||
// download can reopen the modal. Later/skip only apply when dismissing the offer.
|
||||
else if (version && status !== 'downloading') setLaterVersion(version)
|
||||
setModalOpen(false)
|
||||
}, [skipVersion, version, status])
|
||||
|
||||
const openModal = useCallback(() => setModalOpen(true), [])
|
||||
|
||||
return {
|
||||
update,
|
||||
isGenerationActive,
|
||||
// Keep the modal mounted across a periodic re-check (`checking`) so it does not
|
||||
// unmount/remount. Do not treat `available` as busy in main — that would hide a
|
||||
// newer version after the user clicked Later.
|
||||
isModalOpen:
|
||||
window.electronAPI.platform !== 'darwin'
|
||||
&& modalOpen
|
||||
&& (MODAL_STATUSES.has(status) || status === 'checking'),
|
||||
openModal,
|
||||
closeModal,
|
||||
checkForUpdates,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user