From c074cc70ae4fd228e5d218158a4b090e438549d8 Mon Sep 17 00:00:00 2001 From: wed150 Date: Sat, 23 May 2026 04:04:27 +0800 Subject: [PATCH] fix: mobile UI --- index.html | 1167 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 719 insertions(+), 448 deletions(-) diff --git a/index.html b/index.html index 4705059..e3cef3e 100644 --- a/index.html +++ b/index.html @@ -1,459 +1,730 @@ - - - - - - Construct Expansion Pack Generator - - - - - - - - - - - +// 当前语言设置(可通过 switchLang 切换) +let currentLang = 'zh'; -
- -
-

Construct Pack Generator

-

100% Offline · No Upload · Instant Convert

-
+/** + * 带变量替换的翻译函数 + * @param {string} key - i18n 键名 + * @param {Object} vars - 替换变量,如 {name: 'test.txt'} + * @returns {string} 翻译后的文本 + */ +function t(key, vars) { + let text = i18n[currentLang][key] || key; + if (vars) { + for (const [k, v] of Object.entries(vars)) { + text = text.replace('{' + k + '}', v); + } + } + return text; +} - -
-

- Upload Files - Supported: .mcstructure | .nbt | .litematic -

-
- Drag files here or click to select - - -
-
-
+/** + * 切换语言并应用到页面 + * @param {string} lang - 目标语言 'zh' 或 'en' + */ +function switchLang(lang) { + if (!i18n[lang]) return; + currentLang = lang; + // 存储到 localStorage + try { localStorage.setItem('lang', lang); } catch(e) {} + applyI18n(); +} - -
Open in browser for best experience
+/** + * 将 i18n 翻译应用到页面中所有带 data-i18n 属性的元素 + */ +function applyI18n() { + // 页面标题 + document.title = t('PAGE_TITLE'); + + // 遍历所有带 data-i18n 属性的元素 + document.querySelectorAll('[data-i18n]').forEach(el => { + const key = el.getAttribute('data-i18n'); + const val = i18n[currentLang][key]; + if (val !== undefined) { + if (el.tagName === 'INPUT' && el.type !== 'button') { + // input 元素:仅当当前值等于任一语言的默认值时才替换(避免覆盖用户输入) + const attr = el.getAttribute('data-i18n-attr') || 'value'; + const currentVal = el[attr]; + const allDefaults = Object.values(i18n).map(lang => lang[key]); + if (allDefaults.includes(currentVal) || currentVal === '') { + el[attr] = val; + } + } else { + el.textContent = val; + } + } + }); + + // 处理 data-i18n-placeholder 属性 + document.querySelectorAll('[data-i18n-placeholder]').forEach(el => { + const key = el.getAttribute('data-i18n-placeholder'); + const val = i18n[currentLang][key]; + if (val !== undefined) { + el.placeholder = val; + } + }); + + // 更新语言切换按钮文本 + const langBtn = document.getElementById('langSwitchBtn'); + if (langBtn) { + langBtn.textContent = currentLang === 'zh' ? 'EN' : 'ZH'; + } +} - -
-

Configuration

-
- - -
- -
+/** + * 初始化语言:优先系统语言,若有手动切换记录则使用手动选择,默认 zh + */ +function initLang() { + // 1. 先根据系统语言设置默认值 + try { + const sysLang = (navigator.language || navigator.userLanguage || 'zh').toLowerCase(); + currentLang = sysLang.startsWith('zh') ? 'zh' : 'en'; + } catch(e) {} + // 2. 若用户曾手动切换过语言,优先使用手动选择 + try { + const saved = localStorage.getItem('lang'); + if (saved && i18n[saved]) { + currentLang = saved; + } + } catch(e) {} +} - - +/** + * UUID生成器 - 符合RFC4122标准 + * @returns {string} 生成的UUID字符串 + */ +function generateUUID() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + const r = Math.random() * 16 | 0; + const v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} - - +/** + * 计算文件MD5哈希值用于去重 + * @param {File} file - 要计算哈希的文件对象 + * @returns {Promise} 文件的MD5哈希值 + */ +function computeFileHash(file) { + return new Promise((resolve, reject) => { + const spark = new SparkMD5.ArrayBuffer(); + const reader = new FileReader(); + + reader.onload = function(e) { + spark.append(e.target.result); + resolve(spark.end()); + }; + + reader.onerror = function() { + reject(new Error(i18n[currentLang].ERROR_FILE_READ)); + }; + + reader.readAsArrayBuffer(file); + }); +} - - -
+/** + * 验证文件类型是否为.mcstructure、.nbt 或.litematic + * @param {File} file - 要验证的文件对象 + * @returns {boolean} 是否为有效文件类型 + */ +function isValidFileType(file) { + const fileName = file.name.toLowerCase(); + return fileName.endsWith('.mcstructure') || + fileName.endsWith('.nbt') || + fileName.endsWith('.litematic'); +} - - - - - + const fileName = file.name.toLowerCase(); + let mcstructureData = null; + let convertedFileName = ''; + + if (fileName.endsWith('.litematic')) { + // Litematic 先转 NBT,再转 Mcstructure(需要两步转换) + if (typeof window.LitematicConverter === 'undefined') { + console.warn('LitematicConverter 未加载,跳过转换'); + return; + } + + const nbtResult = await window.LitematicConverter.convert(file); + if (!nbtResult.success) { + console.error(`Litematic 转 NBT 失败:${nbtResult.error}`); + return; + } + + if (typeof window.NBTToMcStructure === 'undefined') { + console.warn('NBTToMcStructure 未加载,跳过转换'); + return; + } + + await window.NBTToMcStructure.init('./data/'); + const buffer = nbtResult.data; + const result = await window.NBTToMcStructure.convert( + buffer, + file.name.replace(/\.litematic$/i, '.nbt'), + { version: '1.21.0.03' } + ); + + if (result.success) { + mcstructureData = result.data; + convertedFileName = file.name.replace(/\.litematic$/i, '.mcstructure'); + } else { + console.error(`NBT 转 Mcstructure 失败:${result.error}`); + return; + } + } else if (fileName.endsWith('.nbt')) { + // NBT 直接转 Mcstructure + if (typeof window.NBTToMcStructure === 'undefined') { + console.warn('NBTToMcStructure 未加载,跳过转换'); + return; + } + + await window.NBTToMcStructure.init('./data/'); + const buffer = await file.arrayBuffer(); + const result = await window.NBTToMcStructure.convert( + buffer, + file.name, + { version: '1.21.0.03' } + ); + + if (result.success) { + mcstructureData = result.data; + convertedFileName = file.name.replace(/\.nbt$/i, '.mcstructure'); + } else { + console.error(`NBT 转 Mcstructure 失败:${result.error}`); + return; + } + } + + if (mcstructureData) { + this.convertedFiles.set(hash, { + data: mcstructureData, + name: convertedFileName, + originalName: file.name + }); + console.log(`文件转换成功:${file.name} → ${convertedFileName}`); + } + } catch (error) { + console.error(`转换文件 ${file.name} 失败:`, error); + } + } + + /** + * 清空所有文件 + */ + clearFiles() { + this.uploadedFiles = []; + this.fileHashes.clear(); + this.generatedZipBlob = null; + this.convertedFiles.clear(); + } + + /** + * 获取文件数量 + * @returns {number} 文件总数 + */ + getFileCount() { + return this.uploadedFiles.length; + } + + /** + * 根据哈希值删除文件 + * @param {string} hash - 文件哈希值 + * @returns {boolean} 是否成功删除 + */ + removeFileByHash(hash) { + const initialLength = this.uploadedFiles.length; + this.uploadedFiles = this.uploadedFiles.filter(f => f.hash !== hash); + this.fileHashes.delete(hash); + this.convertedFiles.delete(hash); // 同时删除转换后的文件 + return this.uploadedFiles.length < initialLength; + } +} + +/** + * ZIP生成器类 + */ +class ZipGenerator { + constructor(fileProcessor, progressManager = null) { + this.fileProcessor = fileProcessor; + this.progressManager = progressManager; + this.zip = new JSZip(); + } + + /** + * 生成完整的ZIP包 + * @param {string} projectName - 项目名称 + * @returns {Promise} 生成的ZIP文件Blob + */ + async generate(projectName) { + try { + // 更新进度:开始生成 + this.updateProgress(10); + + // 生成manifest.json配置文件 + this.generateManifest(projectName); + this.updateProgress(30); + + // 生成脚本文件 + await this.generateScripts(); + this.updateProgress(50); + + // 添加图标文件 + await this.addIcon(); + this.updateProgress(70); + + // 添加用户上传的结构文件 + await this.addStructureFiles(); + this.updateProgress(90); + + // 生成最终的ZIP文件 + const blob = await this.createZipBlob(); + this.updateProgress(100); + + return blob; + } catch (error) { + console.error('生成ZIP文件失败:', error); + throw error; + } + } + + /** + * 更新进度 + * @param {number} percentage - 进度百分比 + */ + updateProgress(percentage) { + if (this.progressManager) { + this.progressManager.update(percentage); + } + } + + /** + * 生成manifest.json配置文件 + * @param {string} projectName - 项目名称 + */ + generateManifest(projectName) { + const manifest = { + format_version: 2, + header: { + name: `投影-扩展包-${projectName}`, + description: `编号ID:${generateUUID()}扩展包模板 §l§4该扩展包需依赖于投影模组运行,作者:EnderTrekker&wed15`, + uuid: generateUUID(), + version: [1, 0, 0], + min_engine_version: [1, 21, 90] + }, + modules: [ + { + type: "data", + uuid: generateUUID(), + version: [1, 0, 0] + }, + { + type: "script", + language: "javascript", + uuid: generateUUID(), + entry: "scripts/main.js", + version: [1, 0, 0] + } + ], + dependencies: [ + { + module_name: "@minecraft/server", + version: "2.1.0" + } + ] + }; + + this.zip.file("manifest.json", JSON.stringify(manifest, null, 2)); + } + + /** + * 生成脚本文件 + */ + async generateScripts() { + const scriptsFolder = this.zip.folder("scripts"); + let fileNames = this.fileProcessor.uploadedFiles + + .map(file => file.name.replace(/\.mcstructure$/i, '')) + .map(file => JSON.stringify(file)) + + .join("或\n"); + + + + + const mainJsContent = `//@ts-ignore +import { world, system, EasingType } from "@minecraft/server"; + +const HELP_MESSAGE = \`使用打开构筑菜单,点击创建新项目,输入:\\n${fileNames}\\n点击应用放置对应项目\`; +world.afterEvents.playerSpawn.subscribe((event) => { + if (event.initialSpawn) { + system.runTimeout(() => { + event.player.sendMessage(HELP_MESSAGE); + }, 20); + } +}); +//@ts-ignore +world.beforeEvents.chatSend.subscribe((event) => { + var freeCameraRunId = Number(event.sender.getDynamicProperty("help")) || 0; + if (event.message === "提示") { + event.cancel = true; + event.sender.sendMessage(HELP_MESSAGE); + return; + } +});`; + + scriptsFolder.file("main.js", mainJsContent); + } + + /** + * 添加图标文件 + * @returns {Promise} + */ + async addIcon() { + try { + const imageUrl = `./pack_icon.png`; + const response = await fetch(imageUrl); + if (response.ok) { + const blob = await response.blob(); + this.zip.file("pack_icon.png", blob); + } + } catch (error) { + console.warn(i18n[currentLang].ERROR_IMAGE_FETCH, error); + } + } + + /** + * 添加结构文件到 ZIP(包括转换后的文件) + * @returns {Promise} + */ + async addStructureFiles() { + const structuresFolder = this.zip.folder("structures"); + const files = this.fileProcessor.uploadedFiles; + + for (const item of files) { + let arrayBuffer; + let fileName = item.name; + const fileNameLower = item.name.toLowerCase(); + const converted = this.fileProcessor.convertedFiles.get(item.hash); + + if (converted) { + // 使用已转换的 mcstructure 文件 + arrayBuffer = converted.data; + fileName = converted.name; + } else if (fileNameLower.endsWith('.nbt') || fileNameLower.endsWith('.litematic')) { + // 延迟转换 + await this.fileProcessor.convertFile(item.file, item.hash); + const convertedAfter = this.fileProcessor.convertedFiles.get(item.hash); + if (convertedAfter) { + arrayBuffer = convertedAfter.data; + fileName = convertedAfter.name; + } else { + // 转换失败,回退为原始文件 + arrayBuffer = await item.file.arrayBuffer(); + } + } else { + arrayBuffer = await item.file.arrayBuffer(); + } + + structuresFolder.file(fileName, arrayBuffer); + } + } + + /** + * 创建ZIP文件Blob + * @returns {Promise} ZIP文件Blob + */ + async createZipBlob() { + return await this.zip.generateAsync({ + type: "blob", + compression: "DEFLATE", + compressionOptions: { + level: 6 + } + }); + } +} + +/** + * 进度管理器 + */ +class ProgressManager { + constructor(progressBar, progressText, statusMessage) { + this.progressBar = progressBar; + this.progressText = progressText; + this.statusMessage = statusMessage; + } + + /** + * 更新进度显示 + * @param {number} percentage - 进度百分比 (0-100) + */ + update(percentage) { + this.progressBar.style.width = percentage + '%'; + this.progressText.textContent = Math.round(percentage) + '%'; + } + + /** + * 显示状态消息 + * @param {string} message - 消息内容 + * @param {string} color - 文字颜色 + */ + showStatus(message, color = 'black') { + this.statusMessage.textContent = message; + this.statusMessage.style.color = color; + console.log(message) + } + + /** + * 重置进度 + */ + reset() { + this.update(0); + this.progressText.textContent = i18n[currentLang].PROGRESS_TEXT; + this.showStatus(''); + } +}