simplify extension ui

This commit is contained in:
2026-05-28 00:59:37 +08:00
parent 3f86aeaf0c
commit 8e8d3d6da0
10 changed files with 1416 additions and 536 deletions

View File

@@ -1,12 +1,12 @@
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import { singletonPanel } from './shared';
import { getToolWebviewStyles, singletonPanel } from './shared';
const store: { panel: vscode.WebviewPanel | null } = { panel: null };
export function openBase64Scanner(_context: vscode.ExtensionContext) {
const { panel, isNew } = singletonPanel(store, 'hplToolbox.base64Scanner', 'Base64 Asset Scanner');
const { panel, isNew } = singletonPanel(store, 'hplToolbox.base64Scanner', 'Base64 Scanner');
if (!isNew) return;
panel.webview.html = getHtml();
@@ -16,7 +16,8 @@ export function openBase64Scanner(_context: vscode.ExtensionContext) {
if (ws) {
const distPath = path.join(ws.uri.fsPath, 'dist');
if (fs.existsSync(distPath) && fs.statSync(distPath).isDirectory()) {
panel.webview.postMessage({ type: 'folderPicked', path: distPath });
const files = await collectHtmlFiles(distPath);
panel.webview.postMessage({ type: 'filesPicked', paths: files });
}
}
} else if (msg.type === 'pickFolder') {
@@ -27,7 +28,8 @@ export function openBase64Scanner(_context: vscode.ExtensionContext) {
openLabel: 'Select Folder',
});
if (picked && picked[0]) {
panel.webview.postMessage({ type: 'folderPicked', path: picked[0].fsPath });
const files = await collectHtmlFiles(picked[0].fsPath);
panel.webview.postMessage({ type: 'filesPicked', paths: files });
}
} else if (msg.type === 'pickFiles') {
const picked = await vscode.window.showOpenDialog({
@@ -144,46 +146,125 @@ function getHtml(): string {
<head>
<meta charset="UTF-8" />
<style>
body { font-family: var(--vscode-font-family); padding: 12px; color: var(--vscode-foreground); }
.row { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; }
input[type=text] { flex: 1; padding: 4px 6px; background: var(--vscode-input-background); color: var(--vscode-input-foreground); border: 1px solid var(--vscode-input-border); }
button { padding: 4px 12px; background: var(--vscode-button-background); color: var(--vscode-button-foreground); border: none; cursor: pointer; }
button:hover { background: var(--vscode-button-hoverBackground); }
.row-result { display: flex; gap: 8px; padding: 2px 0; font-family: var(--vscode-editor-font-family); font-size: 12px; }
.mark { width: 14px; flex-shrink: 0; font-weight: bold; }
.ok { color: #3fb950; }
.bad { color: #f85149; }
.file-name { word-break: break-all; }
.assets { color: var(--vscode-descriptionForeground); margin-left: 6px; word-break: break-all; }
.summary { margin-top: 12px; opacity: 0.8; }
${getToolWebviewStyles()}
.asset-list { margin: 0; padding-left: 16px; }
.asset-list li + li { margin-top: 3px; }
.selected-list { margin-top: 8px; }
.remove-selected {
min-width: 28px;
width: 28px;
padding: 3px 0;
color: var(--vscode-errorForeground);
font-size: 16px;
line-height: 1;
}
.pager { margin-top: 8px; display: flex; align-items: center; gap: 8px; justify-content: flex-end; }
</style>
</head>
<body>
<h2>Base64 Asset Scanner</h2>
<div class="row">
<input id="folder" type="text" placeholder="Folder to scan recursively..." />
<button id="pick">Browse Folder...</button>
<button id="pickFiles">Pick Files...</button>
<button id="scan">Scan</button>
<main class="tool-page">
<header class="tool-header">
<h2 class="tool-title">Base64 Scanner</h2>
<p class="tool-description">Scan folders or selected HTML files for asset references that are not embedded as base64 data URIs.</p>
</header>
<section class="tool-panel input-panel">
<div class="panel-header">
<h3 class="panel-title">Inputs</h3>
</div>
<div class="panel-body">
<div class="control-group">
<div class="field-row">
<input id="folder" type="hidden" />
<button id="pick" class="secondary">Select Folder</button>
<button id="pickFiles" class="secondary">Select File(s)</button>
<button id="clear" class="secondary">Clear</button>
</div>
<div id="selection" class="selected-list"></div>
</div>
<div class="action-row">
<button id="scan">Scan</button>
</div>
<div id="status" class="status-panel"></div>
</div>
</section>
<div id="outputPanel" class="is-hidden">
<div id="results" class="results-panel"></div>
</div>
<div id="selection" class="summary"></div>
<div id="status"></div>
<div id="results"></div>
</main>
<script>
const vscode = acquireVsCodeApi();
vscode.postMessage({ type: 'ready' });
const folderEl = document.getElementById('folder');
const statusEl = document.getElementById('status');
const resultsEl = document.getElementById('results');
const outputPanel = document.getElementById('outputPanel');
const selectionEl = document.getElementById('selection');
const PAGE_SIZE = 5;
let pickedFolder = '';
let pickedFiles = [];
let selectedPage = 0;
function clearFiles() {
pickedFiles = [];
selectionEl.textContent = '';
function basename(p) {
const i = Math.max(p.lastIndexOf('/'), p.lastIndexOf('\\\\'));
return i >= 0 ? p.slice(i + 1) : p;
}
folderEl.addEventListener('input', clearFiles);
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
function renderSelection() {
const oldPager = document.getElementById('selectedPager');
if (oldPager) oldPager.remove();
const items = pickedFiles.length
? pickedFiles.map((p, i) => ({ label: basename(p), index: i, type: 'file' }))
: (pickedFolder ? [{ label: pickedFolder, index: 0, type: 'folder' }] : []);
if (!items.length) {
selectionEl.innerHTML = '<span class="file-name">(no files selected)</span>';
return;
}
const maxPage = Math.max(0, Math.ceil(items.length / PAGE_SIZE) - 1);
selectedPage = Math.min(selectedPage, maxPage);
const visibleItems = items.slice(selectedPage * PAGE_SIZE, selectedPage * PAGE_SIZE + PAGE_SIZE);
selectionEl.innerHTML =
'<div class="results-panel" style="margin-top:0;">' +
'<table class="data-table">' +
'<thead><tr><th>Filename</th><th style="width:44px;"></th></tr></thead>' +
'<tbody>' +
visibleItems.map(item =>
'<tr><td class="mono wrap">' + escapeHtml(item.label) + '</td>' +
'<td><button class="remove-selected danger" data-index="' + item.index + '" data-type="' + item.type + '" title="Remove">×</button></td></tr>'
).join('') +
'</tbody></table>' +
'</div>';
selectionEl.querySelectorAll('.remove-selected').forEach(btn => {
btn.addEventListener('click', () => {
if (btn.dataset.type === 'folder') {
pickedFolder = '';
folderEl.value = '';
} else {
pickedFiles.splice(Number(btn.dataset.index), 1);
}
renderSelection();
});
});
if (items.length > PAGE_SIZE) {
const pager = document.createElement('div');
pager.id = 'selectedPager';
pager.className = 'pager';
pager.innerHTML =
'<button class="secondary" id="prevPage"' + (selectedPage === 0 ? ' disabled' : '') + '>Previous</button>' +
'<span class="file-name">Page ' + (selectedPage + 1) + ' of ' + (maxPage + 1) + '</span>' +
'<button class="secondary" id="nextPage"' + (selectedPage === maxPage ? ' disabled' : '') + '>Next</button>';
selectionEl.appendChild(pager);
pager.querySelector('#prevPage').addEventListener('click', () => { selectedPage--; renderSelection(); });
pager.querySelector('#nextPage').addEventListener('click', () => { selectedPage++; renderSelection(); });
}
}
renderSelection();
document.getElementById('pick').addEventListener('click', () => {
vscode.postMessage({ type: 'pickFolder' });
@@ -191,9 +272,20 @@ function getHtml(): string {
document.getElementById('pickFiles').addEventListener('click', () => {
vscode.postMessage({ type: 'pickFiles' });
});
document.getElementById('clear').addEventListener('click', () => {
folderEl.value = '';
pickedFiles = [];
pickedFolder = '';
selectedPage = 0;
renderSelection();
statusEl.textContent = '';
resultsEl.innerHTML = '';
outputPanel.classList.add('is-hidden');
});
document.getElementById('scan').addEventListener('click', () => {
statusEl.textContent = 'Scanning...';
resultsEl.innerHTML = '';
outputPanel.classList.add('is-hidden');
if (pickedFiles.length) {
vscode.postMessage({ type: 'scan', files: pickedFiles });
} else {
@@ -207,35 +299,63 @@ function getHtml(): string {
const msg = event.data;
if (msg.type === 'folderPicked') {
folderEl.value = msg.path;
clearFiles();
pickedFolder = msg.path;
pickedFiles = [];
selectedPage = 0;
renderSelection();
} else if (msg.type === 'filesPicked') {
pickedFiles = msg.paths;
const existing = new Set(pickedFiles);
pickedFiles = pickedFiles.concat((msg.paths || []).filter(p => !existing.has(p)));
folderEl.value = '';
selectionEl.textContent = pickedFiles.length + ' file(s) selected';
pickedFolder = '';
selectedPage = Math.max(0, Math.ceil(pickedFiles.length / PAGE_SIZE) - 1);
renderSelection();
} else if (msg.type === 'results') {
if (msg.error) { statusEl.textContent = 'Error: ' + msg.error; return; }
if (msg.error) {
statusEl.textContent = 'Error: ' + msg.error;
outputPanel.classList.add('is-hidden');
return;
}
const total = msg.results.length;
const flagged = msg.results.filter(r => !r.ok).length;
statusEl.innerHTML = '<div class="summary">Scanned ' + total + ' HTML file(s). ' + flagged + ' contain non-base64 assets.</div>';
resultsEl.innerHTML = '';
statusEl.textContent = 'Scanned ' + total + ' HTML file(s). ' + flagged + ' contain non-base64 assets.';
if (!total) {
resultsEl.innerHTML = '';
outputPanel.classList.add('is-hidden');
return;
}
outputPanel.classList.remove('is-hidden');
resultsEl.innerHTML =
'<table class="data-table">' +
'<thead><tr><th style="width:90px;">Status</th><th>File</th><th style="width:90px;">Issues</th><th>Non-base64 assets</th></tr></thead>' +
'<tbody></tbody>' +
'</table>';
const tbody = resultsEl.querySelector('tbody');
for (const r of msg.results) {
const row = document.createElement('div');
row.className = 'row-result';
const mark = document.createElement('span');
mark.className = 'mark ' + (r.ok ? 'ok' : 'bad');
mark.textContent = r.ok ? '✓' : '✗';
const name = document.createElement('span');
name.className = 'file-name';
const row = document.createElement('tr');
const status = document.createElement('td');
status.innerHTML = r.ok ? '<span class="badge ok">OK</span>' : '<span class="badge err">Review</span>';
const name = document.createElement('td');
name.className = 'mono wrap';
name.textContent = r.file;
row.appendChild(mark);
row.appendChild(name);
const count = document.createElement('td');
count.textContent = String(r.assets.length);
const assets = document.createElement('td');
assets.className = 'wrap';
if (!r.ok) {
const a = document.createElement('span');
a.className = 'assets';
a.textContent = '— ' + r.assets.join(', ');
row.appendChild(a);
const list = document.createElement('ul');
list.className = 'asset-list mono';
r.assets.forEach(asset => {
const item = document.createElement('li');
item.textContent = asset;
list.appendChild(item);
});
assets.appendChild(list);
} else {
assets.innerHTML = '<span class="muted">None</span>';
}
resultsEl.appendChild(row);
row.append(status, name, count, assets);
tbody.appendChild(row);
}
}
});