Files
Cultivation/src/utils/download.ts
2025-11-27 19:43:08 -07:00

200 lines
6.1 KiB
TypeScript

import { invoke } from '@tauri-apps/api/core'
import { listen } from '@tauri-apps/api/event'
import { byteToString } from './string'
export default class DownloadHandler {
downloads: {
path: string
progress: number
total: number
total_downloaded: number
status: string
startTime: number
error?: string
speed?: string
onFinish?: () => void
}[]
// Pass tauri invoke function
constructor() {
this.downloads = []
listen('download_progress', ({ payload }) => {
// @ts-expect-error Payload may be unknown but backend always returns this object
const obj: {
downloaded: string
total: string
path: string
total_downloaded: string
} = payload
const index = this.downloads.findIndex((download) => download.path === obj.path)
this.downloads[index].progress = parseInt(obj.downloaded, 10)
this.downloads[index].total = parseInt(obj.total, 10)
this.downloads[index].total_downloaded = parseInt(obj.total_downloaded, 10)
// Set download speed based on startTime
const now = Date.now()
const timeDiff = now - this.downloads[index].startTime
let speed = (this.downloads[index].progress / timeDiff) * 1000
if (this.downloads[index].total === 0) {
// If our total is 0, then we are downloading a file without a size
// Calculate the average speed based total_downloaded and startTme
speed = (this.downloads[index].total_downloaded / timeDiff) * 1000
}
this.downloads[index].speed = byteToString(speed) + '/s'
})
listen('download_finished', ({ payload }) => {
// Remove from array
const filename = payload
// set status to finished
const index = this.downloads.findIndex((download) => download.path === filename)
this.downloads[index].status = 'finished'
// Call onFinish callback
if (this.downloads[index]?.onFinish) {
this.downloads[index]?.onFinish()
}
})
listen('download_error', ({ payload }) => {
// @ts-expect-error shut up typescript
const errorData: {
path: string
error: string
} = payload
// Set download to error
const index = this.downloads.findIndex((download) => download.path === errorData.path)
this.downloads[index].status = 'error'
this.downloads[index].error = errorData.error
// Remove GIMI from list as fallback will replace it
if (errorData.path.includes('GIMI.zip')) {
this.downloads.splice(index, 1)
}
})
// Extraction events
listen('extract_start', ({ payload }) => {
// Find the download that is extracting and set it's status as such
const index = this.downloads.findIndex((download) => download.path === payload)
this.downloads[index].status = 'extracting'
})
listen('extract_end', ({ payload }) => {
// @ts-expect-error shut up typescript
const obj: {
file: string
new_folder: string
} = payload
// Find the download that is not extracting and set it's status as such
const index = this.downloads.findIndex((download) => download.path === obj.file)
this.downloads[index].status = 'finished'
// Remove completed extraction from list
this.downloads.splice(index, 1)
})
}
getDownloads() {
return this.downloads
}
downloadingJar() {
// Kinda hacky but it works
return this.downloads.some(
(d) => d.path.includes('grasscutter.zip') && !(d.status.includes('finished') || d.status.includes('error'))
)
}
downloadingFullBuild() {
// Kinda hacky but it works
return this.downloads.some(
(d) => d.path.includes('GrasscutterCulti') && !(d.status.includes('finished') || d.status.includes('error'))
)
}
downloadingResources() {
// Kinda hacky but it works
return this.downloads.some(
(d) => d.path.includes('resources') && !(d.status.includes('finished') || d.status.includes('error'))
)
}
downloadingRepo() {
return this.downloads.some(
(d) => d.path.includes('grasscutter_repo.zip') && !(d.status.includes('finished') || d.status.includes('error'))
)
}
downloadingMigoto() {
return this.downloads.some(
(d) => d.path.includes('3dmigoto') && !(d.status.includes('finished') || d.status.includes('error'))
)
}
addDownload(url: string, path: string, onFinish?: () => void) {
// Begin download from rust backend, don't add if the download addition fails
invoke('download_file', { url, path })
const obj = {
path,
progress: 0,
total: 0,
total_downloaded: 0,
status: 'downloading',
startTime: Date.now(),
onFinish,
}
this.downloads.push(obj)
}
stopDownload(path: string) {
// Stop download and remove from list.
invoke('stop_download', { path })
// Remove from list
const index = this.downloads.findIndex((download) => download.path === path)
this.downloads.splice(index, 1)
}
getDownloadProgress(path: string) {
const index = this.downloads.findIndex((download) => download.path === path)
return this.downloads[index] || null
}
getDownloadSize(path: string) {
const index = this.downloads.findIndex((download) => download.path === path)
return byteToString(this.downloads[index].total) || null
}
getTotalAverage() {
const files = this.downloads.filter((d) => d.status === 'downloading')
const total = files.reduce((acc, d) => acc + d.total, 0)
const progress = files.reduce((acc, d) => (d.progress !== 0 ? acc + d.progress : acc + d.total_downloaded), 0)
let speedStr = '0 B/s'
// Get download speed based on startTimes
if (files.length > 0) {
const now = Date.now()
const timeDiff = now - files[0].startTime
const speed = (progress / timeDiff) * 1000
speedStr = byteToString(speed) + '/s'
}
return {
average: (progress / total) * 100 || 0,
files: this.downloads.filter((d) => d.status === 'downloading').length,
extracting: this.downloads.filter((d) => d.status === 'extracting').length,
totalSize: total,
speed: speedStr,
}
}
}