mirror of
https://github.com/modrinth/code.git
synced 2026-08-25 00:55:25 +00:00
feat: add hide and show download items (#7106)
* feat: add hide and show download items * pnpm prepr
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
>
|
||||
<NotificationToast
|
||||
v-if="item.toast"
|
||||
class="min-w-full"
|
||||
:type="item.toast.type"
|
||||
:action-loading="toastActionLoading(item.id)"
|
||||
:actor-name="item.toast.actorName"
|
||||
@@ -37,9 +38,15 @@
|
||||
@open-actor="item.toast.onOpenActor?.()"
|
||||
@open-instance="handleToastAction(item, item.toast.onOpenInstance)"
|
||||
/>
|
||||
<div v-else-if="isDownloadNotification(item)" class="flex flex-col gap-4">
|
||||
<TransitionGroup
|
||||
v-else-if="isDownloadNotification(item)"
|
||||
name="popup-downloads"
|
||||
tag="div"
|
||||
class="flex flex-col gap-3"
|
||||
>
|
||||
<div v-for="progressItem in downloadToastItems(item)" :key="progressItem.id">
|
||||
<NotificationToast
|
||||
class="min-w-full"
|
||||
type="instance-download"
|
||||
:entity-name="progressItem.title || item.title"
|
||||
:entity-icon-url="progressItem.iconUrl ?? item.iconUrl ?? MinecraftServerIcon"
|
||||
@@ -54,10 +61,10 @@
|
||||
:actions="progressItem.buttons"
|
||||
:dismissible="progressItem.dismissible"
|
||||
@dismiss="handleProgressItemDismiss(item, progressItem)"
|
||||
@action="(index) => handleProgressItemAction(progressItem, index)"
|
||||
@action="(index) => handleProgressItemAction(item, progressItem, index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
<div
|
||||
v-else
|
||||
class="flex w-full flex-col gap-3 overflow-hidden rounded-2xl bg-bg-raised shadow-xl border-surface-5 border-solid border p-4"
|
||||
@@ -100,7 +107,7 @@
|
||||
type="quiet"
|
||||
label="Close"
|
||||
class="-m-1.5"
|
||||
@click="dismiss(item.id)"
|
||||
@click="handleNotificationDismiss(item)"
|
||||
>
|
||||
<XIcon />
|
||||
</IconButton>
|
||||
@@ -201,7 +208,7 @@ import NotificationToast from '../notifications/NotificationToast.vue'
|
||||
|
||||
const popupNotificationManager = injectPopupNotificationManager()
|
||||
const notifications = computed<PopupNotification[]>(() =>
|
||||
popupNotificationManager.getNotifications(),
|
||||
popupNotificationManager.getVisibleNotifications(),
|
||||
)
|
||||
const { stackCount } = useModalStack()
|
||||
const hasModalActive = computed(() => stackCount.value > 0)
|
||||
@@ -213,7 +220,6 @@ const activeToastActions = ref<Record<string, 'accept'>>({})
|
||||
const stopTimer = (n: PopupNotification) => popupNotificationManager.stopNotificationTimer(n)
|
||||
const setNotificationTimer = (n: PopupNotification) =>
|
||||
popupNotificationManager.setNotificationTimer(n)
|
||||
const dismiss = (id: string | number) => popupNotificationManager.removeNotification(id)
|
||||
const toastActionLoading = (id: string | number) => activeToastActions.value[String(id)] ?? null
|
||||
|
||||
function isDownloadNotification(item: PopupNotification) {
|
||||
@@ -225,7 +231,7 @@ function isDownloadNotification(item: PopupNotification) {
|
||||
|
||||
function downloadToastItems(item: PopupNotification): PopupNotificationProgressItem[] {
|
||||
if (item.progressItems?.length) {
|
||||
return item.progressItems
|
||||
return popupNotificationManager.getVisibleDownloadProgressItems(item)
|
||||
}
|
||||
|
||||
return [
|
||||
@@ -242,35 +248,32 @@ function downloadToastItems(item: PopupNotification): PopupNotificationProgressI
|
||||
]
|
||||
}
|
||||
|
||||
async function handleProgressItemDismiss(
|
||||
function handleProgressItemDismiss(
|
||||
item: PopupNotification,
|
||||
progressItem: PopupNotificationProgressItem,
|
||||
) {
|
||||
if (progressItem.onDismiss) {
|
||||
await progressItem.onDismiss()
|
||||
return
|
||||
}
|
||||
|
||||
dismiss(item.id)
|
||||
popupNotificationManager.hideDownloadItem(item.id, progressItem.id)
|
||||
}
|
||||
|
||||
async function handleProgressItemAction(
|
||||
item: PopupNotification,
|
||||
progressItem: PopupNotificationProgressItem,
|
||||
index: number,
|
||||
) {
|
||||
const button = progressItem.buttons?.[index]
|
||||
if (button) {
|
||||
await handleProgressItemButtonClick(progressItem, button)
|
||||
await handleProgressItemButtonClick(item, progressItem, button)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleProgressItemButtonClick(
|
||||
item: PopupNotification,
|
||||
progressItem: PopupNotificationProgressItem,
|
||||
btn: PopupNotificationButton,
|
||||
) {
|
||||
await btn.action()
|
||||
if (!btn.keepOpen) {
|
||||
await progressItem.onDismiss?.()
|
||||
popupNotificationManager.hideDownloadItem(item.id, progressItem.id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,6 +284,11 @@ async function handleButtonClick(id: string | number, btn: PopupNotificationButt
|
||||
}
|
||||
}
|
||||
|
||||
async function handleNotificationDismiss(item: PopupNotification) {
|
||||
await item.onDismiss?.()
|
||||
popupNotificationManager.removeNotification(item.id)
|
||||
}
|
||||
|
||||
async function handleToastAction(item: PopupNotification, action?: () => void | Promise<void>) {
|
||||
popupNotificationManager.removeNotification(item.id)
|
||||
await action?.()
|
||||
@@ -381,6 +389,21 @@ withDefaults(
|
||||
|
||||
.popup-notifs-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(100%) scale(0.8);
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.popup-downloads-move {
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.popup-downloads-leave-active {
|
||||
transition:
|
||||
opacity 0.3s ease-in-out,
|
||||
transform 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.popup-downloads-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Component } from 'vue'
|
||||
import { type Component, type Ref, ref } from 'vue'
|
||||
|
||||
import { createContext } from '.'
|
||||
|
||||
@@ -25,7 +25,6 @@ export interface PopupNotificationProgressItem {
|
||||
progressCurrent?: number
|
||||
progressTotal?: number
|
||||
dismissible?: boolean
|
||||
onDismiss?: () => void | Promise<void>
|
||||
buttons?: PopupNotificationButton[]
|
||||
}
|
||||
|
||||
@@ -73,15 +72,51 @@ export interface PopupNotification {
|
||||
buttons?: PopupNotificationButton[]
|
||||
toast?: PopupNotificationToast
|
||||
dismissible?: boolean
|
||||
onDismiss?: () => void | Promise<void>
|
||||
autoCloseMs?: number | null
|
||||
timer?: NodeJS.Timeout
|
||||
}
|
||||
|
||||
export interface PopupNotificationDownloadState {
|
||||
total: number
|
||||
hidden: number
|
||||
}
|
||||
|
||||
export abstract class AbstractPopupNotificationManager {
|
||||
protected readonly DEFAULT_AUTO_CLOSE_MS = 30 * 1000
|
||||
private readonly hiddenDownloadItemKeys: Ref<Set<string>> = ref(new Set())
|
||||
|
||||
abstract getNotifications(): PopupNotification[]
|
||||
|
||||
getDownloadState = (): PopupNotificationDownloadState => {
|
||||
const itemKeys = this.getDownloadNotifications().flatMap((notification) =>
|
||||
this.getDownloadItemKeys(notification),
|
||||
)
|
||||
return {
|
||||
total: itemKeys.length,
|
||||
hidden: itemKeys.filter((key) => this.hiddenDownloadItemKeys.value.has(key)).length,
|
||||
}
|
||||
}
|
||||
|
||||
getVisibleNotifications = (): PopupNotification[] =>
|
||||
this.getNotifications().filter(
|
||||
(notification) =>
|
||||
!this.isDownloadNotification(notification) ||
|
||||
this.getDownloadItemKeys(notification).some(
|
||||
(key) => !this.hiddenDownloadItemKeys.value.has(key),
|
||||
),
|
||||
)
|
||||
|
||||
getVisibleDownloadProgressItems = (
|
||||
notification: PopupNotification,
|
||||
): PopupNotificationProgressItem[] =>
|
||||
(notification.progressItems ?? []).filter(
|
||||
(progressItem) =>
|
||||
!this.hiddenDownloadItemKeys.value.has(
|
||||
this.getDownloadItemKey(notification.id, progressItem.id),
|
||||
),
|
||||
)
|
||||
|
||||
protected abstract addNotificationToStorage(notification: PopupNotification): void
|
||||
protected abstract removeNotificationFromStorage(id: string | number): void
|
||||
protected abstract clearAllNotificationsFromStorage(): void
|
||||
@@ -103,6 +138,9 @@ export abstract class AbstractPopupNotificationManager {
|
||||
const notification = notifications.find((n) => n.id === id)
|
||||
if (notification) {
|
||||
this.clearNotificationTimer(notification)
|
||||
this.getDownloadItemKeys(notification).forEach((key) =>
|
||||
this.hiddenDownloadItemKeys.value.delete(key),
|
||||
)
|
||||
this.removeNotificationFromStorage(id)
|
||||
}
|
||||
}
|
||||
@@ -110,6 +148,45 @@ export abstract class AbstractPopupNotificationManager {
|
||||
clearAllNotifications = (): void => {
|
||||
this.getNotifications().forEach((n) => this.clearNotificationTimer(n))
|
||||
this.clearAllNotificationsFromStorage()
|
||||
this.hiddenDownloadItemKeys.value.clear()
|
||||
}
|
||||
|
||||
hideDownloadItem = (notificationId: string | number, progressItemId: string): void => {
|
||||
const notification = this.getDownloadNotifications().find(
|
||||
(candidate) => candidate.id === notificationId,
|
||||
)
|
||||
if (!notification?.progressItems?.some((item) => item.id === progressItemId)) return
|
||||
|
||||
this.hiddenDownloadItemKeys.value.add(this.getDownloadItemKey(notificationId, progressItemId))
|
||||
if (
|
||||
this.getDownloadItemKeys(notification).every((key) =>
|
||||
this.hiddenDownloadItemKeys.value.has(key),
|
||||
)
|
||||
) {
|
||||
this.clearNotificationTimer(notification)
|
||||
}
|
||||
}
|
||||
|
||||
toggleDownloadNotifications = (): void => {
|
||||
const downloadNotifications = this.getDownloadNotifications()
|
||||
const hasHiddenDownloads = downloadNotifications.some((notification) =>
|
||||
this.getDownloadItemKeys(notification).some((key) =>
|
||||
this.hiddenDownloadItemKeys.value.has(key),
|
||||
),
|
||||
)
|
||||
|
||||
if (hasHiddenDownloads) {
|
||||
this.hiddenDownloadItemKeys.value.clear()
|
||||
downloadNotifications.forEach((notification) => this.setNotificationTimer(notification))
|
||||
return
|
||||
}
|
||||
|
||||
downloadNotifications.forEach((notification) => {
|
||||
this.getDownloadItemKeys(notification).forEach((key) =>
|
||||
this.hiddenDownloadItemKeys.value.add(key),
|
||||
)
|
||||
this.clearNotificationTimer(notification)
|
||||
})
|
||||
}
|
||||
|
||||
setNotificationTimer = (notification: PopupNotification): void => {
|
||||
@@ -134,6 +211,30 @@ export abstract class AbstractPopupNotificationManager {
|
||||
notification.timer = undefined
|
||||
}
|
||||
}
|
||||
|
||||
private isDownloadNotification(notification: PopupNotification): boolean {
|
||||
return notification.type === 'download' || notification.toast?.type === 'instance-download'
|
||||
}
|
||||
|
||||
private getDownloadNotifications(): PopupNotification[] {
|
||||
return this.getNotifications().filter((notification) =>
|
||||
this.isDownloadNotification(notification),
|
||||
)
|
||||
}
|
||||
|
||||
private getDownloadItemKeys(notification: PopupNotification): string[] {
|
||||
if (!this.isDownloadNotification(notification)) return []
|
||||
if (notification.progressItems?.length) {
|
||||
return notification.progressItems.map((progressItem) =>
|
||||
this.getDownloadItemKey(notification.id, progressItem.id),
|
||||
)
|
||||
}
|
||||
return [this.getDownloadItemKey(notification.id)]
|
||||
}
|
||||
|
||||
private getDownloadItemKey(notificationId: string | number, progressItemId?: string): string {
|
||||
return JSON.stringify([typeof notificationId, notificationId, progressItemId ?? null])
|
||||
}
|
||||
}
|
||||
|
||||
export const [injectPopupNotificationManager, providePopupNotificationManager] =
|
||||
|
||||
Reference in New Issue
Block a user