feat: add hide and show download items (#7106)

* feat: add hide and show download items

* pnpm prepr
This commit is contained in:
Truman Gao
2026-08-12 16:18:02 +00:00
committed by GitHub
parent 6ec979a076
commit 4758cabbf9
5 changed files with 252 additions and 46 deletions
@@ -1,15 +1,16 @@
<template>
<div class="flex gap-2 items-center">
<IconButton
v-if="hasActiveLoadingBars && !hasVisibleActiveDownloadToasts"
v-tooltip="formatMessage(messages.viewActiveDownloads)"
type="quiet"
color="brand"
:label="formatMessage(messages.viewActiveDownloads)"
@click="openDownloadToast()"
>
<DownloadIcon />
</IconButton>
<div v-if="downloadState.total > 0 || hasActiveLoadingBars" class="relative">
<IconButton
v-tooltip="downloadToggleLabel"
:color="downloadState.hidden > 0 ? 'brand' : undefined"
type="quiet"
:label="downloadToggleLabel"
@click="toggleDownloadNotifications"
>
<DownloadIcon />
</IconButton>
</div>
<div v-if="offline" class="flex items-center gap-1">
<UnplugIcon class="text-secondary" />
<span class="text-sm text-contrast"> {{ formatMessage(messages.offline) }} </span>
@@ -218,8 +219,35 @@ const messages = defineMessages({
id: 'app.action-bar.view-active-downloads',
defaultMessage: 'View active downloads',
},
hideDownloads: {
id: 'app.action-bar.hide-downloads',
defaultMessage: 'Hide active downloads',
},
showDownloads: {
id: 'app.action-bar.show-downloads',
defaultMessage: 'Show active downloads',
},
})
const downloadState = computed(() => popupNotificationManager.getDownloadState())
const downloadToggleLabel = computed(() =>
formatMessage(
downloadState.value.hidden > 0
? messages.showDownloads
: downloadState.value.total > 0
? messages.hideDownloads
: messages.viewActiveDownloads,
),
)
function toggleDownloadNotifications(): void {
if (downloadState.value.total > 0) {
popupNotificationManager.toggleDownloadNotifications()
} else if (hasActiveLoadingBars.value) {
openDownloadToast()
}
}
const currentProcesses = ref<RunningProcess[]>([])
const selectedProcess = ref<RunningProcess | undefined>()
@@ -296,6 +324,7 @@ function goToTerminal(instanceId?: string) {
const currentLoadingBars = ref<LoadingBar[]>([])
const currentLoadingBarIconUrls = ref<Record<string, string | null>>({})
const notificationId = ref<string | number | null>(null)
const terminalNotificationIds = new Map<string, string | number>()
const dismissed = ref(false)
function getLoadingBarKey(loadingBar: LoadingBar): string {
@@ -341,6 +370,44 @@ function removeNotification(): void {
notificationId.value = null
}
function syncTerminalNotifications(): void {
const terminalNotifications = installJobNotifications.terminalNotifications.value
const currentJobIds = new Set(terminalNotifications.map((notification) => notification.id))
for (const terminal of terminalNotifications) {
const popupId = terminalNotificationIds.get(terminal.id)
let notification = popupId
? popupNotificationManager.getNotifications().find((candidate) => candidate.id === popupId)
: undefined
if (!notification) {
notification = popupNotificationManager.addPopupNotification({
title: terminal.title,
text: terminal.text,
type: terminal.type,
buttons: terminal.buttons,
onDismiss: terminal.onDismiss,
autoCloseMs: null,
})
terminalNotificationIds.set(terminal.id, notification.id)
continue
}
notification.title = terminal.title
notification.text = terminal.text
notification.type = terminal.type
notification.buttons = terminal.buttons
notification.onDismiss = terminal.onDismiss
}
for (const [jobId, popupId] of terminalNotificationIds) {
if (!currentJobIds.has(jobId)) {
popupNotificationManager.removeNotification(popupId)
terminalNotificationIds.delete(jobId)
}
}
}
function buildDownloadItems(): PopupNotificationProgressItem[] {
return [
...installJobNotifications.progressItems.value,
@@ -358,12 +425,13 @@ function buildDownloadItems(): PopupNotificationProgressItem[] {
]
}
const hasVisibleActiveDownloadToasts = computed(() => !!getNotification())
const hasActiveLoadingBars = computed(
() => currentLoadingBars.value.length > 0 || installJobNotifications.active.value,
)
function updateNotification(resummon = false): void {
syncTerminalNotifications()
if (resummon) {
dismissed.value = false
}
@@ -392,7 +460,6 @@ function updateNotification(resummon = false): void {
: formatMessage(messages.downloads)
notif.text = undefined
notif.progressItems = progressItems
notif.buttons = installJobNotifications.buttons.value
notif.progress = undefined
notif.waiting = undefined
} else {
@@ -403,7 +470,6 @@ function updateNotification(resummon = false): void {
type: 'download',
autoCloseMs: null,
progressItems,
buttons: installJobNotifications.buttons.value,
})
notificationId.value = notif.id
}
@@ -502,6 +568,8 @@ function selectProcess(process: RunningProcess) {
onBeforeUnmount(() => {
removeNotification()
terminalNotificationIds.forEach((id) => popupNotificationManager.removeNotification(id))
terminalNotificationIds.clear()
dismissed.value = false
window.removeEventListener('offline', handleOffline)
window.removeEventListener('online', handleOnline)
@@ -404,8 +404,6 @@ export async function useInstallJobNotifications(opts: {
}
function getProgress(job: InstallJobSnapshot): number {
if (job.status === 'succeeded') return 1
if (job.status === 'failed' || job.status === 'interrupted') return 0
const progress = getEffectiveProgress(job)
if (!progress || progress.total <= 0) return 0
return Math.max(0, Math.min(1, progress.current / progress.total))
@@ -526,8 +524,12 @@ export async function useInstallJobNotifications(opts: {
)
}
const activeJobs = computed(() =>
jobs.value.filter((job) => job.status === 'queued' || job.status === 'running'),
)
const progressItems = computed<PopupNotificationProgressItem[]>(() =>
jobs.value.map((job) => {
activeJobs.value.map((job) => {
const progress = getEffectiveProgress(job)
return {
@@ -536,20 +538,26 @@ export async function useInstallJobNotifications(opts: {
text: getText(job),
iconUrl: iconUrls.value[job.job_id] ?? null,
progress: getProgress(job),
waiting: !job.progress && ['queued', 'running'].includes(job.status),
showProgress: !isTerminalJob(job),
wrapText: isTerminalJob(job),
progressType: isTerminalJob(job) ? undefined : getProgressType(job),
progressCurrent: isTerminalJob(job) ? undefined : progress?.current,
progressTotal: isTerminalJob(job) ? undefined : progress?.total,
waiting: !job.progress && job.status === 'running',
showProgress: job.status === 'running',
progressType: getProgressType(job),
progressCurrent: progress?.current,
progressTotal: progress?.total,
buttons: getButtons(job),
dismissible: isTerminalJob(job),
onDismiss: getDismissHandler(job),
}
}),
)
const buttons = computed<PopupNotificationButton[] | undefined>(() => undefined)
const terminalNotifications = computed(() =>
jobs.value.filter(isTerminalJob).map((job) => ({
id: job.job_id,
title: getTitle(job),
text: getText(job),
type: job.status === 'failed' ? ('error' as const) : ('warning' as const),
buttons: getButtons(job),
onDismiss: getDismissHandler(job),
})),
)
async function refreshMetadata(notify = true) {
const request = ++metadataRequest
@@ -631,10 +639,10 @@ export async function useInstallJobNotifications(opts: {
await refresh(false)
return {
active: computed(() => jobs.value.length > 0),
active: computed(() => activeJobs.value.length > 0),
title: computed(() => formatMessage(messages.installs)),
progressItems,
buttons,
terminalNotifications,
refresh,
dispose: () => {
for (const timeout of copiedResetTimeouts.values()) {
@@ -8,6 +8,9 @@
"app.action-bar.downloads": {
"message": "Downloads"
},
"app.action-bar.hide-downloads": {
"message": "Hide active downloads"
},
"app.action-bar.hide-more-running-instances": {
"message": "Hide more running instances"
},
@@ -110,6 +113,9 @@
"app.action-bar.reload-to-update": {
"message": "Reload to update"
},
"app.action-bar.show-downloads": {
"message": "Show active downloads"
},
"app.action-bar.show-more-running-instances": {
"message": "Show more running instances"
},