Skip to content
Closed
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
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ anyhow = "1.0.100"
arc-swap = "1.7.1"
argon2 = { version = "0.5.3", features = ["std"] }
ariadne = { path = "packages/ariadne" }
async-compression = "0.4.32"
async-minecraft-ping = { path = "packages/async-minecraft-ping" }
async-recursion = "1.1.1"
async-stripe = { version = "0.41.0", default-features = false, features = [
Expand Down Expand Up @@ -113,6 +114,7 @@ iana-time-zone = "0.1.64"
image = { version = "0.25.8", default-features = false, features = ["rayon"] }
indexmap = "2.11.4"
indicatif = "0.18.0"
infer = "0.22.0"
itertools = "0.14.0"
jemalloc_pprof = "0.8.1"
json-patch = { version = "4.1.0", default-features = false }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
PlayIcon,
TrashIcon,
UpdatedIcon,
UploadIcon,
XIcon,
} from '@modrinth/assets'
import {
Expand Down Expand Up @@ -106,7 +107,11 @@ const instanceLink = computed(() =>
>
<component
:is="
job.kind === 'external-file' || job.kind === 'debug-export' ? DownloadIcon : BoxIcon
job.kind === 'shared-instance-publish'
? UploadIcon
: job.kind === 'external-file' || job.kind === 'debug-export'
? DownloadIcon
: BoxIcon
"
class="size-6 text-primary"
aria-hidden="true"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { reactive, ref } from 'vue'

const RATE_WINDOW_MS = 5000

export interface SharedInstancePublishTask {
id: string
instanceId: string
instanceName: string
iconPath: string | null
status: 'running' | 'succeeded' | 'failed'
uploadedBytes: number
totalBytes: number
rate: number
createdAt: string
finishedAt?: string
error?: string
details?: string
}

export const sharedInstancePublishes = ref(new Map<string, SharedInstancePublishTask>())

export function findRunningSharedInstancePublish(instanceId: string) {
for (const task of sharedInstancePublishes.value.values()) {
if (task.instanceId === instanceId && task.status === 'running') return task
}
return null
}

export function trackSharedInstancePublish(instance: {
id: string
name: string
icon_path?: string | null
}) {
const task = reactive<SharedInstancePublishTask>({
id: `shared-instance-publish-${crypto.randomUUID()}`,
instanceId: instance.id,
instanceName: instance.name,
iconPath: instance.icon_path ?? null,
status: 'running',
uploadedBytes: 0,
totalBytes: 0,
rate: 0,
createdAt: new Date().toISOString(),
})
sharedInstancePublishes.value.set(task.id, task)
const samples: { time: number; bytes: number }[] = []

function finish(status: SharedInstancePublishTask['status']) {
task.status = status
task.finishedAt = new Date().toISOString()
task.rate = 0
}

return {
update(uploadedBytes: number, totalBytes: number) {
if (task.status !== 'running') return
const now = performance.now()
samples.push({ time: now, bytes: uploadedBytes })
while (samples.length > 2 && now - samples[1].time >= RATE_WINDOW_MS) samples.shift()
const oldest = samples[0]
const elapsed = now - oldest.time
task.rate = elapsed >= 1000 ? ((uploadedBytes - oldest.bytes) * 1000) / elapsed : 0
task.uploadedBytes = uploadedBytes
task.totalBytes = totalBytes
},
succeed() {
finish('succeeded')
},
fail(error: string) {
task.error = error
finish('failed')
},
setDetails(details: string) {
task.details = details
},
}
}

export function getSharedInstancePublishRate(task: SharedInstancePublishTask) {
return task.status === 'running' ? task.rate : 0
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,17 @@ import {
getExternalFileDownloadRate,
} from './external-file-downloads'
import { createInstallJobProgressTracker } from './install-job-progress'
import {
getSharedInstancePublishRate,
sharedInstancePublishes,
type SharedInstancePublishTask,
} from './shared-instance-publishes'
import { storeVerificationTask } from './store-verification'
import { useInstallJobDisplay } from './use-install-job-display'

export interface DownloadManagerJob {
id: string
kind?: 'external-file' | 'debug-export'
kind?: 'external-file' | 'debug-export' | 'shared-instance-publish'
serverId?: string
createdAt?: string
instanceId: string | null
Expand Down Expand Up @@ -94,6 +99,21 @@ const debugExportMessages = defineMessages({
canceling: { id: 'app.debug-export.canceling', defaultMessage: 'Canceling export…' },
})

const publishMessages = defineMessages({
preparing: {
id: 'app.shared-instance-publish.preparing',
defaultMessage: 'Preparing update…',
},
uploading: {
id: 'app.shared-instance-publish.uploading',
defaultMessage: 'Pushing update…',
},
pushed: {
id: 'app.shared-instance-publish.pushed',
defaultMessage: 'Update pushed',
},
})

function getIconUrl(icon: string | null | undefined): string | null {
if (!icon) return null
return /^(https?:|data:|blob:|asset:|tauri:)/.test(icon) ? icon : convertFileSrc(icon)
Expand Down Expand Up @@ -184,11 +204,15 @@ export function useDownloadManager() {
const debugExportRows = computed(() =>
[...debugInfoExports.value.values()].map(buildDebugExportRow),
)
const publishRows = computed(() =>
[...sharedInstancePublishes.value.values()].map(buildPublishRow),
)
const allRows = computed(() => [
...rows.value,
...verificationRows.value,
...fileRows.value,
...debugExportRows.value,
...publishRows.value,
])

const activeJobs = computed(() =>
Expand All @@ -214,7 +238,12 @@ export function useDownloadManager() {
display.formatRate(
activeJobs.value.reduce((total, job) => {
const file = externalFileDownloads.value.get(job.id)
const fileRate = file ? getExternalFileDownloadRate(file, now.value) : null
const publish = sharedInstancePublishes.value.get(job.id)
const fileRate = file
? getExternalFileDownloadRate(file, now.value)
: publish
? getSharedInstancePublishRate(publish)
: null
return total + (fileRate ?? transfer.get(job.id, now.value).rate ?? 0)
}, 0),
),
Expand Down Expand Up @@ -262,6 +291,53 @@ export function useDownloadManager() {
}
}

function buildPublishRow(task: SharedInstancePublishTask): DownloadManagerJob {
const rate = getSharedInstancePublishRate(task)
const uploading = task.totalBytes > 0
const progress = uploading ? Math.min(0.99, task.uploadedBytes / task.totalBytes) : 0
return {
id: task.id,
kind: 'shared-instance-publish',
createdAt: task.createdAt,
instanceId: task.instanceId,
status: task.status,
paused: false,
canceling: false,
canPause: false,
canCancel: false,
title: task.instanceName,
iconUrl: getIconUrl(task.iconPath),
text:
task.error ??
formatMessage(
task.status === 'succeeded'
? publishMessages.pushed
: uploading
? publishMessages.uploading
: publishMessages.preparing,
),
finishedAt: task.finishedAt,
progress,
overallProgress: task.status === 'succeeded' ? 1 : progress,
progressLabel:
task.status === 'running' && uploading
? [
`${formatBytes(task.uploadedBytes)} / ${formatBytes(task.totalBytes)}`,
display.formatRate(rate),
]
.filter(Boolean)
.join(' · ')
: '',
waiting: !uploading,
eta:
rate && uploading ? display.formatEta((task.totalBytes - task.uploadedBytes) / rate) : '',
canRetry: false,
canCopyDetails: !!task.error || (appSettings.alwaysShowCopyDetails && !!task.details),
copied: copiedJobs.value.has(task.id),
busy: busyJobs.value.has(task.id),
}
}

function buildFileRow(task: ExternalFileDownloadTask): DownloadManagerJob {
const rate = getExternalFileDownloadRate(task, now.value)
const progress = task.totalBytes ? Math.min(0.99, task.downloadedBytes / task.totalBytes) : 0
Expand Down Expand Up @@ -491,6 +567,11 @@ export function useDownloadManager() {
if (file.status !== 'running') externalFileDownloads.value.delete(id)
return
}
const publish = sharedInstancePublishes.value.get(id)
if (publish) {
if (publish.status !== 'running') sharedInstancePublishes.value.delete(id)
return
}
if (storeVerificationTask.value?.id === id) {
if (storeVerificationTask.value.status !== 'running') storeVerificationTask.value = null
return
Expand All @@ -512,8 +593,13 @@ export function useDownloadManager() {

async function copyDetails(id: string) {
await runAction(id, async () => {
const publish = sharedInstancePublishes.value.get(id)
const file = externalFileDownloads.value.get(id) ?? debugInfoExports.value.get(id)
const details = file ? (file.error ?? '') : await install_job_support_details(id)
const details = publish
? (publish.details ?? publish.error ?? '')
: file
? (file.error ?? '')
: await install_job_support_details(id)
if (disposed) return
await navigator.clipboard.writeText(details)
if (disposed) return
Expand Down
Loading
Loading