import { resourceMetadataJSON } from "@/features/resources/resourceMetadata.js"; export const resourceBatchUploadSize = 10; const resourceCodeMaxLength = 96; const resourceCodeSuffixLength = 3; const randomResourceCodeChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; const resourceTypeAliases = new Map([ ["头像框", "avatar_frame"], ["坐骑", "vehicle"], ["座驾", "vehicle"], ["气泡", "chat_bubble"], ["勋章", "badge"], ["徽章", "badge"], ["飘窗", "floating_screen"], ["飘屏", "floating_screen"], ["mic声波", "mic_seat_animation"], ["麦位动效", "mic_seat_animation"], ["背景卡", "profile_card"], ["资料卡", "profile_card"], ["礼物", "gift"], ]); const pricedResourceTypes = new Set(["avatar_frame", "vehicle", "gift"]); const translationTimeoutMs = 2500; const localNameTranslations = new Map([ ["星光", "starlight"], ["星辰", "stars"], ["守护", "guardian"], ["夜色", "nightfall"], ["律动", "rhythm"], ["清新", "fresh"], ["梦幻", "dream"], ["浪漫", "romance"], ["彩虹", "rainbow"], ["月光", "moonlight"], ["阳光", "sunshine"], ["火焰", "flame"], ["冰雪", "snow"], ["海洋", "ocean"], ["森林", "forest"], ["皇冠", "crown"], ["皇家", "royal"], ["黄金", "gold"], ["白银", "silver"], ["青铜", "bronze"], ["钻石", "diamond"], ["爱心", "heart"], ["玫瑰", "rose"], ["天使", "angel"], ["恶魔", "devil"], ["贵族", "noble"], ["幸运", "lucky"], ["超级", "super"], ["炫彩", "colorful"], ["流光", "glow"], ["闪耀", "sparkle"], ["典藏", "classic"], ["限定", "limited"], ]); export function parseResourceFolderFiles(fileList) { const files = Array.from(fileList || []).filter((file) => file?.name && !file.name.startsWith(".")); const groups = new Map(); files.forEach((file) => { const parsed = parseResourceFilename(file); if (!parsed.ok) { return; } const key = [parsed.resourceType, parsed.name, parsed.price, parsed.badgeForm].join("|"); const current = groups.get(key) || { badgeForm: parsed.badgeForm, coverFile: null, animationFile: null, name: parsed.name, price: parsed.price, resourceType: parsed.resourceType, }; const fileKey = parsed.role === "cover" ? "coverFile" : "animationFile"; if (!current[fileKey]) { current[fileKey] = file; } groups.set(key, current); }); const resources = Array.from(groups.values()) .filter((item) => item.coverFile && item.animationFile) .map((item, index) => ({ ...item, resourceCode: initialResourceCodeFor(item, index), })); return { errors: [], resources, }; } export function resourcePlanToPayload(item) { const coinPrice = pricedResourceTypes.has(item.resourceType) ? Number(item.price || 0) : 0; return { amount: 0, animationUrl: item.animationUrl, assetUrl: item.animationUrl, badgeForm: item.resourceType === "badge" ? item.badgeForm || "tile" : undefined, badgeKind: item.resourceType === "badge" ? "normal" : undefined, coinPrice, managerGrantEnabled: true, metadataJson: resourceMetadataJSON(item.resourceType, item.metadataJson), name: item.name, previewUrl: item.coverUrl, priceType: coinPrice > 0 ? "coin" : "free", resourceCode: item.resourceCode, resourceType: item.resourceType, sortOrder: 0, status: "active", }; } export async function translateResourceCodes(resources, options = {}) { const translated = []; for (let index = 0; index < resources.length; index += 1) { const resourceCode = await resourceCodeFor(resources[index], index); translated.push({ ...resources[index], resourceCode: appendRandomResourceCodeSuffix(resourceCode, index), }); options.onProgress?.({ completed: index + 1, index, resource: translated[index], total: resources.length, }); } return translated; } function parseResourceFilename(file) { const baseName = stripExtension(file.name); const parts = baseName .split("_") .map((part) => part.trim()) .filter(Boolean); if (parts.length < 3) { return { ok: false }; } const role = normalizeRole(parts[parts.length - 1]); if (!role) { return { ok: false }; } const resourceType = resourceTypeAliases.get(parts[0].toLowerCase()) || resourceTypeAliases.get(parts[0]); if (!resourceType) { return { ok: false }; } const name = parts[1]; if (!name) { return { ok: false }; } let price = 0; let badgeForm = "tile"; if (pricedResourceTypes.has(resourceType)) { price = Number(parts[2]); if (!Number.isInteger(price) || price <= 0) { return { ok: false }; } } else if (resourceType === "badge") { const formToken = parts.length >= 4 ? parts[2] : ""; badgeForm = formToken === "长" || formToken === "long" || formToken === "strip" ? "strip" : "tile"; } return { ok: true, badgeForm, name, price, resourceType, role }; } function normalizeRole(value) { const role = String(value || "") .trim() .toLowerCase(); if (role === "cover" || role === "封面") { return "cover"; } if (role === "animation" || role === "anim" || role === "动效") { return "animation"; } return ""; } async function resourceCodeFor(item, index) { const name = String(item.name || "").trim(); if (!name) { return fallbackResourceCode(index); } if (!hasChinese(name)) { return slugifyResourceCode(name) || fallbackResourceCode(index); } const translated = await translateChineseToEnglish(name); return slugifyResourceCode(translated) || fallbackResourceCode(index); } function initialResourceCodeFor(item, index) { return String(item.name || "").trim() || fallbackResourceCode(index); } async function translateChineseToEnglish(text) { const translators = [ translateWithMyMemory, (value) => translateWithLingva(value, "https://lingva.ml"), (value) => translateWithLingva(value, "https://lingva.lunar.icu"), (value) => translateWithLibreTranslate(value, "https://translate.argosopentech.com"), translateWithLocalDictionary, ]; for (const translator of translators) { try { const translated = String((await translator(text)) || "").trim(); if (translated && !hasChinese(translated)) { return translated; } } catch { // Public translation endpoints are best-effort; continue to the next provider. } } return ""; } async function translateWithMyMemory(text) { const url = `https://api.mymemory.translated.net/get?q=${encodeURIComponent(text)}&langpair=zh-CN|en`; const data = await fetchJSON(url); return data?.responseData?.translatedText || ""; } async function translateWithLingva(text, host) { const url = `${host}/api/v1/zh/en/${encodeURIComponent(text)}`; const data = await fetchJSON(url); return data?.translation || ""; } async function translateWithLibreTranslate(text, host) { const body = new URLSearchParams({ format: "text", q: text, source: "zh", target: "en", }); const data = await fetchJSON(`${host}/translate`, { body, headers: { "Content-Type": "application/x-www-form-urlencoded" }, method: "POST", }); return data?.translatedText || ""; } function translateWithLocalDictionary(text) { let index = 0; const parts = []; const keys = Array.from(localNameTranslations.keys()).sort((left, right) => right.length - left.length); while (index < text.length) { const rest = text.slice(index); const matched = keys.find((key) => rest.startsWith(key)); if (matched) { parts.push(localNameTranslations.get(matched)); index += matched.length; continue; } const char = text[index]; if (/[a-z0-9]/i.test(char)) { parts.push(char); index += 1; continue; } if (hasChinese(char)) { return ""; } index += 1; } return parts.join(" "); } async function fetchJSON(url, options = {}) { const controller = new AbortController(); const timeout = window.setTimeout(() => controller.abort(), translationTimeoutMs); try { const response = await fetch(url, { ...options, signal: controller.signal }); if (!response.ok) { return null; } return response.json(); } finally { window.clearTimeout(timeout); } } function slugifyResourceCode(value) { return String(value || "") .normalize("NFKD") .replace(/[\u0300-\u036f]/g, "") .replace(/['’]/g, "") .replace(/&/g, " and ") .replace(/[^a-z0-9]+/gi, "_") .replace(/^_+|_+$/g, "") .toLowerCase() .slice(0, resourceCodeMaxLength); } function fallbackResourceCode(index) { return `resource_${String(index + 1).padStart(2, "0")}`; } function appendRandomResourceCodeSuffix(value, index) { const suffix = `_${randomResourceCodeChar()}${randomResourceCodeChar()}`; const maxBaseLength = resourceCodeMaxLength - resourceCodeSuffixLength; const base = String(value || fallbackResourceCode(index)) .slice(0, maxBaseLength) .replace(/_+$/g, ""); return `${base || fallbackResourceCode(index)}${suffix}`; } function randomResourceCodeChar() { return randomResourceCodeChars[Math.floor(Math.random() * randomResourceCodeChars.length)]; } function hasChinese(value) { return /[\u3400-\u9fff]/.test(String(value || "")); } function stripExtension(filename) { return String(filename || "").replace(/\.[^.]+$/, ""); }