jan/web-app/src/hooks/useAppUpdater.ts
2025-08-14 12:39:48 +07:00

248 lines
6.8 KiB
TypeScript

import { isDev } from '@/lib/utils'
import { check, Update } from '@tauri-apps/plugin-updater'
import { useState, useCallback, useEffect } from 'react'
import { events, AppEvent } from '@janhq/core'
import { emit } from '@tauri-apps/api/event'
import { SystemEvent } from '@/types/events'
import { stopAllModels } from '@/services/models'
export interface UpdateState {
isUpdateAvailable: boolean
updateInfo: Update | null
isDownloading: boolean
downloadProgress: number
downloadedBytes: number
totalBytes: number
remindMeLater: boolean
}
export const useAppUpdater = () => {
const [updateState, setUpdateState] = useState<UpdateState>({
isUpdateAvailable: false,
updateInfo: null,
isDownloading: false,
downloadProgress: 0,
downloadedBytes: 0,
totalBytes: 0,
remindMeLater: false,
})
// Listen for app update state sync events
useEffect(() => {
const handleUpdateStateSync = (newState: Partial<UpdateState>) => {
setUpdateState((prev) => ({
...prev,
...newState,
}))
}
events.on('onAppUpdateStateSync', handleUpdateStateSync)
return () => {
events.off('onAppUpdateStateSync', handleUpdateStateSync)
}
}, [])
const syncStateToOtherInstances = useCallback(
(partialState: Partial<UpdateState>) => {
// Emit event to sync state across all useAppUpdater instances
events.emit('onAppUpdateStateSync', partialState)
},
[]
)
const checkForUpdate = useCallback(
async (resetRemindMeLater = false) => {
if (AUTO_UPDATER_DISABLED) {
console.log('Auto updater is disabled')
return
}
try {
// Reset remindMeLater if requested (e.g., when called from settings)
if (resetRemindMeLater) {
const newState = {
remindMeLater: false,
}
setUpdateState((prev) => ({
...prev,
...newState,
}))
// Sync to other instances
syncStateToOtherInstances(newState)
}
if (!isDev()) {
// Production mode - use actual Tauri updater
const update = await check()
if (update) {
const newState = {
isUpdateAvailable: true,
remindMeLater: false,
updateInfo: update,
}
setUpdateState((prev) => ({
...prev,
...newState,
}))
// Sync to other instances
syncStateToOtherInstances(newState)
console.log('Update available:', update.version)
return update
} else {
// No update available - reset state
const newState = {
isUpdateAvailable: false,
updateInfo: null,
}
setUpdateState((prev) => ({
...prev,
...newState,
}))
// Sync to other instances
syncStateToOtherInstances(newState)
return null
}
} else {
const newState = {
isUpdateAvailable: false,
updateInfo: null,
...(resetRemindMeLater && { remindMeLater: false }),
}
setUpdateState((prev) => ({
...prev,
...newState,
}))
// Sync to other instances
syncStateToOtherInstances(newState)
return null
}
} catch (error) {
console.error('Error checking for updates:', error)
// Reset state on error
const newState = {
isUpdateAvailable: false,
updateInfo: null,
}
setUpdateState((prev) => ({
...prev,
...newState,
}))
// Sync to other instances
syncStateToOtherInstances(newState)
return null
}
},
[syncStateToOtherInstances]
)
const setRemindMeLater = useCallback(
(remind: boolean) => {
const newState = {
remindMeLater: remind,
}
setUpdateState((prev) => ({
...prev,
...newState,
}))
// Sync to other instances
syncStateToOtherInstances(newState)
},
[syncStateToOtherInstances]
)
const downloadAndInstallUpdate = useCallback(async () => {
if (AUTO_UPDATER_DISABLED) {
console.log('Auto updater is disabled')
return
}
if (!updateState.updateInfo) return
try {
setUpdateState((prev) => ({
...prev,
isDownloading: true,
}))
let downloaded = 0
let contentLength = 0
await stopAllModels()
emit(SystemEvent.KILL_SIDECAR)
await new Promise((resolve) => setTimeout(resolve, 1000))
await updateState.updateInfo.downloadAndInstall((event) => {
switch (event.event) {
case 'Started':
contentLength = event.data.contentLength || 0
setUpdateState((prev) => ({
...prev,
totalBytes: contentLength,
}))
console.log(`Started downloading ${contentLength} bytes`)
// Emit app update download started event
events.emit(AppEvent.onAppUpdateDownloadUpdate, {
progress: 0,
downloadedBytes: 0,
totalBytes: contentLength,
})
break
case 'Progress': {
downloaded += event.data.chunkLength
const progress = contentLength > 0 ? downloaded / contentLength : 0
setUpdateState((prev) => ({
...prev,
downloadProgress: progress,
downloadedBytes: downloaded,
}))
console.log(`Downloaded ${downloaded} from ${contentLength}`)
// Emit app update download progress event
events.emit(AppEvent.onAppUpdateDownloadUpdate, {
progress: progress,
downloadedBytes: downloaded,
totalBytes: contentLength,
})
break
}
case 'Finished':
console.log('Download finished')
setUpdateState((prev) => ({
...prev,
isDownloading: false,
downloadProgress: 1,
}))
// Emit app update download success event
events.emit(AppEvent.onAppUpdateDownloadSuccess, {})
break
}
})
await window.core?.api?.relaunch()
console.log('Update installed')
} catch (error) {
console.error('Error downloading update:', error)
setUpdateState((prev) => ({
...prev,
isDownloading: false,
}))
// Emit app update download error event
events.emit(AppEvent.onAppUpdateDownloadError, {
message: error instanceof Error ? error.message : 'Unknown error',
})
}
}, [updateState.updateInfo])
return {
updateState,
checkForUpdate,
downloadAndInstallUpdate,
setRemindMeLater,
}
}