// ===================== 常量配置 ===================== const CONFIG_DIR = "/home/ztl/LJ360/config"; const LOGO_DIR = CONFIG_DIR + "/logos"; const FLASH_SCRIPT = "/home/ztl/LJ360/bash/flash_logo.sh"; // ===================== 工具函数 ===================== function runCmd(args, callback) { const proc = cockpit.spawn(args, { superuser: "require", err: "out" }); let output = ""; proc.stream(data => { output += data; }); proc.then(() => callback(output, null)); proc.catch(err => callback(output, err)); } function setLogoOutput(text) { const box = document.getElementById("logo-output"); if (box) { box.textContent = text; box.scrollTop = box.scrollHeight; } } function appendLogoOutput(text) { const box = document.getElementById("logo-output"); if (box) { box.textContent += text; box.scrollTop = box.scrollHeight; } } function showUploadStatus(msg, type) { const el = document.getElementById("upload-status"); if (!el) return; el.textContent = msg; el.className = "upload-status " + type; el.classList.remove("hidden"); if (type === "success") { setTimeout(() => el.classList.add("hidden"), 4000); } } // ===================== 标签页切换 ===================== function initTabs() { const tabItems = document.querySelectorAll(".pf-c-tabs__item"); tabItems.forEach(item => { const btn = item.querySelector(".pf-c-tabs__link"); btn.addEventListener("click", () => { // 切换激活样式 tabItems.forEach(i => i.classList.remove("pf-m-current")); item.classList.add("pf-m-current"); // 切换内容面板 const targetId = item.getAttribute("data-tab"); document.querySelectorAll(".tab-panel").forEach(panel => { panel.classList.add("hidden"); }); const target = document.getElementById(targetId); if (target) target.classList.remove("hidden"); }); }); } // ===================== 确保目录存在 ===================== function ensureDir(callback) { runCmd(["mkdir", "-p", LOGO_DIR], (out, err) => { if (err) { setLogoOutput("❌ 无法创建配置目录: " + (err.message || err)); } else { if (callback) callback(); } }); } // ===================== Logo 列表 ===================== let logoFiles = []; // 缓存当前文件列表 function refreshLogoList() { const listEl = document.getElementById("logo-list"); const selectEl = document.getElementById("flash-logo-select"); listEl.innerHTML = '
正在加载...
'; ensureDir(() => { runCmd(["bash", "-c", `ls -lh "${LOGO_DIR}"/*.bmp 2>/dev/null || echo "__EMPTY__"`], (out, err) => { logoFiles = []; if (err || !out || out.trim() === "__EMPTY__" || out.trim() === "") { listEl.innerHTML = '
暂无 Logo 文件,请上传
'; selectEl.innerHTML = ''; return; } // 解析 ls -lh 输出 const lines = out.trim().split("\n").filter(l => l && !l.startsWith("total")); lines.forEach(line => { // ls -lh 格式: -rw-r--r-- 1 root root 1.2M May 1 12:00 /path/to/file.bmp const parts = line.trim().split(/\s+/); if (parts.length >= 9) { const filePath = parts.slice(8).join(" "); const fileName = filePath.split("/").pop(); const fileSize = parts[4]; const fileDate = parts[5] + " " + parts[6] + " " + parts[7]; logoFiles.push({ path: filePath, name: fileName, size: fileSize, date: fileDate }); } }); if (logoFiles.length === 0) { listEl.innerHTML = '
暂无 Logo 文件,请上传
'; selectEl.innerHTML = ''; return; } // 渲染列表 listEl.innerHTML = ""; selectEl.innerHTML = ''; logoFiles.forEach(file => { // 列表项 const item = document.createElement("div"); item.className = "logo-list-item"; item.innerHTML = `
${file.name}
${file.size}  ·  ${file.date}
`; listEl.appendChild(item); // 下拉选项 const opt = document.createElement("option"); opt.value = file.name; opt.textContent = file.name + " (" + file.size + ")"; selectEl.appendChild(opt); }); // 绑定列表按钮事件 bindListItemEvents(); // 加载缩略图(读取文件转 base64 显示) logoFiles.forEach(file => loadThumb(file)); }); }); } function bindListItemEvents() { // 预览按钮 document.querySelectorAll(".btn-preview").forEach(btn => { btn.addEventListener("click", function() { const path = this.getAttribute("data-path"); const name = this.getAttribute("data-name"); previewLogoFile(path, name); }); }); // 选为烧写按钮 document.querySelectorAll(".btn-select-flash").forEach(btn => { btn.addEventListener("click", function() { const name = this.getAttribute("data-name"); const sel = document.getElementById("flash-logo-select"); if (sel) { sel.value = name; updateFlashButton(); // 滚动到烧写区域 sel.scrollIntoView({ behavior: "smooth", block: "center" }); } }); }); // 删除按钮 document.querySelectorAll(".btn-delete").forEach(btn => { btn.addEventListener("click", function() { const path = this.getAttribute("data-path"); const name = this.getAttribute("data-name"); if (confirm("确定要删除 " + name + " 吗?此操作不可恢复。")) { deleteLogo(path); } }); }); } // ===================== 缩略图加载 ===================== function loadThumb(file) { // 使用 cockpit.file 读取 BMP 文件,转为 base64 显示 const fileHandle = cockpit.file(file.path, { binary: true }); fileHandle.read().then(content => { fileHandle.close(); if (!content) return; // 将 Uint8Array 转为 base64 const b64 = uint8ToBase64(content); const dataUrl = "data:image/bmp;base64," + b64; // 设置缩略图 const thumbEl = document.getElementById("thumb-" + CSS.escape(file.name)); if (thumbEl) { thumbEl.innerHTML = `${file.name}`; } }).catch(() => { fileHandle.close(); }); } // ===================== 预览 Logo ===================== function previewLogoFile(path, name) { setLogoOutput("⏳ 正在读取 " + name + " ..."); const imgEl = document.getElementById("logo-preview-img"); const placeholder = document.getElementById("logo-placeholder"); const fileHandle = cockpit.file(path, { binary: true }); fileHandle.read().then(content => { fileHandle.close(); if (!content || content.length === 0) { setLogoOutput("❌ 文件内容为空或读取失败"); return; } const b64 = uint8ToBase64(content); const dataUrl = "data:image/bmp;base64," + b64; imgEl.src = dataUrl; imgEl.classList.remove("hidden"); placeholder.classList.add("hidden"); setLogoOutput("✅ 已预览:" + name + "\n路径:" + path + "大小:" + content.length + " 字节"); }).catch(err => { fileHandle.close(); setLogoOutput("❌ 读取文件失败:" + (err.message || err)); }); } // 读取当前设备 Logo(从分区) function readCurrentLogo() { setLogoOutput("⏳ 正在从设备分区读取 Logo...\n命令: dd if=/dev/disk/by-partlabel/logo of=/tmp/current_logo_preview.bmp bs=512 count=100 2>&1"); const imgEl = document.getElementById("logo-preview-img"); const placeholder = document.getElementById("logo-placeholder"); // 先用 dd 从分区读出前半段(第一个 BMP,即 splash logo) runCmd(["bash", "-c", "dd if=/dev/disk/by-partlabel/logo of=/tmp/current_logo_preview.bmp bs=512 2>/dev/null && echo OK" ], (out, err) => { if (err && !out.includes("OK")) { setLogoOutput("❌ 读取分区失败: " + (err.message || err) + "" + out +"提示:确认 /dev/disk/by-partlabel/logo 分区存在"); return; } // 读取保存的临时文件显示预览 const fileHandle = cockpit.file("/tmp/current_logo_preview.bmp", { binary: true }); fileHandle.read().then(content => { fileHandle.close(); if (!content || content.length === 0) { setLogoOutput("❌ 读取到的分区内容为空"); return; } const b64 = uint8ToBase64(content); const dataUrl = "data:image/bmp;base64," + b64; imgEl.src = dataUrl; imgEl.classList.remove("hidden"); placeholder.classList.add("hidden"); setLogoOutput("✅ 已读取设备当前 Logo\n分区:/dev/disk/by-partlabel/logo大小:" + content.length + " 字节"); }).catch(err => { fileHandle.close(); setLogoOutput("❌ 读取临时文件失败:" + (err.message || err)); }); }); } // ===================== 上传 Logo ===================== function handleFileUpload(file) { if (!file) return; // 检查扩展名 if (!file.name.toLowerCase().endsWith(".bmp")) { showUploadStatus("❌ 仅支持 .bmp 格式文件", "error"); return; } showUploadStatus("⏳ 正在上传 " + file.name + " ...", "uploading"); setLogoOutput("⏳ 正在上传文件:" + file.name); const reader = new FileReader(); reader.onload = function(e) { const arrayBuffer = e.target.result; const uint8 = new Uint8Array(arrayBuffer); const destPath = LOGO_DIR + "/" + file.name; ensureDir(() => { // 使用 cockpit.file 写入 const fileHandle = cockpit.file(destPath, { binary: true, superuser: "require" }); fileHandle.replace(uint8).then(() => { fileHandle.close(); showUploadStatus("✅ 上传成功:" + file.name, "success"); setLogoOutput("✅ 文件已保存到:" + destPath + "大小:" + uint8.length + " 字节"); refreshLogoList(); }).catch(err => { fileHandle.close(); showUploadStatus("❌ 上传失败:" + (err.message || err), "error"); setLogoOutput("❌ 写入文件失败:" + (err.message || err)); }); }); }; reader.onerror = function() { showUploadStatus("❌ 文件读取失败", "error"); }; reader.readAsArrayBuffer(file); } // ===================== 删除 Logo ===================== function deleteLogo(path) { setLogoOutput("⏳ 正在删除:" + path); runCmd(["rm", "-f", path], (out, err) => { if (err) { setLogoOutput("❌ 删除失败:" + (err.message || err)); } else { setLogoOutput("✅ 已删除:" + path); refreshLogoList(); } }); } // ===================== 烧写 Logo ===================== function updateFlashButton() { const sel = document.getElementById("flash-logo-select"); const btn = document.getElementById("btn-flash-logo"); if (sel && btn) { btn.disabled = !sel.value; } } function flashLogo() { const sel = document.getElementById("flash-logo-select"); if (!sel || !sel.value) return; const logoName = sel.value; const logoPath = LOGO_DIR + "/" + logoName; // 检查是否存在 logo_kernel.bmp const kernelPath = LOGO_DIR + "/logo_kernel.bmp"; setLogoOutput("⏳ 准备烧写...\n主 Logo:" + logoPath + "内核 Logo:" + kernelPath); // 检查文件是否存在 runCmd(["bash", "-c", `test -f "${logoPath}" && echo "logo_ok" || echo "logo_missing"`], (out1) => { if (out1.trim() !== "logo_ok") { setLogoOutput("❌ 找不到文件:" + logoPath); return; } runCmd(["bash", "-c", `test -f "${kernelPath}" && echo "kernel_ok" || echo "kernel_missing"`], (out2) => { const hasKernel = out2.trim() === "kernel_ok"; if (!hasKernel) { setLogoOutput("⚠️ 未找到 logo_kernel.bmp(" + kernelPath + ")\n将仅烧写主 Logo,跳过内核 Logo 合并步骤。\n\n⏳ 开始烧写..."); } else { setLogoOutput("✅ 文件检查通过\n⏳ 开始合并并烧写...\n"); } doFlash(logoPath, hasKernel ? kernelPath : null); }); }); } function doFlash(logoPath, kernelPath) { // 构建烧写命令(与原始命令一致) let cmd; if (kernelPath) { cmd = [ "bash", "-c", `set -e && ` + `cat "${logoPath}" > /tmp/logo && ` + `truncate -s %512 /tmp/logo && ` + `cat "${kernelPath}" >> /tmp/logo && ` + `cat /tmp/logo > /dev/disk/by-partlabel/logo && ` + `echo "FLASH_OK"` ]; } else { cmd = [ "bash", "-c", `set -e && ` + `cat "${logoPath}" > /tmp/logo && ` + `truncate -s %512 /tmp/logo && ` + `cat /tmp/logo > /dev/disk/by-partlabel/logo && ` + `echo "FLASH_OK"` ]; } runCmd(cmd, (out, err) => { if (err || !out.includes("FLASH_OK")) { setLogoOutput( "❌ 烧写失败!\n" + "错误信息:" + (err ? (err.message || err) : "未知错误") + "" + (out ? "\n输出:\n" + out : "") + "\n\n请检查:\n" + " 1. /dev/disk/by-partlabel/logo 分区是否存在" + " 2. 是否有足够权限写入分区" + " 3. BMP 文件格式是否正确" ); } else { setLogoOutput( "✅ 烧写成功!\n" + "主 Logo:" + logoPath + "" + (kernelPath ? "内核 Logo:" + kernelPath + "" : "") + "目标分区:/dev/disk/by-partlabel/logo\n" + "重启设备后新 Logo 将生效。" ); } }); } // ===================== 拖拽上传 ===================== function initDragDrop() { const area = document.getElementById("upload-area"); if (!area) return; area.addEventListener("dragover", e => { e.preventDefault(); area.classList.add("drag-over"); }); area.addEventListener("dragleave", () => { area.classList.remove("drag-over"); }); area.addEventListener("drop", e => { e.preventDefault(); area.classList.remove("drag-over"); const file = e.dataTransfer.files[0]; if (file) handleFileUpload(file); }); // 点击选择文件 const fileInput = document.getElementById("logo-file-input"); if (fileInput) { fileInput.addEventListener("change", function() { if (this.files[0]) handleFileUpload(this.files[0]); this.value = ""; // 重置,允许重复选同一文件 }); } } // ===================== Base64 工具 ===================== function uint8ToBase64(uint8Array) { let binary = ""; const chunkSize = 8192; for (let i = 0; i < uint8Array.length; i += chunkSize) { const chunk = uint8Array.subarray(i, i + chunkSize); binary += String.fromCharCode.apply(null, chunk); } return btoa(binary); } // ===================== 页面初始化 ===================== document.addEventListener("DOMContentLoaded", function() { initTabs(); initDragDrop(); refreshLogoList(); // 读取当前 Logo 按钮 const btnReadLogo = document.getElementById("btn-read-logo"); if (btnReadLogo) btnReadLogo.addEventListener("click", readCurrentLogo); // 刷新列表按钮 const btnRefreshList = document.getElementById("btn-refresh-list"); if (btnRefreshList) btnRefreshList.addEventListener("click", refreshLogoList); // 烧写按钮 const btnFlash = document.getElementById("btn-flash-logo"); if (btnFlash) btnFlash.addEventListener("click", flashLogo); // 下拉选择变化 const flashSel = document.getElementById("flash-logo-select"); if (flashSel) flashSel.addEventListener("change", updateFlashButton); });