Files
window-axis-innovators-box1.17/javascript/HtmlJarViewer.html
tzdwindows 7 8de2b0f2fe chore(jcef): 更新缓存数据库日志文件
- 更新 shared_proto_db/metadata/000003.log 文件内容
- 更新 Site Characteristics Database/00003.log 文件内容
- 添加新的数据库条目和元数据记录
- 保持数据库文件格式的一致性
- 删除Vivid2D的内容
- 重写启动加载界面
2026-01-02 17:12:54 +08:00

1217 lines
64 KiB
HTML
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.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Java Decompiler Pro</title>
<link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
<style>
:root {
--bg-app: #2b2b2b;
--bg-sidebar: #313335;
--bg-header: #313335;
--bg-editor: #2b2b2b;
--border-color: #3c3f41;
--border-focus: #528bff;
--accent-color: #528bff;
--text-main: #e6e6e6;
--text-sub: #9aa0a6;
--item-hover: #3a3c3f;
--font-ui: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
--font-code: 'JetBrains Mono', 'Fira Code', monospace;
--muted: rgba(154,160,166,0.85);
}
/* --- 全局布局 --- */
* { box-sizing: border-box; }
html,body { height: 100%; }
body { margin: 0; background: var(--bg-app); color: var(--text-main); font-family: var(--font-ui); height: 100vh; display: flex; flex-direction: column; overflow: hidden; font-size: 13px; -webkit-font-smoothing:antialiased; }
/* --- 顶部栏 (含下拉搜索) --- */
.header { height: 48px; background: linear-gradient(180deg, rgba(0,0,0,0.06), transparent), var(--bg-header); display: flex; align-items: center; padding: 0 14px; border-bottom: 1px solid var(--border-color); gap: 10px; flex-shrink: 0; position: relative; z-index: 100; }
.brand { font-weight: 600; font-size: 15px; color: var(--text-main); display: flex; align-items: center; gap: 10px; margin-right: 8px; letter-spacing: 0.2px; }
.brand i { color: var(--accent-color); font-size: 18px; display:inline-block; transform: translateY(1px); animation: pulse 2.8s infinite; }
.toolbar-btn { background: transparent; border: 1px solid transparent; color: var(--text-sub); padding: 6px 12px; font-size: 12px; cursor: pointer; border-radius: 6px; display: flex; align-items: center; gap: 8px; font-weight: 500; transition: all .18s cubic-bezier(.2,.9,.2,1); position: relative; overflow: hidden; }
.toolbar-btn:hover { background: rgba(82,139,255,0.12); color: var(--text-main); border-color: rgba(82,139,255,0.14); transform: translateY(-1px); box-shadow: 0 6px 18px rgba(12,18,35,0.35); }
/* 搜索框 */
.search-container { flex: 1; max-width: 720px; display: flex; align-items: center; background: rgba(255,255,255,0.02); border: 1px solid var(--border-color); border-radius: 8px; padding: 6px; margin-left: auto; position: relative; box-shadow: inset 0 -1px 0 rgba(0,0,0,0.3); transition: box-shadow .18s; }
.search-container:focus-within { border-color: var(--border-focus); box-shadow: 0 10px 30px rgba(12,18,35,0.45); }
.search-mode-switch { background: transparent; color: var(--muted); border: none; border-right: 1px solid rgba(255,255,255,0.03); cursor: pointer; font-size: 12px; padding: 6px 10px; outline: none; -webkit-appearance:none; border-radius:6px; }
.search-input { flex: 1; background: transparent; border: none; color: var(--text-main); padding: 6px 10px; outline: none; font-size: 13px; font-family: var(--font-code); }
.icon-btn { background: transparent; border: none; color: var(--muted); padding: 6px 8px; cursor: pointer; border-radius: 6px; position: relative; }
.icon-btn:hover { color: var(--text-main); background: rgba(255,255,255,0.02); transform: translateY(-1px); }
/* 下拉搜索结果 */
.search-dropdown { position: absolute; top: calc(100% + 10px); left: 0; right: 0; background: linear-gradient(180deg, #2a2a2a, #222225); border: 1px solid rgba(255,255,255,0.04); box-shadow: 0 18px 60px rgba(0,0,0,0.75); z-index: 1200; max-height: 420px; overflow-y: auto; display: none; border-radius: 10px; padding: 8px; transform-origin: top center; animation: dropdownHide .18s ease forwards; }
.search-dropdown.show { display: block; animation: dropdownShow .18s cubic-bezier(.2,.9,.2,1) forwards; }
.search-dropdown::-webkit-scrollbar { width: 10px; }
.search-dropdown::-webkit-scrollbar-track { background: transparent; }
.search-dropdown::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.04); border-radius: 6px; }
/* --- 主区域 --- */
.main-container { flex: 1; display: flex; overflow: hidden; min-height: 0; }
.sidebar { width: 300px; background: var(--bg-sidebar); border-right: 1px solid var(--border-color); display: flex; flex-direction: column; flex-shrink: 0; min-width: 200px; max-width: 600px; transition: width .18s cubic-bezier(.2,.9,.2,1); }
.resizer { width: 6px; cursor: col-resize; background: linear-gradient(180deg, rgba(255,255,255,0.02), transparent); transition: background .12s; }
.resizer:hover { background: rgba(255,255,255,0.02); }
.editor-area { flex: 1; display: flex; flex-direction: column; background: var(--bg-editor); position: relative; min-width: 0; transition: background .2s; }
/* --- 侧边栏 --- */
.sidebar-header { height: 40px; padding: 0 14px; font-size: 11px; font-weight: 700; text-transform: uppercase; color: var(--text-sub); display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--border-color); letter-spacing: .6px; }
.sidebar-content-wrapper { flex: 1; overflow: auto; padding-top: 8px; }
/* 树节点样式 */
.tree-node, .folder-node { cursor: pointer; padding: 8px 14px; white-space: nowrap; font-size: 13px; color: var(--text-main); display: flex; align-items: center; user-select: none; border-left: 3px solid transparent; transition: all .18s cubic-bezier(.2,.9,.2,1); border-radius: 6px; margin: 6px 8px; transform-origin: left center; }
.tree-node:hover, .folder-node:hover { background: var(--item-hover); color: var(--text-main); transform: translateX(6px); box-shadow: 0 6px 18px rgba(0,0,0,0.45); }
.tree-node.selected { background: rgba(82,139,255,0.12); color: var(--text-main); border-left-color: var(--accent-color); box-shadow: inset 3px 0 0 var(--accent-color); }
.folder-content { padding-left: 0; display: none; transition: max-height .22s ease, opacity .18s ease; overflow: hidden; }
.folder-content.open { display: block; }
.tree-node i, .folder-node i { margin-right: 8px; font-size: 16px; color: var(--text-sub); opacity: 0.95; transition: transform .18s; }
.ri-java-line { color: #e2892f !important; }
.ri-folder-3-fill { color: #f1c40f !important; }
.arrow { margin-right: 6px; width: 16px; text-align: center; transition: transform 0.18s; color: var(--muted); }
.folder-node.open .arrow { transform: rotate(90deg); }
/* --- 标签页 --- */
.tabs-container { height: 48px; background: var(--bg-sidebar); display: flex; overflow-x: auto; border-bottom: 1px solid var(--border-color); align-items:center; padding-left:8px; gap:6px; }
.tab { display: flex; align-items: center; padding: 8px 12px; background: transparent; color: var(--text-sub); cursor: pointer; font-size: 13px; min-width: 120px; max-width: 360px; border-top: 2px solid transparent; border-radius: 10px; margin-right:6px; transition: all .22s cubic-bezier(.2,.9,.2,1); opacity: 0; transform: translateY(-6px); animation: tabIn .22s forwards; }
.tab.active { background: rgba(255,255,255,0.02); color: var(--text-main); border-top-color: var(--accent-color); box-shadow: 0 6px 18px rgba(0,0,0,0.6); transform: translateY(0); }
.tab-title { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-left: 6px; }
.tab-close { margin-left: 8px; font-size: 14px; opacity: 0; border-radius: 6px; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; }
.tab:hover .tab-close { opacity: 1; }
/* --- 通用列表项 (搜索结果/引用查找) --- */
.search-result-item, .usage-item { padding: 10px 12px; border-bottom: 1px solid rgba(255,255,255,0.02); cursor: pointer; display: flex; flex-direction: column; gap: 6px; border-radius: 6px; transition: background .12s, transform .12s; }
.search-result-item:hover, .usage-item:hover { background: rgba(255,255,255,0.02); transform: translateX(6px); }
.res-file, .usage-path { font-weight: 600; font-size: 13px; color: var(--text-main); display: flex; align-items: center; justify-content: space-between; }
.res-path { font-size: 12px; color: var(--text-sub); font-weight: normal; margin-left: 10px; }
.res-code, .usage-preview { font-family: var(--font-code); font-size: 12px; color: #bfb7d1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 4px; }
.usage-preview { background: rgba(0,0,0,0.06); padding: 6px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.02); color: #cfcfe8; }
.highlight, .usage-match { color: #ffc66d; font-weight: bold; background: rgba(255,198,109,0.06); padding: 0 2px; border-radius: 2px; }
/* --- 模态窗 & 遮罩 --- */
.modal-overlay { position: fixed; inset: 0; background: rgba(10,10,10,0.55); z-index: 2000; display: none; align-items: flex-start; justify-content: center; padding-top: 80px; }
.modal { background: #2f3133; width: 760px; max-width: 96%; border: 1px solid rgba(255,255,255,0.03); box-shadow: 0 22px 70px rgba(0,0,0,0.8); display: flex; flex-direction: column; max-height: 72vh; border-radius: 12px; overflow: hidden; }
.modal-header { padding: 12px 16px; background: rgba(0,0,0,0.06); border-bottom: 1px solid rgba(255,255,255,0.02); font-weight: 700; display: flex; justify-content: space-between; align-items: center; color: var(--text-main); }
.modal-body { overflow-y: auto; flex: 1; padding: 12px; }
#loading { position: absolute; inset: 0; background: rgba(43,43,43,0.76); display: none; flex-direction: column; align-items: center; justify-content: center; color: white; z-index: 999; backdrop-filter: blur(2px); border-radius: 6px; }
#media-overlay { display: none; flex: 1; align-items: center; justify-content: center; background: var(--bg-editor); flex-direction: column; padding: 18px; gap:12px; overflow: auto; }
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--text-sub); gap: 10px; padding: 28px; border-radius: 8px; }
.empty-state i { font-size: 48px; opacity: 0.15; transform: translateY(0); animation: float 3s ease-in-out infinite; }
.editor-line-highlight { background: rgba(82,139,255,0.10) !important; }
/* 媒体查看器控制栏 */
.media-toolbar { width: 100%; max-width: 1200px; display:flex; align-items:center; justify-content:space-between; gap:10px; padding:8px 12px; border-radius:8px; background: linear-gradient(180deg, rgba(0,0,0,0.25), rgba(0,0,0,0.12)); border:1px solid rgba(255,255,255,0.02); }
.media-toolbar-left { display:flex; align-items:center; gap:10px; }
.media-title { font-weight:600; color:var(--text-main); }
.media-actions { display:flex; gap:8px; }
.media-actions button { background: transparent; border: 1px solid rgba(255,255,255,0.04); color: var(--text-sub); padding:6px 10px; border-radius:8px; cursor:pointer; transition: all .16s; }
.media-actions button:hover { color: var(--text-main); border-color: rgba(82,139,255,0.14); background: rgba(255,255,255,0.02); transform: translateY(-2px); }
/* 图片容器可缩放 */
.media-image { max-width: 100%; max-height: calc(80vh - 120px); object-fit: contain; border-radius:6px; box-shadow: 0 6px 18px rgba(0,0,0,0.6); transition: transform .18s cubic-bezier(.2,.9,.2,1); }
/* JSON/TOML 视觉化容器 */
.kv-visual { width: 100%; max-width: 1000px; background: linear-gradient(180deg,#262626,#202123); border-radius: 10px; padding: 12px; box-shadow: 0 12px 40px rgba(0,0,0,0.6); border: 1px solid rgba(255,255,255,0.03); color: var(--text-main); }
.kv-toolbar { display:flex; gap:8px; align-items:center; margin-bottom:8px; }
.kv-tree { font-family: var(--font-code); font-size: 13px; color: var(--text-main); max-height: 60vh; overflow: auto; padding: 6px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.02); background: rgba(0,0,0,0.06); }
.kv-node { padding: 8px; border-radius: 8px; margin: 6px 4px; transition: all .14s; display:flex; align-items:flex-start; gap:10px; }
.kv-key { min-width: 180px; font-weight:700; color: var(--accent-color); }
.kv-value { flex:1; color: var(--text-sub); word-break:break-word; }
.kv-children { margin-left: 12px; border-left: 1px dashed rgba(255,255,255,0.03); padding-left: 12px; transition: max-height .16s ease; overflow: hidden; }
.kv-toggle { cursor:pointer; user-select:none; display:inline-flex; align-items:center; gap:6px; color:var(--muted); }
.kv-badge { background: rgba(255,255,255,0.04); color: var(--text-sub); padding: 2px 6px; border-radius: 6px; font-size: 11px; }
/* 小屏下按钮布局 */
@media (max-width: 700px) {
.media-toolbar { flex-direction: column; align-items:flex-start; gap:8px; }
.search-container { max-width: 100%; margin-left: 8px; margin-right: 8px; }
}
/* --- 动画 keyframes --- */
@keyframes pulse {
0% { transform: translateY(1px) scale(1); opacity:1; }
50% { transform: translateY(-1px) scale(1.06); opacity:0.9; }
100% { transform: translateY(1px) scale(1); opacity:1; }
}
@keyframes float {
0% { transform: translateY(0); }
50% { transform: translateY(-6px); }
100% { transform: translateY(0); }
}
@keyframes dropdownShow { from { opacity: 0; transform: translateY(-6px) scale(.99); } to { opacity: 1; transform: translateY(0) scale(1); } }
@keyframes dropdownHide { from { opacity: 1; } to { opacity: 0; } }
@keyframes tabIn { to { opacity: 1; transform: translateY(0); } }
/* ripple */
.ripple {
position: absolute;
border-radius: 50%;
transform: scale(0);
animation: rippleAnim 600ms linear;
background: rgba(255,255,255,0.08);
pointer-events: none;
}
@keyframes rippleAnim {
to { transform: scale(4); opacity: 0; }
}
/* collapse animation helper */
.collapsed { max-height: 0; opacity: 0; }
.expanded { opacity: 1; max-height: 2000px; }
</style>
<script>var require = { paths: { 'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.36.1/min/vs' } };</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.36.1/min/vs/loader.min.js"></script>
</head>
<body>
<div class="header">
<div class="brand"><i class="ri-code-box-line"></i> JD-PRO</div>
<button class="toolbar-btn" onclick="requestOpenJar()" onmousedown="createRipple(event)">
<i class="ri-folder-open-line"></i> 打开 JAR
</button>
<!-- 搜索区域 -->
<div class="search-container">
<select id="search-mode" class="search-mode-switch" title="搜索模式">
<option value="file">文件名</option>
<option value="string">常量池 (极速/无行号)</option>
<option value="full_string">全字符串 (反编译/含上下文)</option>
<option value="content">全局代码</option>
</select>
<input type="text" id="global-search" class="search-input"
placeholder="全局搜索 (文件名/内容)..."
autocomplete="off"
oninput="performSearch()"
onfocus="performSearch()"
onkeydown="navigateResults(event)">
<button class="icon-btn" onclick="performSearch()" title="搜索" onmousedown="createRipple(event)"><i class="ri-search-line"></i></button>
<!-- 下拉结果容器 -->
<div id="search-dropdown" class="search-dropdown" aria-hidden="true"></div>
</div>
</div>
<div class="main-container">
<div class="sidebar" id="sidebar">
<div class="sidebar-header">
<span id="sidebar-title">资源管理器</span>
<i class="ri-menu-unfold-line" style="cursor: pointer; opacity: 0.6"></i>
</div>
<div class="sidebar-content-wrapper" id="sidebar-content">
<div id="file-tree-view">
<div class="empty-state">
<i class="ri-inbox-archive-line"></i>
<p>拖入 JAR 或点击打开</p>
</div>
</div>
</div>
</div>
<!-- 可拖动拉宽的分隔器 -->
<div id="sidebar-resizer" class="resizer" title="拖动调整左侧宽度"></div>
<div class="editor-area" id="editor-area">
<div class="tabs-container" id="tabs-bar"></div>
<div id="monaco-container" style="flex:1; min-height:0; display:flex; position:relative;">
<div class="empty-state" style="background: var(--bg-editor); width:100%;">
<i class="ri-command-line"></i>
<p>请选择文件以查看源代码</p>
</div>
</div>
<div id="media-overlay"></div>
<div id="loading"><i class="ri-loader-4-line ri-spin" style="font-size:32px;color:var(--accent-color);margin-bottom:10px"></i>处理中...</div>
</div>
</div>
<!-- 引用查找模态窗 -->
<div class="modal-overlay" id="generic-modal" aria-hidden="true">
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-title">
<div class="modal-header">
<span id="modal-title">搜索结果</span>
<span style="cursor:pointer; opacity: 0.8;" onclick="closeModal()"><i class="ri-close-line"></i></span>
</div>
<div class="modal-body" id="modal-list"></div>
</div>
</div>
<script>
// --- 变量定义 ---
let editor;
let allFilesList = [];
let searchDebounceTimer;
const MIN_SIDEBAR = 200, MAX_SIDEBAR = 600;
// helper: 归一化文件名(剔除内部类、更正 .class 后的数字后缀)
function sanitizeFileName(name) {
if (!name) return name;
name = name.replace(/(\.class)\d+$/i, '$1');
if (/\.class$/i.test(name)) {
name = name.replace(/\$.*(?=\.class$)/, '');
}
return name;
}
// helper: 通过原始 file 对象构造 canonical path key目录 + sanitized 文件名)
function canonicalKeyForPath(path) {
const parts = path.split('/');
const dir = parts.slice(0, -1).join('/');
const leaf = sanitizeFileName(parts[parts.length - 1]);
return (dir ? dir + '/' : '') + leaf;
}
// ripple effect for buttons
function createRipple(e) {
const target = e.currentTarget || e.target;
const rect = target.getBoundingClientRect();
const circle = document.createElement('span');
circle.className = 'ripple';
const size = Math.max(rect.width, rect.height) * 1.2;
circle.style.width = circle.style.height = size + 'px';
circle.style.left = (e.clientX - rect.left - size / 2) + 'px';
circle.style.top = (e.clientY - rect.top - size / 2) + 'px';
target.appendChild(circle);
setTimeout(() => circle.remove(), 650);
}
// --- Tab 管理 ---
const TabManager = {
tabs: [], activePath: null,
open(path, content, type, mime) {
const existing = this.tabs.find(t => t.path === path);
if (existing) { this.activate(path); return; }
// detect display type from extension if not provided
const ext = (path.split('.').pop() || '').toLowerCase();
if (!type) {
if (mime && mime.startsWith('image/')) type = 'image';
else if (ext === 'png' || ext === 'jpg' || ext === 'jpeg' || ext === 'gif') type = 'image';
else if (ext === 'mp3' || ext === 'wav') type = 'audio';
else if (ext === 'mp4' || ext === 'webm') type = 'video';
else if (ext === 'json') type = 'json';
else if (ext === 'toml') type = 'toml';
else if (mime && mime.includes('json')) type = 'json';
else if (mime && mime.includes('toml')) type = 'toml';
else type = 'java';
}
let model = null;
// for textual languages we create a monaco model to allow raw view
if (type === 'java' || type === 'text' || type === 'json') {
const language = type === 'java' ? 'java' : (type === 'json' ? 'json' : 'plaintext');
try { model = monaco.editor.createModel(content, language); } catch(e){ model = null; }
}
this.tabs.push({ path, name: path.split('/').pop(), model, viewState: null, type: type, mime, content, raw: content, visualMode: (type === 'json' ? 'auto' : (type === 'toml' ? 'visual' : 'raw')) });
this.renderTabs();
this.activate(path);
},
activate(path) {
if (this.activePath && editor && editor.getModel()) {
const old = this.tabs.find(t => t.path === this.activePath);
if (old && old.model) old.viewState = editor.saveViewState();
}
this.activePath = path;
const tab = this.tabs.find(t => t.path === path);
if (!tab) return;
const ed = document.getElementById('monaco-container');
const md = document.getElementById('media-overlay');
const empty = ed.querySelector('.empty-state');
if (empty) empty.style.display = 'none';
// 媒体显示image/video/audio或者可视化 (json/toml)
if (tab.type === 'image' || tab.type === 'video' || tab.type === 'audio' || tab.type === 'toml' || tab.type === 'json') {
ed.style.display = 'none';
md.style.display = 'flex';
md.innerHTML = ''; // 先清空
const toolbar = document.createElement('div');
toolbar.className = 'media-toolbar';
const left = document.createElement('div'); left.className = 'media-toolbar-left';
left.innerHTML = `<div class="media-title">${tab.name}</div>`;
const actions = document.createElement('div'); actions.className = 'media-actions';
// common actions: download / open new / toggle view for json/toml
const downloadBtn = document.createElement('button'); downloadBtn.title='下载'; downloadBtn.innerHTML = '<i class="ri-download-line"></i> 下载';
downloadBtn.onclick = () => {
if (tab.type === 'image' || tab.type === 'video' || tab.type === 'audio') {
const a = document.createElement('a');
const dataUrl = `data:${tab.mime};base64,${tab.content}`;
a.href = dataUrl;
a.download = tab.name;
document.body.appendChild(a);
a.click();
a.remove();
} else {
const blob = new Blob([tab.raw], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = tab.name;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}
};
const openBtn = document.createElement('button'); openBtn.title='在新窗口打开'; openBtn.innerHTML = '<i class="ri-external-link-line"></i> 新窗打开';
openBtn.onclick = () => {
// 1. 获取内容和类型
let dataUrl = '';
if (tab.type === 'image' || tab.type === 'video' || tab.type === 'audio') {
// 二进制媒体,直接使用 Base64
dataUrl = `data:${tab.mime};base64,${tab.content}`;
} else {
// 2. 文本类型:构建一个更像编辑器的 HTML 页面
// --- A. 定义样式 (类似于 Monaco Editor 的暗色主题) ---
const cssStyle = `
body {
margin: 0; padding: 0;
background: #2b2b2b; color: #a9b7c6;
font-family: 'JetBrains Mono', 'Consolas', 'Courier New', monospace;
overflow: hidden; /* 防止双滚动条 */
display: flex; flex-direction: column; height: 100vh;
}
/* 顶部搜索工具栏 */
#toolbar {
height: 40px; background: #3c3f41;
border-bottom: 1px solid #555;
display: flex; align-items: center; justify-content: space-between;
padding: 0 16px; flex-shrink: 0;
}
.file-info { font-size: 13px; color: #888; font-weight: bold; }
/* 搜索区域 */
#search-box {
display: flex; align-items: center; gap: 8px;
background: #313335; padding: 4px; border-radius: 4px; border: 1px solid #555;
}
#search-input {
background: transparent; border: none; color: #e6e6e6; outline: none;
font-family: inherit; font-size: 13px; width: 200px;
}
.btn {
background: #4c5052; border: none; color: #bbb;
cursor: pointer; padding: 2px 8px; border-radius: 2px;
font-size: 12px; height: 24px; display:flex; align-items:center; justify-content:center;
}
.btn:hover { background: #5c6164; color: #fff; }
/* 代码内容区域 */
#code-container {
flex: 1; overflow: auto; padding: 10px;
counter-reset: line;
}
pre { margin: 0; font-family: inherit; font-size: 13px; line-height: 1.5; tab-size: 4; }
code {
white-space: pre-wrap; /* 自动换行,防止横向滚动条过长 */
word-break: break-all;
}
/* 选中文字颜色 */
::selection { background: #214283; }
`;
// --- B. 定义脚本 (搜索逻辑 + 快捷键) ---
const jsScript = `
const input = document.getElementById('search-input');
const codeContainer = document.getElementById('code-container');
// 自动聚焦输入框
// input.focus();
// 监听 Ctrl+F
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && (e.key === 'f' || e.key === 'F')) {
e.preventDefault();
input.focus();
input.select();
}
});
// 回车搜索
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
doFind(!e.shiftKey); // Shift+Enter 反向搜索
}
});
function doFind(next) {
const val = input.value;
if (!val) return;
// 使用原生查找 API
// find(text, caseSensitive, backwards, wrapAround, wholeWord, searchInFrames, showDialog)
const found = window.find(val, false, !next, true, false, true, false);
// 如果没找到,尝试重置光标位置再搜一次
if (!found) {
window.getSelection().removeAllRanges();
window.find(val, false, !next, true, false, true, false);
}
}
`;
// --- C. 组装完整 HTML ---
// 注意tab.raw 需要转义,防止 HTML 注入破坏格式
const safeCode = escapeHtml(tab.raw);
const safeName = escapeHtml(tab.name || 'View');
const htmlContent = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>${safeName}</title>
<style>${cssStyle}</style>
</head>
<body>
<div id="toolbar">
<span class="file-info">${safeName}</span>
<div id="search-box">
<span style="color:#777;font-size:12px;margin-right:4px">🔍</span>
<input type="text" id="search-input" placeholder="Ctrl+F 查找..." autocomplete="off">
<button class="btn" onclick="doFind(true)" title="下一个 (Enter)">↓</button>
<button class="btn" onclick="doFind(false)" title="上一个 (Shift+Enter)">↑</button>
</div>
</div>
<div id="code-container">
<pre><code>${safeCode}</code></pre>
</div>
<script>${jsScript}<\/script>
</body>
</html>
`;
// --- D. 编码为 Data URL ---
// 使用 UTF-8 安全的方式编码
const base64Content = btoa(unescape(encodeURIComponent(htmlContent)));
dataUrl = `data:text/html;charset=utf-8;base64,${base64Content}`;
}
// 3. 打开窗口
window.open(dataUrl, '_blank');
};
actions.appendChild(downloadBtn);
actions.appendChild(openBtn);
// toggle view for json/toml: visual/raw
if (tab.type === 'json' || tab.type === 'toml') {
const toggleBtn = document.createElement('button');
toggleBtn.title = '切换视图';
toggleBtn.innerHTML = '<i class="ri-swap-line"></i> 切换视图';
toggleBtn.onclick = () => {
tab.visualMode = (tab.visualMode === 'visual' ? 'raw' : 'visual');
// re-render
if (tab.type === 'json') renderJsonVisual(tab, md);
else renderTomlVisual(tab, md);
};
actions.appendChild(toggleBtn);
}
toolbar.appendChild(left); toolbar.appendChild(actions);
md.appendChild(toolbar);
// 建立主体展示区
if (tab.type === 'image' || tab.type === 'video' || tab.type === 'audio') {
const dataUrl = `data:${tab.mime};base64,${tab.content}`;
if (tab.type === 'image') {
const img = document.createElement('img');
img.className = 'media-image';
img.src = dataUrl;
img.alt = tab.name;
let fit = true;
const fitBtn = actions.querySelector('.ri-aspect-ratio-line') ? null : null;
// 鼠标滚轮放大缩小
let scale = 1;
img.style.transformOrigin = 'center center';
img.addEventListener('wheel', (e) => {
e.preventDefault();
const delta = e.deltaY > 0 ? -0.1 : 0.1;
scale = Math.max(0.1, Math.min(5, scale + delta));
img.style.transform = `scale(${scale})`;
});
img.addEventListener('dblclick', () => { scale = 1; img.style.transform = 'scale(1)'; img.style.objectFit='contain'; });
md.appendChild(img);
} else if (tab.type === 'video') {
const video = document.createElement('video');
video.controls = true;
video.src = dataUrl;
video.style.maxWidth = '100%';
video.style.maxHeight = '70vh';
video.style.borderRadius = '6px';
md.appendChild(video);
} else if (tab.type === 'audio') {
const audioWrap = document.createElement('div');
audioWrap.style.width = '100%';
audioWrap.style.maxWidth = '900px';
audioWrap.style.display = 'flex';
audioWrap.style.flexDirection = 'column';
audioWrap.style.alignItems = 'stretch';
audioWrap.style.gap = '12px';
const audio = document.createElement('audio');
audio.controls = true;
audio.src = dataUrl;
audio.style.width = '100%';
audioWrap.appendChild(audio);
md.appendChild(audioWrap);
}
} else if (tab.type === 'toml') {
// render TOML visual by parsing content
renderTomlVisual(tab, md);
} else if (tab.type === 'json') {
// render JSON visual or raw
renderJsonVisual(tab, md);
}
} else {
// normal code display in monaco editor
document.getElementById('media-overlay').style.display = 'none';
ed.style.display = 'flex';
if(tab.model && editor) {
editor.setModel(tab.model);
if (tab.viewState) editor.restoreViewState(tab.viewState);
editor.focus();
} else {
// fallback to raw text in editor
try {
const model = monaco.editor.createModel(tab.raw || '', 'plaintext');
editor.setModel(model);
} catch(e){}
}
}
this.renderTabs();
},
close(path) {
const idx = this.tabs.findIndex(t => t.path === path);
if (idx === -1) return;
if (this.tabs[idx].model) this.tabs[idx].model.dispose();
this.tabs.splice(idx, 1);
if (this.activePath === path) {
if (this.tabs.length > 0) this.activate(this.tabs[Math.min(idx, this.tabs.length - 1)].path);
else {
this.activePath = null;
if (editor) editor.setModel(null);
document.getElementById('media-overlay').style.display = 'none';
document.getElementById('monaco-container').style.display = 'flex';
const empty = document.querySelector('#monaco-container .empty-state');
if (empty) empty.style.display = 'flex';
}
}
this.renderTabs();
},
renderTabs() {
const c = document.getElementById('tabs-bar'); c.innerHTML = '';
this.tabs.forEach((t, i) => {
const el = document.createElement('div');
el.className = `tab ${t.path === this.activePath ? 'active' : ''}`;
el.style.animationDelay = (i * 28) + 'ms';
el.innerHTML = `<span class="tab-title">${t.name}</span><span class="tab-close" title="关闭"><i class="ri-close-line"></i></span>`;
el.onclick = () => this.activate(t.path);
el.querySelector('.tab-close').onclick = (e) => { e.stopPropagation(); this.close(t.path); };
c.appendChild(el);
});
}
};
// --- Java Bridge (保留 mock 支持) ---
function callJava(type, payload, onSuccess) {
if (window.cefQuery) {
window.cefQuery({
request: JSON.stringify({ type, ...payload }),
onSuccess: (resp) => onSuccess(JSON.parse(resp)),
onFailure: (c, m) => alert("Java Error: " + m)
});
} else {
console.log("Mock Call:", type, payload);
// 模拟数据供预览(示例)
if(type === 'openJar') onSuccess({ jarName: "mock.jar", files: [{path:"com/test/Main.java", name:"Main.java"}, {path:"com/test/EntityManagementSystem$EmptyEntityGette2425EntityManagementSystem.class2627", name:"EntityManagementSystem$EmptyEntityGette2425EntityManagementSystem.class2627"}, {path:"com/test/EntityManagementSystem.class", name:"EntityManagementSystem.class"}, {path:"res/icon.png", name:"icon.png"}, {path:"DiexvSword.mixins.json", name:"DiexvSword.mixins.json"}, {path:"assets/sound/bgm.mp3", name:"bgm.mp3"}, {path:"config/settings.toml", name:"settings.toml"}, {path:"data/sample.json", name:"sample.json"}] });
if(type === 'getFile') {
if(payload.path && payload.path.endsWith('.png')) onSuccess({ content: MOCK_IMG_BASE64(), type: "binary", mime: "image/png" });
else if(payload.path && payload.path.endsWith('.mp3')) onSuccess({ content: MOCK_AUDIO_BASE64(), type: "binary", mime: "audio/mpeg" });
else if(payload.path && payload.path.endsWith('.toml')) onSuccess({ content:
`# sample toml
title = "TOML 示例"
isActive = true
threshold = 3.14
[owner]
name = "Jane Doe"
dob = 1978
[database]
server = "192.168.1.1"
ports = [ 8001, 8001, 8002 ]
enabled = true
[servers.alpha]
ip = "10.0.0.1"
dc = "eqdc10"
`, type: "text", mime: "text/x-toml" });
else if(payload.path && payload.path.endsWith('.json')) onSuccess({ content: JSON.stringify({ name: "example", version: 1, nested: { a: [1,2,3], enabled: true }, list: [{ id:1 }, { id:2 }] }, null, 2), type: "text", mime: "application/json" });
else onSuccess({ content: "public class Main { void test() { Util.run(); } }", type: "java", mime: "text/x-java" });
}
if(type === 'findDefinition') onSuccess({ definitions: [{ path: "com/test/Util.java", line: 5 }] });
if(type === 'searchContent') onSuccess({ results: [{ path: "com/test/Main.java", line: 1, text: "void test() {"}] });
if(type === 'prepareDrag') onSuccess({ url: "file:///tmp/export/Main.java" });
}
}
// Mock base64 helper (只用于 mock 预览,真实环境由 Java 返回)
function MOCK_IMG_BASE64() { return ""; }
function MOCK_AUDIO_BASE64() { return ""; }
// --- 初始化 Monaco 编辑器 ---
require(['vs/editor/editor.main'], function() {
monaco.editor.defineTheme('jd-pro-darcula', {
base: 'vs-dark',
inherit: true,
rules: [
{ token: '', background: '2b2b2b', foreground: 'e6e6e6' },
{ token: 'keyword', foreground: 'cc7832' },
{ token: 'string', foreground: '6a8759' },
{ token: 'comment', foreground: '808080' },
],
colors: {
'editor.background': '#2b2b2b',
'editorLineNumber.foreground': '#5c6166',
'editorLineNumber.activeForeground': '#9ba1a6',
'editor.foreground': '#e6e6e6',
'editorCursor.foreground': '#528bff',
'editor.selectionBackground': '#21406b',
'editor.inactiveSelectionBackground': '#1b2633',
'editorIndentGuide.background': '#3b3b3b',
'editorIndentGuide.activeBackground': '#515151',
'editorLineHighlightBackground': '#313335',
'editorSuggestWidget.background': '#2d2f30',
'editorSuggestWidget.border': '#3c3f41'
}
});
editor = monaco.editor.create(document.getElementById('monaco-container'), {
value: "", language: 'java', theme: 'jd-pro-darcula',
automaticLayout: true, readOnly: true, fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
minimap: { enabled: true, renderCharacters: false }, glyphMargin: true, lineNumbers: 'on',
fontSize: 13, smoothScrolling: true
});
// 跳转到定义 (F12)
editor.addAction({
id: 'goto-definition-custom',
label: '跳转到定义 (Go to Definition)',
contextMenuGroupId: 'navigation',
contextMenuOrder: 1.0,
keybindings: [ monaco.KeyCode.F12 ],
run: function(ed) {
const model = ed.getModel();
const pos = ed.getPosition();
const word = model.getWordAtPosition(pos);
if (!word) { alert("请选中标识符"); return; }
callJava('findDefinition', { word: word.word }, (data) => {
const defs = data.definitions || [];
if (defs.length === 0) { alert("未找到定义"); return; }
if (defs.length === 1) loadFile(defs[0].path, defs[0].line);
else showGenericModal("选择定义", word.word, defs);
});
}
});
// 查找引用 (Alt+F7)
editor.addAction({
id: 'find-usages',
label: '查找所有引用 (Find Usages)',
contextMenuGroupId: 'navigation',
contextMenuOrder: 1.5,
keybindings: [ monaco.KeyMod.Alt | monaco.KeyCode.F7 ],
run: function(ed) {
const model = ed.getModel();
const pos = ed.getPosition();
const word = model.getWordAtPosition(pos);
if (word) performGlobalUsageSearch(word.word);
else alert("请选中标识符");
}
});
});
// --- 业务:打开 JAR / 构建树(实现文件夹优先 & 内部类合并) ---
function requestOpenJar() { callJava('openJar', {}, handleJarOpened); }
function handleJarOpened(data) {
const files = data.files || [];
const canonicalMap = {};
files.forEach(f => {
const key = canonicalKeyForPath(f.path);
if (!canonicalMap[key]) {
canonicalMap[key] = Object.assign({}, f);
} else {
const existingLeaf = canonicalMap[key].path.split('/').pop();
const newLeaf = f.path.split('/').pop();
if (newLeaf.indexOf('$') === -1 && existingLeaf.indexOf('$') !== -1) {
canonicalMap[key] = Object.assign({}, f);
}
}
});
allFilesList = Object.values(canonicalMap).map(f => {
const parts = f.path.split('/');
const leaf = parts[parts.length - 1];
f.name = sanitizeFileName(leaf);
return f;
});
buildFileTree(allFilesList);
TabManager.tabs = []; TabManager.renderTabs();
document.getElementById('sidebar-title').innerHTML = `<i class="ri-archive-line"></i> ${data.jarName || 'Archive'}`;
}
function buildFileTree(files) {
const root = {};
files.forEach(f => {
const partsRaw = f.path.split('/');
const dirParts = partsRaw.slice(0, -1);
const sanitizedLeaf = sanitizeFileName(partsRaw[partsRaw.length - 1]);
const parts = dirParts.concat([sanitizedLeaf]);
let curr = root;
parts.forEach((p, i) => {
if (!curr[p]) curr[p] = (i === parts.length - 1) ? { __file: f } : {};
curr = curr[p];
});
});
const container = document.getElementById('file-tree-view');
container.innerHTML = '';
renderTreeRecursive(root, container, 0);
}
// 树节点渲染folders first
function renderTreeRecursive(node, container, level) {
Object.keys(node).sort((a,b) => {
const aIsFile = !!node[a].__file;
const bIsFile = !!node[b].__file;
if (aIsFile !== bIsFile) return aIsFile ? 1 : -1;
return a.localeCompare(b, undefined, { sensitivity: 'base' });
}).forEach(key => {
if (key === '__file') return;
const item = node[key];
const isFile = !!item.__file;
const div = document.createElement('div');
if (isFile) {
div.className = 'tree-node';
div.style.paddingLeft = (level * 16 + 12) + 'px';
const leafName = item.__file.name || item.__file.path.split('/').pop();
let icon = 'ri-file-line';
const lower = leafName.toLowerCase();
if(lower.endsWith('.java') || lower.endsWith('.class')) icon = 'ri-java-line';
if(lower.endsWith('.png') || lower.endsWith('.jpg') || lower.endsWith('.jpeg') || lower.endsWith('.gif')) icon = 'ri-image-line';
if(lower.endsWith('.mp3') || lower.endsWith('.wav')) icon = 'ri-music-line';
if(lower.endsWith('.json')) icon = 'ri-file-json-line';
if(lower.endsWith('.toml')) icon = 'ri-file-list-3-line';
div.innerHTML = `<i class="${icon}"></i> ${leafName}`;
// 拖拽导出逻辑
div.draggable = true;
div.onmousedown = () => {
callJava('prepareDrag', { path: item.__file.path }, (res) => {
div.dataset.downloadUrl = `application/octet-stream:${leafName}:${res.url}`;
});
};
div.ondragstart = (e) => {
if (div.dataset.downloadUrl) {
try {
e.dataTransfer.setData("DownloadURL", div.dataset.downloadUrl);
e.dataTransfer.setData("text/plain", div.dataset.downloadUrl);
} catch (err) {}
}
};
div.onclick = () => {
document.querySelectorAll('.tree-node').forEach(n => n.classList.remove('selected'));
div.classList.add('selected');
loadFile(item.__file.path);
};
container.appendChild(div);
} else {
const folderDiv = document.createElement('div');
folderDiv.className = 'folder-node';
folderDiv.style.paddingLeft = (level * 16 + 8) + 'px';
folderDiv.innerHTML = `<span class="arrow"><i class="ri-arrow-right-s-line"></i></span> <i class="ri-folder-3-fill"></i> ${key}`;
const contentDiv = document.createElement('div');
contentDiv.className = 'folder-content';
folderDiv.onclick = (e) => {
contentDiv.classList.toggle('open');
folderDiv.classList.toggle('open');
};
container.appendChild(folderDiv);
container.appendChild(contentDiv);
renderTreeRecursive(item, contentDiv, level + 1);
}
});
}
// --- 加载文件(根据 mime 自适应媒体 / json / toml ---
function loadFile(path, line) {
document.getElementById('loading').style.display = 'flex';
callJava('getFile', { path }, (data) => {
document.getElementById('loading').style.display = 'none';
let mime = data.mime || '';
let type = data.type || 'text';
// determine by extension
const ext = (path.split('.').pop() || '').toLowerCase();
if (mime.startsWith('image/') || ext === 'png' || ext === 'jpg' || ext === 'jpeg' || ext === 'gif') type = 'image';
else if (mime.startsWith('video/') || ext === 'mp4' || ext === 'webm') type = 'video';
else if (mime.startsWith('audio/') || ext === 'mp3' || ext === 'wav') type = 'audio';
else if (ext === 'json' || mime.includes('json')) type = 'json';
else if (ext === 'toml' || mime.includes('toml')) type = 'toml';
else type = (mime.includes('java') || ext === 'java' || ext === 'class') ? 'java' : 'text';
// pass raw content when textual, binary content stays base64 in content
TabManager.open(path, data.content, type, mime);
if (line && editor && type === 'java') {
setTimeout(() => {
editor.revealLineInCenter(line);
editor.setPosition({ lineNumber: line, column: 1 });
editor.focus();
const decorations = editor.deltaDecorations([], [{ range: new monaco.Range(line, 1, line, 1), options: { isWholeLine: true, className: 'editor-line-highlight' } }]);
setTimeout(() => editor.deltaDecorations(decorations, []), 1400);
}, 80);
}
});
}
// --- 搜索功能 (下拉框版) ---
function performSearch() {
clearTimeout(searchDebounceTimer);
const query = document.getElementById('global-search').value.trim();
const mode = document.getElementById('search-mode').value;
const dropdown = document.getElementById('search-dropdown');
if (!query) { dropdown.classList.remove('show'); dropdown.innerHTML = ''; return; }
searchDebounceTimer = setTimeout(() => {
dropdown.classList.add('show');
dropdown.setAttribute('aria-hidden', 'false');
dropdown.innerHTML = '<div style="padding:12px;text-align:center;color:var(--text-sub)"><i class="ri-loader-4-line ri-spin"></i> 搜索中...</div>';
if (mode === 'file') {
const matches = allFilesList.filter(f => (f.name||f.path).toLowerCase().includes(query.toLowerCase()));
renderDropdownResults(matches.map(f => ({ path: f.path, line: 0, text: null })));
} else {
callJava('searchContent', { query: query, mode: mode }, (data) => renderDropdownResults(data.results));
}
}, 220);
}
function renderDropdownResults(results) {
const dropdown = document.getElementById('search-dropdown');
dropdown.innerHTML = '';
if (!results || results.length === 0) {
dropdown.innerHTML = '<div style="padding:14px;color:var(--text-sub);text-align:center">无匹配结果</div>';
return;
}
results.slice(0, 80).forEach(res => {
const el = document.createElement('div');
el.className = 'search-result-item';
const fileName = res.path.split('/').pop();
const preview = res.text ? `<div class="res-code"><span style="color:#ffc66d">${res.line}:</span> ${escapeHtml(res.text)}</div>` : `<div class="res-code" style="opacity:0.6">文件名匹配</div>`;
el.innerHTML = `<div class="res-file"><span><i class="ri-file-text-line" style="margin-right:6px;color:var(--accent-color)"></i>${fileName}</span><span class="res-path">${res.path}</span></div>${preview}`;
el.onclick = () => { loadFile(res.path, res.line); dropdown.classList.remove('show'); dropdown.setAttribute('aria-hidden', 'true'); };
dropdown.appendChild(el);
});
}
function performGlobalUsageSearch(keyword) {
callJava('searchContent', { query: keyword }, (data) => {
const results = data.results || [];
if (results.length === 0) { alert(`未找到 "${keyword}" 的引用。`); return; }
showGenericModal("查找引用", keyword, results);
});
}
function showGenericModal(title, keyword, results) {
const modal = document.getElementById('generic-modal');
const list = document.getElementById('modal-list');
document.getElementById('modal-title').innerHTML = `<i class="ri-search-eye-line" style="margin-right:8px;color:var(--accent-color)"></i> ${title}: "${keyword}" (${results.length})`;
list.innerHTML = '';
results.forEach(res => {
const item = document.createElement('div');
item.className = 'usage-item';
const safeText = escapeHtml(res.text || '');
const re = new RegExp(escapeRegExp(keyword), 'gi');
const highlighted = safeText.replace(re, (m) => `<span class="usage-match">${m}</span>`);
item.innerHTML = `
<div class="usage-path"><span>${res.path.split('/').pop()}</span> <span>Line ${res.line}</span></div>
<div class="usage-preview">${highlighted}</div>
`;
item.onclick = () => { loadFile(res.path, res.line); closeModal(); };
list.appendChild(item);
});
modal.style.display = 'flex';
modal.setAttribute('aria-hidden', 'false');
modal.querySelector('.modal').style.transform = 'translateY(-8px)';
setTimeout(()=> modal.querySelector('.modal').style.transform = 'translateY(0)', 10);
}
// --- JSON / TOML 可视化渲染器 ---
// 简易 TOML 解析器(适配常用键值与表结构,非完全规范)
function simpleTomlParse(text) {
const lines = text.split(/\r?\n/);
const root = {};
let current = root;
let currentPath = [];
function setAtPath(path, key, value) {
let obj = root;
path.forEach(p => {
if (!obj[p]) obj[p] = {};
obj = obj[p];
});
obj[key] = value;
}
for (let rawLine of lines) {
let line = rawLine.trim();
if (!line || line.startsWith('#')) continue;
// table
if (/^\[.+\]$/.test(line)) {
const inner = line.replace(/^\[|\]$/g, '').trim();
currentPath = inner.split('.').map(s=>s.trim()).filter(Boolean);
continue;
}
// key = value
const m = line.match(/^([\w\-\.\"]+)\s*=\s*(.+)$/);
if (m) {
let key = m[1].replace(/^"(.*)"$/, '$1');
let val = m[2].trim();
// remove trailing inline comment
const commentIdx = val.indexOf('#');
if (commentIdx !== -1) val = val.slice(0, commentIdx).trim();
// parse value types: string, boolean, number, array
if (/^".*"$/.test(val) || /^'.*'$/.test(val)) {
val = val.replace(/^"(.*)"$/, '$1').replace(/^'(.*)'$/, '$1');
} else if (/^\[.*\]$/.test(val)) {
try {
const inner = val.slice(1, -1).trim();
if (inner === '') val = [];
else {
val = inner.split(',').map(v => {
v = v.trim();
if (/^".*"$/.test(v) || /^'.*'$/.test(v)) return v.replace(/^"(.*)"$/, '$1').replace(/^'(.*)'$/, '$1');
if (/^\d+$/.test(v)) return parseInt(v,10);
if (/^\d*\.\d+$/.test(v)) return parseFloat(v);
if (/^(true|false)$/i.test(v)) return v.toLowerCase() === 'true';
return v;
});
}
} catch(e) { val = val; }
} else if (/^(true|false)$/i.test(val)) {
val = val.toLowerCase() === 'true';
} else if (/^[+-]?\d+$/.test(val)) {
val = parseInt(val, 10);
} else if (/^[+-]?\d*\.\d+$/.test(val)) {
val = parseFloat(val);
} else {
// fallback to raw string without quotes
val = val.replace(/^"(.*)"$/, '$1').replace(/^'(.*)'$/, '$1');
}
setAtPath(currentPath, key, val);
}
}
return root;
}
// render arbitrary object as collapsible kv tree
function renderObjectAsTree(obj, container, keyName) {
container.innerHTML = '';
const rootNode = document.createElement('div');
rootNode.className = 'kv-visual';
const toolbarLine = document.createElement('div');
toolbarLine.className = 'kv-toolbar';
toolbarLine.innerHTML = `<div class="kv-badge">${keyName || 'root'}</div><div style="flex:1"></div>`;
rootNode.appendChild(toolbarLine);
const tree = document.createElement('div');
tree.className = 'kv-tree';
rootNode.appendChild(tree);
buildKVNodes(obj, tree, 0);
container.appendChild(rootNode);
}
function buildKVNodes(obj, parentEl, level) {
Object.keys(obj).forEach(k => {
const v = obj[k];
const node = document.createElement('div');
node.className = 'kv-node';
const keyEl = document.createElement('div');
keyEl.className = 'kv-key';
keyEl.textContent = k;
const valEl = document.createElement('div');
valEl.className = 'kv-value';
if (v === null) valEl.textContent = 'null';
else if (Array.isArray(v)) {
valEl.textContent = JSON.stringify(v);
const badge = document.createElement('span'); badge.className = 'kv-badge'; badge.textContent = `array(${v.length})`;
keyEl.appendChild(document.createTextNode(' ')); keyEl.appendChild(badge);
} else if (typeof v === 'object') {
const toggle = document.createElement('span');
toggle.className = 'kv-toggle';
toggle.innerHTML = `<i class="ri-arrow-right-s-line"></i> <span style="font-weight:700">${typeof v === 'object' ? '展开' : ''}</span>`;
keyEl.appendChild(document.createTextNode(' '));
keyEl.appendChild(toggle);
valEl.textContent = '{...}';
node.appendChild(keyEl);
node.appendChild(valEl);
const children = document.createElement('div');
children.className = 'kv-children collapsed';
node.appendChild(children);
toggle.onclick = () => {
const icon = toggle.querySelector('i');
const expanded = children.classList.toggle('expanded');
if (expanded) {
children.classList.remove('collapsed'); children.classList.add('expanded');
icon.style.transform = 'rotate(90deg)';
// lazy render
if (!children.dataset.rendered) {
buildKVNodes(v, children, level+1);
children.dataset.rendered = '1';
}
} else {
children.classList.remove('expanded'); children.classList.add('collapsed');
icon.style.transform = 'rotate(0deg)';
}
};
parentEl.appendChild(node);
return;
} else {
valEl.textContent = String(v);
}
node.appendChild(keyEl);
node.appendChild(valEl);
parentEl.appendChild(node);
});
}
function renderTomlVisual(tab, host) {
const container = document.createElement('div');
container.style.width = '100%';
container.style.display = 'flex';
container.style.justifyContent = 'center';
container.style.padding = '12px 6px';
host.appendChild(container);
let parsed = {};
try {
parsed = simpleTomlParse(tab.raw || tab.content || '');
} catch(e) {
container.innerHTML = `<div class="kv-visual"><div style="padding:12px;color:var(--text-sub)">无法解析 TOML${escapeHtml(e.message || '')}</div></div>`;
return;
}
renderObjectAsTree(parsed, container, tab.name);
}
function renderJsonVisual(tab, host) {
const container = document.createElement('div');
container.style.width = '100%';
container.style.display = 'flex';
container.style.justifyContent = 'center';
container.style.padding = '12px 6px';
host.appendChild(container);
// If visual mode
if (tab.visualMode === 'visual') {
let parsed;
try {
parsed = JSON.parse(tab.raw || tab.content || '{}');
} catch(e) {
container.innerHTML = `<div class="kv-visual"><div style="padding:12px;color:var(--text-sub)">无法解析 JSON${escapeHtml(e.message || '')}</div></div>`;
return;
}
renderObjectAsTree(parsed, container, tab.name);
} else {
// raw mode: show monaco editor (readonly)
const area = document.createElement('div');
area.style.width = '100%';
area.style.maxWidth = '1100px';
area.style.height = '60vh';
area.style.borderRadius = '8px';
area.style.overflow = 'hidden';
area.style.border = '1px solid rgba(255,255,255,0.03)';
host.appendChild(area);
// create monaco editor instance inside
try {
const model = monaco.editor.createModel(tab.raw || tab.content || '', 'json');
const mini = monaco.editor.create(area, {
model: model, theme: 'jd-pro-darcula', readOnly: true, automaticLayout: true,
minimap: { enabled: true, renderCharacters: false }, lineNumbers: 'on', fontSize: 13
});
} catch(e) {
area.innerHTML = `<pre style="padding:12px;color:var(--text-sub)">${escapeHtml(tab.raw || '')}</pre>`;
}
}
}
// --- 辅助功能 ---
function navigateResults(e) { if(e.key === 'Enter') performSearch(); }
document.addEventListener('click', (e) => {
if (!document.querySelector('.search-container').contains(e.target)) {
const dd = document.getElementById('search-dropdown');
if (dd) { dd.classList.remove('show'); dd.setAttribute('aria-hidden','true'); }
}
});
function closeModal() { document.getElementById('generic-modal').style.display = 'none'; document.getElementById('generic-modal').setAttribute('aria-hidden','true'); }
document.getElementById('generic-modal').onclick = (e) => { if(e.target.id === 'generic-modal') closeModal(); };
function escapeHtml(text) { return text ? text.toString().replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;") : ""; }
function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
// --- 侧边栏可拉宽resizer ---
(function setupResizer() {
const resizer = document.getElementById('sidebar-resizer');
const sidebar = document.getElementById('sidebar');
const editorArea = document.getElementById('editor-area');
let dragging = false;
let startX = 0, startWidth = 0;
resizer.addEventListener('mousedown', (e) => {
dragging = true; startX = e.clientX; startWidth = sidebar.getBoundingClientRect().width;
document.body.style.userSelect = 'none';
resizer.classList.add('dragging');
});
window.addEventListener('mousemove', (e) => {
if (!dragging) return;
const dx = e.clientX - startX;
let newW = Math.round(startWidth + dx);
if (newW < MIN_SIDEBAR) newW = MIN_SIDEBAR;
if (newW > MAX_SIDEBAR) newW = MAX_SIDEBAR;
sidebar.style.width = newW + 'px';
});
window.addEventListener('mouseup', () => {
if (!dragging) return;
dragging = false;
document.body.style.userSelect = '';
resizer.classList.remove('dragging');
try { localStorage.setItem('jdpro.sidebar.width', sidebar.style.width); } catch(e){}
});
try {
const w = localStorage.getItem('jdpro.sidebar.width');
if (w) sidebar.style.width = w;
} catch(e){}
})();
// --- 外部文件拖入 ---
document.body.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); document.body.style.boxShadow = "inset 0 0 0 2px rgba(82,139,255,0.12)"; });
document.body.addEventListener('dragleave', (e) => { e.preventDefault(); e.stopPropagation(); document.body.style.boxShadow = "none"; });
document.body.addEventListener('drop', (e) => {
e.preventDefault(); e.stopPropagation(); document.body.style.boxShadow = "none";
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
const file = e.dataTransfer.files[0];
if (file.path && /\.(jar|zip|war|class)$/i.test(file.path)) {
document.getElementById('loading').style.display = 'flex';
callJava('openJar', { path: file.path }, handleJarOpened);
}
}
});
</script>
</body>
</html>