Files
2026-04-18 17:02:56 +02:00

131 lines
5.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Shared utilities for the upload system.
* These are pure functions no React, no side effects.
*/
// ─── Constants ────────────────────────────────────────────────────────────────
export const IMAGE_EXTENSIONS = new Set(['jpg', 'jpeg', 'png', 'webp'])
export const IMAGE_MIME = new Set(['image/jpeg', 'image/png', 'image/webp'])
export const ARCHIVE_EXTENSIONS = new Set(['zip', 'rar', '7z', 'tar', 'gz'])
export const ARCHIVE_MIME = new Set([
'application/zip',
'application/x-zip-compressed',
'application/x-rar-compressed',
'application/vnd.rar',
'application/x-7z-compressed',
'application/x-tar',
'application/gzip',
'application/x-gzip',
'application/octet-stream',
])
export const PRIMARY_IMAGE_MAX_BYTES = 50 * 1024 * 1024 // 50 MB
export const PRIMARY_ARCHIVE_MAX_BYTES = 200 * 1024 * 1024 // 200 MB
export const SCREENSHOT_MAX_BYTES = 10 * 1024 * 1024 // 10 MB
// ─── Type detection ───────────────────────────────────────────────────────────
export function getExtension(fileName = '') {
const parts = String(fileName).toLowerCase().split('.')
return parts.length > 1 ? parts.pop() : ''
}
/**
* @param {File} file
* @returns {'image' | 'archive' | 'unsupported'}
*/
export function detectFileType(file) {
if (!file) return 'unknown'
const extension = getExtension(file.name)
const mime = String(file.type || '').toLowerCase()
if (IMAGE_MIME.has(mime) || IMAGE_EXTENSIONS.has(extension)) return 'image'
if (ARCHIVE_MIME.has(mime) || ARCHIVE_EXTENSIONS.has(extension)) return 'archive'
return 'unsupported'
}
// ─── Formatting ───────────────────────────────────────────────────────────────
export function formatBytes(bytes) {
if (!Number.isFinite(bytes) || bytes <= 0) return '—'
if (bytes < 1024) return `${bytes} B`
const kb = bytes / 1024
if (kb < 1024) return `${kb.toFixed(1)} KB`
const mb = kb / 1024
if (mb < 1024) return `${mb.toFixed(1)} MB`
return `${(mb / 1024).toFixed(2)} GB`
}
// ─── Image utils ──────────────────────────────────────────────────────────────
/**
* @param {File} file
* @returns {Promise<{width: number, height: number}>}
*/
export function readImageDimensions(file) {
return new Promise((resolve, reject) => {
const blobUrl = URL.createObjectURL(file)
const img = new Image()
img.onload = () => {
resolve({ width: img.naturalWidth, height: img.naturalHeight })
URL.revokeObjectURL(blobUrl)
}
img.onerror = () => {
reject(new Error('image_read_failed'))
URL.revokeObjectURL(blobUrl)
}
img.src = blobUrl
})
}
// ─── Category tree ────────────────────────────────────────────────────────────
export function buildCategoryTree(contentTypes = []) {
const rootsById = new Map()
contentTypes.forEach((type) => {
const categories = Array.isArray(type?.categories) ? type.categories : []
categories.forEach((category) => {
if (!category?.id) return
const key = String(category.id)
if (!rootsById.has(key)) {
rootsById.set(key, {
id: key,
name: category.name || `Category ${category.id}`,
children: [],
})
}
const root = rootsById.get(key)
const children = Array.isArray(category?.children) ? category.children : []
children.forEach((child) => {
if (!child?.id) return
const exists = root.children.some((item) => String(item.id) === String(child.id))
if (!exists) {
root.children.push({ id: String(child.id), name: child.name || `Subcategory ${child.id}` })
}
})
})
})
return Array.from(rootsById.values())
}
// ─── Content type helpers ─────────────────────────────────────────────────────
export function getContentTypeValue(type) {
if (!type) return ''
return String(type.id ?? type.key ?? type.slug ?? type.name ?? '')
}
export function getContentTypeVisualKey(type) {
const raw = String(type?.slug || type?.name || type?.key || '').toLowerCase()
const normalized = raw.replace(/\s+/g, '_').replace(/[^a-z0-9_]/g, '')
if (normalized.includes('wallpaper')) return 'wallpapers'
if (normalized.includes('skin')) return 'skins'
if (normalized.includes('photo')) return 'photography'
return 'other'
}
// ─── Processing status helpers ────────────────────────────────────────────────
export function getProcessingTransparencyLabel(processingStatus, machineState) {
if (!['processing', 'finishing', 'publishing'].includes(machineState)) return ''
const normalized = String(processingStatus || '').toLowerCase()
if (normalized === 'generating_preview') return 'Generating preview'
if (['processed', 'ready', 'published', 'queued', 'publish_ready'].includes(normalized)) {
return 'Preparing for publish'
}
return 'Analyzing content'
}