simplify extension ui
This commit is contained in:
@@ -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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user