Files
cockpit_avm/lj360/lj360_config.js
2026-04-21 15:38:57 +08:00

487 lines
18 KiB
JavaScript
Raw 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.
// ===================== 常量配置 =====================
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 = '<div class="logo-list-empty"><i class="fa fa-spinner fa-spin"></i> 正在加载...</div>';
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 = '<div class="logo-list-empty"><i class="fa fa-inbox"></i> 暂无 Logo 文件,请上传</div>';
selectEl.innerHTML = '<option value="">-- 暂无可用文件 --</option>';
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 = '<div class="logo-list-empty"><i class="fa fa-inbox"></i> 暂无 Logo 文件,请上传</div>';
selectEl.innerHTML = '<option value="">-- 暂无可用文件 --</option>';
return;
}
// 渲染列表
listEl.innerHTML = "";
selectEl.innerHTML = '<option value="">-- 请选择 Logo 文件 --</option>';
logoFiles.forEach(file => {
// 列表项
const item = document.createElement("div");
item.className = "logo-list-item";
item.innerHTML = `
<div class="logo-item-thumb" id="thumb-${CSS.escape(file.name)}">
<span class="logo-item-thumb-placeholder"><i class="fa fa-image"></i></span>
</div>
<div class="logo-item-info">
<div class="logo-item-name" title="${file.name}">${file.name}</div>
<div class="logo-item-meta">${file.size} &nbsp;·&nbsp; ${file.date}</div>
</div>
<div class="logo-item-actions">
<button class="pf-c-button pf-m-secondary btn-sm btn-preview" data-path="${file.path}" data-name="${file.name}">
<i class="fa fa-eye"></i> 预览
</button>
<button class="pf-c-button pf-m-secondary btn-sm btn-select-flash" data-name="${file.name}">
<i class="fa fa-bolt"></i> 选为烧写
</button>
<button class="pf-c-button pf-m-secondary btn-sm btn-delete" data-path="${file.path}" data-name="${file.name}">
<i class="fa fa-trash-o"></i>
</button>
</div>
`;
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 = `<img src="${dataUrl}" alt="${file.name}" style="max-width:100%;max-height:100%;object-fit:contain;">`;
}
}).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);
});