// ===================== 常量配置 =====================
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 = `
`;
}
}).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);
});