import { Popover, PopoverContent, PopoverTrigger, } from '@/components/ui/popover' import { Progress } from '@/components/ui/progress' import { useDownloadStore } from '@/hooks/useDownloadStore' import { useLeftPanel } from '@/hooks/useLeftPanel' import { useModelProvider } from '@/hooks/useModelProvider' import { useAppUpdater } from '@/hooks/useAppUpdater' import { abortDownload } from '@/services/models' import { getProviders } from '@/services/providers' import { DownloadEvent, DownloadState, events, AppEvent } from '@janhq/core' import { IconDownload, IconX } from '@tabler/icons-react' import { useCallback, useEffect, useMemo, useState } from 'react' import { toast } from 'sonner' export function DownloadManagement() { const { setProviders } = useModelProvider() const { open: isLeftPanelOpen } = useLeftPanel() const [isPopoverOpen, setIsPopoverOpen] = useState(false) const { downloads, updateProgress, removeDownload } = useDownloadStore() const { updateState } = useAppUpdater() const [appUpdateState, setAppUpdateState] = useState({ isDownloading: false, downloadProgress: 0, downloadedBytes: 0, totalBytes: 0, }) useEffect(() => { setAppUpdateState({ isDownloading: updateState.isDownloading, downloadProgress: updateState.downloadProgress, downloadedBytes: updateState.downloadedBytes, totalBytes: updateState.totalBytes, }) }, [updateState]) const onAppUpdateDownloadUpdate = useCallback( (data: { progress?: number downloadedBytes?: number totalBytes?: number }) => { setAppUpdateState((prev) => ({ ...prev, isDownloading: true, downloadProgress: data.progress || 0, downloadedBytes: data.downloadedBytes || 0, totalBytes: data.totalBytes || 0, })) }, [] ) const onAppUpdateDownloadSuccess = useCallback(() => { setAppUpdateState((prev) => ({ ...prev, isDownloading: false, downloadProgress: 1, })) toast.success('App Update Downloaded', { description: 'The app update has been downloaded successfully.', }) }, []) const onAppUpdateDownloadError = useCallback(() => { setAppUpdateState((prev) => ({ ...prev, isDownloading: false, })) toast.error('App Update Download Failed', { description: 'Failed to download the app update. Please try again.', }) }, []) const downloadCount = useMemo(() => { const modelDownloads = Object.keys(downloads).length const appUpdateDownload = appUpdateState.isDownloading ? 1 : 0 const total = modelDownloads + appUpdateDownload return total }, [downloads, appUpdateState.isDownloading]) const downloadProcesses = useMemo( () => Object.values(downloads).map((download) => ({ id: download.name, name: download.name, progress: download.progress, current: download.current, total: download.total, })), [downloads] ) const overallProgress = useMemo(() => { const modelTotal = downloadProcesses.reduce((acc, download) => { return acc + download.total }, 0) const modelCurrent = downloadProcesses.reduce((acc, download) => { return acc + download.current }, 0) // Include app update progress in overall calculation const appUpdateTotal = appUpdateState.isDownloading ? appUpdateState.totalBytes : 0 const appUpdateCurrent = appUpdateState.isDownloading ? appUpdateState.downloadedBytes : 0 const total = modelTotal + appUpdateTotal const current = modelCurrent + appUpdateCurrent return total > 0 ? current / total : 0 }, [ downloadProcesses, appUpdateState.isDownloading, appUpdateState.totalBytes, appUpdateState.downloadedBytes, ]) const onFileDownloadUpdate = useCallback( async (state: DownloadState) => { console.debug('onFileDownloadUpdate', state) updateProgress( state.modelId, state.percent, state.modelId, state.size?.transferred, state.size?.total ) }, [updateProgress] ) const onFileDownloadError = useCallback( (state: DownloadState) => { console.debug('onFileDownloadError', state) removeDownload(state.modelId) }, [removeDownload] ) const onFileDownloadStopped = useCallback( (state: DownloadState) => { console.debug('onFileDownloadError', state) removeDownload(state.modelId) }, [removeDownload] ) const onFileDownloadSuccess = useCallback( async (state: DownloadState) => { console.debug('onFileDownloadSuccess', state) removeDownload(state.modelId) getProviders().then(setProviders) toast.success('Download Complete', { id: 'download-complete', description: `The model ${state.modelId} has been downloaded`, }) }, [removeDownload, setProviders] ) useEffect(() => { console.debug('DownloadListener: registering event listeners...') events.on(DownloadEvent.onFileDownloadUpdate, onFileDownloadUpdate) events.on(DownloadEvent.onFileDownloadError, onFileDownloadError) events.on(DownloadEvent.onFileDownloadSuccess, onFileDownloadSuccess) events.on(DownloadEvent.onFileDownloadStopped, onFileDownloadStopped) // Register app update event listeners events.on(AppEvent.onAppUpdateDownloadUpdate, onAppUpdateDownloadUpdate) events.on(AppEvent.onAppUpdateDownloadSuccess, onAppUpdateDownloadSuccess) events.on(AppEvent.onAppUpdateDownloadError, onAppUpdateDownloadError) return () => { console.debug('DownloadListener: unregistering event listeners...') events.off(DownloadEvent.onFileDownloadUpdate, onFileDownloadUpdate) events.off(DownloadEvent.onFileDownloadError, onFileDownloadError) events.off(DownloadEvent.onFileDownloadSuccess, onFileDownloadSuccess) events.off(DownloadEvent.onFileDownloadStopped, onFileDownloadStopped) // Unregister app update event listeners events.off(AppEvent.onAppUpdateDownloadUpdate, onAppUpdateDownloadUpdate) events.off( AppEvent.onAppUpdateDownloadSuccess, onAppUpdateDownloadSuccess ) events.off(AppEvent.onAppUpdateDownloadError, onAppUpdateDownloadError) } }, [ onFileDownloadUpdate, onFileDownloadError, onFileDownloadSuccess, onFileDownloadStopped, onAppUpdateDownloadUpdate, onAppUpdateDownloadSuccess, onAppUpdateDownloadError, ]) function renderGB(bytes: number): string { const gb = bytes / 1024 ** 3 return ((gb * 100) / 100).toFixed(2) } return ( <> {downloadCount > 0 && ( {isLeftPanelOpen ? (
{downloadCount}

Downloads

{Math.round(overallProgress * 100)}%
) : (
{downloadCount}
)}
e.preventDefault} >

Downloading

{appUpdateState.isDownloading && (

App Update

{`${renderGB(appUpdateState.downloadedBytes)} / ${renderGB(appUpdateState.totalBytes)}`}{' '} GB ({Math.round(appUpdateState.downloadProgress * 100)}%)

)} {downloadProcesses.map((download) => (

{download.name}

{ abortDownload(download.name).then(() => { toast.info('Download Cancelled', { id: 'cancel-download', description: 'The download process was cancelled', }) if (downloadProcesses.length === 0) { setIsPopoverOpen(false) } }) }} />

{`${renderGB(download.current)} / ${renderGB(download.total)}`}{' '} GB ({Math.round(download.progress * 100)}%)

))}
)} ) }