mirror of
https://github.com/modrinth/code.git
synced 2026-08-26 17:44:50 +00:00
feat: clean up browse shared layout logic + introduce queuing (#6030)
* feat: clean up edge case behaviour and add queued to install logic * fix: remove version choice modal * feat: queued flow * feat: standardize headers in app on proj pages * fix: clear btn * feat: installing floating popup * fix: lint * fix: onboarding/reset logic change for modpacks * qa: big ol qa * fix: lint * fix: lint --------- Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
import type {
|
||||
ContentCardProject,
|
||||
ContentCardVersion,
|
||||
ContentOwner,
|
||||
} from '../layouts/shared/content-tab/types'
|
||||
|
||||
export type PendingServerContentInstallType = 'mod' | 'plugin' | 'datapack'
|
||||
type PendingServerContentOwner = Omit<ContentOwner, 'link'> & { link?: string }
|
||||
|
||||
export interface PendingServerContentInstall {
|
||||
projectId: string
|
||||
versionId: string
|
||||
contentType: PendingServerContentInstallType
|
||||
title: ContentCardProject['title']
|
||||
versionName?: ContentCardVersion['version_number'] | null
|
||||
versionNumber?: ContentCardVersion['version_number'] | null
|
||||
fileName?: ContentCardVersion['file_name'] | null
|
||||
owner?: PendingServerContentOwner | null
|
||||
slug?: ContentCardProject['slug'] | null
|
||||
iconUrl?: ContentCardProject['icon_url'] | null
|
||||
createdAt: number
|
||||
}
|
||||
|
||||
interface PendingServerContentInstallBaseline {
|
||||
contentKeys: string[]
|
||||
projectIds?: string[]
|
||||
createdAt: number
|
||||
}
|
||||
|
||||
export const pendingServerContentInstallsEvent = 'modrinth:pending-server-content-installs'
|
||||
|
||||
const stalePendingInstallAge = 30 * 60 * 1000
|
||||
|
||||
function getPendingServerContentInstallsKey(serverId: string | null, worldId: string | null) {
|
||||
if (!serverId || !worldId) return null
|
||||
return `server-content-installing:${serverId}:${worldId}`
|
||||
}
|
||||
|
||||
function getPendingServerContentInstallBaselineKey(
|
||||
serverId: string | null,
|
||||
worldId: string | null,
|
||||
) {
|
||||
if (!serverId || !worldId) return null
|
||||
return `server-content-installing-baseline:${serverId}:${worldId}`
|
||||
}
|
||||
|
||||
function isPendingServerContentInstall(value: unknown): value is PendingServerContentInstall {
|
||||
if (!value || typeof value !== 'object') return false
|
||||
const record = value as Record<string, unknown>
|
||||
return (
|
||||
typeof record.projectId === 'string' &&
|
||||
typeof record.versionId === 'string' &&
|
||||
(record.contentType === 'mod' ||
|
||||
record.contentType === 'plugin' ||
|
||||
record.contentType === 'datapack') &&
|
||||
typeof record.title === 'string' &&
|
||||
typeof record.createdAt === 'number'
|
||||
)
|
||||
}
|
||||
|
||||
function isPendingServerContentInstallBaseline(
|
||||
value: unknown,
|
||||
): value is PendingServerContentInstallBaseline {
|
||||
if (!value || typeof value !== 'object') return false
|
||||
const record = value as Record<string, unknown>
|
||||
const contentKeys = record.contentKeys ?? record.projectIds
|
||||
return (
|
||||
Array.isArray(contentKeys) &&
|
||||
contentKeys.every((contentKey) => typeof contentKey === 'string') &&
|
||||
typeof record.createdAt === 'number'
|
||||
)
|
||||
}
|
||||
|
||||
function filterFreshPendingServerContentInstalls(items: PendingServerContentInstall[]) {
|
||||
const cutoff = Date.now() - stalePendingInstallAge
|
||||
return items.filter((item) => item.createdAt >= cutoff)
|
||||
}
|
||||
|
||||
function isFreshPendingServerContentInstallBaseline(item: PendingServerContentInstallBaseline) {
|
||||
return item.createdAt >= Date.now() - stalePendingInstallAge
|
||||
}
|
||||
|
||||
function emitPendingServerContentInstallsChanged(serverId: string | null, worldId: string | null) {
|
||||
if (typeof window === 'undefined') return
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(pendingServerContentInstallsEvent, {
|
||||
detail: { serverId, worldId },
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export function readPendingServerContentInstalls(serverId: string | null, worldId: string | null) {
|
||||
const key = getPendingServerContentInstallsKey(serverId, worldId)
|
||||
if (!key || typeof localStorage === 'undefined') return []
|
||||
|
||||
try {
|
||||
const raw = localStorage.getItem(key)
|
||||
if (!raw) return []
|
||||
const parsed = JSON.parse(raw)
|
||||
if (!Array.isArray(parsed)) return []
|
||||
const freshItems = filterFreshPendingServerContentInstalls(
|
||||
parsed.filter(isPendingServerContentInstall),
|
||||
)
|
||||
if (freshItems.length !== parsed.length) {
|
||||
writePendingServerContentInstalls(serverId, worldId, freshItems)
|
||||
}
|
||||
return freshItems
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export function writePendingServerContentInstalls(
|
||||
serverId: string | null,
|
||||
worldId: string | null,
|
||||
items: PendingServerContentInstall[],
|
||||
) {
|
||||
const key = getPendingServerContentInstallsKey(serverId, worldId)
|
||||
if (!key || typeof localStorage === 'undefined') return
|
||||
|
||||
const freshItems = filterFreshPendingServerContentInstalls(items)
|
||||
if (freshItems.length === 0) {
|
||||
localStorage.removeItem(key)
|
||||
const baselineKey = getPendingServerContentInstallBaselineKey(serverId, worldId)
|
||||
if (baselineKey) {
|
||||
localStorage.removeItem(baselineKey)
|
||||
}
|
||||
} else {
|
||||
localStorage.setItem(key, JSON.stringify(freshItems))
|
||||
}
|
||||
emitPendingServerContentInstallsChanged(serverId, worldId)
|
||||
}
|
||||
|
||||
export function readPendingServerContentInstallBaseline(
|
||||
serverId: string | null,
|
||||
worldId: string | null,
|
||||
) {
|
||||
const key = getPendingServerContentInstallBaselineKey(serverId, worldId)
|
||||
if (!key || typeof localStorage === 'undefined') return null
|
||||
|
||||
try {
|
||||
const raw = localStorage.getItem(key)
|
||||
if (!raw) return null
|
||||
const parsed = JSON.parse(raw)
|
||||
if (!isPendingServerContentInstallBaseline(parsed)) return null
|
||||
if (!isFreshPendingServerContentInstallBaseline(parsed)) {
|
||||
localStorage.removeItem(key)
|
||||
return null
|
||||
}
|
||||
return new Set(parsed.contentKeys ?? parsed.projectIds)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function writePendingServerContentInstallBaseline(
|
||||
serverId: string | null,
|
||||
worldId: string | null,
|
||||
contentKeys: Iterable<string>,
|
||||
) {
|
||||
const key = getPendingServerContentInstallBaselineKey(serverId, worldId)
|
||||
if (!key || typeof localStorage === 'undefined') return
|
||||
|
||||
localStorage.setItem(
|
||||
key,
|
||||
JSON.stringify({
|
||||
contentKeys: Array.from(new Set(contentKeys)),
|
||||
createdAt: Date.now(),
|
||||
} satisfies PendingServerContentInstallBaseline),
|
||||
)
|
||||
emitPendingServerContentInstallsChanged(serverId, worldId)
|
||||
}
|
||||
|
||||
export function addPendingServerContentInstalls(
|
||||
serverId: string | null,
|
||||
worldId: string | null,
|
||||
items: Omit<PendingServerContentInstall, 'createdAt'>[],
|
||||
) {
|
||||
if (items.length === 0) return
|
||||
|
||||
const now = Date.now()
|
||||
const next = new Map(
|
||||
readPendingServerContentInstalls(serverId, worldId).map((item) => [item.projectId, item]),
|
||||
)
|
||||
for (const item of items) {
|
||||
next.set(item.projectId, { ...item, createdAt: now })
|
||||
}
|
||||
writePendingServerContentInstalls(serverId, worldId, Array.from(next.values()))
|
||||
}
|
||||
|
||||
export function removePendingServerContentInstall(
|
||||
serverId: string | null,
|
||||
worldId: string | null,
|
||||
projectId: string,
|
||||
) {
|
||||
writePendingServerContentInstalls(
|
||||
serverId,
|
||||
worldId,
|
||||
readPendingServerContentInstalls(serverId, worldId).filter(
|
||||
(item) => item.projectId !== projectId,
|
||||
),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user