- 更新 shared_proto_db/metadata/000003.log 文件内容 - 更新 Site Characteristics Database/00003.log 文件内容 - 添加新的数据库条目和元数据记录 - 保持数据库文件格式的一致性 - 删除Vivid2D的内容 - 重写启动加载界面
1217 lines
64 KiB
HTML
1217 lines
64 KiB
HTML
<!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, "&").replace(/</g, "<").replace(/>/g, ">") : ""; }
|
||
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>
|