This commit is contained in:
2026-06-08 22:31:02 +08:00
parent ad6403f3e4
commit 6c09beea43
7 changed files with 210 additions and 14 deletions

View File

@@ -3,6 +3,7 @@ import * as fs from 'fs';
import * as path from 'path';
const BETA_TOOLS_ENABLED_KEY = 'hplToolbox.betaTools.enabled';
const TOOL_ORDER_KEY = 'hplToolbox.toolOrder.v1';
interface ToolDefinition {
id?: string;
@@ -54,7 +55,8 @@ export class LauncherViewProvider implements vscode.WebviewViewProvider {
const version = this.context.extension.packageJSON.version as string;
const tools = loadToolDefinitions(this.context.extensionPath);
const betaToolsEnabled = this.context.globalState.get<boolean>(BETA_TOOLS_ENABLED_KEY, false);
view.webview.html = getHtml(version, betaToolsEnabled, tools);
const toolOrder = this.context.globalState.get<string[]>(TOOL_ORDER_KEY, []);
view.webview.html = getHtml(version, betaToolsEnabled, tools, toolOrder);
view.webview.onDidReceiveMessage(async (msg) => {
if (msg?.type === 'open' && typeof msg.command === 'string') {
vscode.commands.executeCommand(msg.command);
@@ -62,7 +64,9 @@ export class LauncherViewProvider implements vscode.WebviewViewProvider {
vscode.commands.executeCommand('hplToolbox.openChangelog');
} else if (msg?.type === 'setBetaToolsEnabled' && typeof msg.enabled === 'boolean') {
await this.context.globalState.update(BETA_TOOLS_ENABLED_KEY, msg.enabled);
view.webview.html = getHtml(version, msg.enabled, tools);
view.webview.html = getHtml(version, msg.enabled, tools, this.context.globalState.get<string[]>(TOOL_ORDER_KEY, []));
} else if (msg?.type === 'setToolOrder' && Array.isArray(msg.order)) {
await this.context.globalState.update(TOOL_ORDER_KEY, msg.order.filter((id: unknown) => typeof id === 'string'));
}
});
}
@@ -88,8 +92,8 @@ function loadToolDefinitions(extensionPath: string): ToolDefinition[] {
}
}
function getHtml(version: string, betaToolsEnabled: boolean, tools: ToolDefinition[]): string {
const visibleTools = sortToolsForDisplay(tools.filter(tool => betaToolsEnabled || !tool.beta));
function getHtml(version: string, betaToolsEnabled: boolean, tools: ToolDefinition[], toolOrder: string[]): string {
const visibleTools = sortToolsForDisplay(tools.filter(tool => betaToolsEnabled || !tool.beta), toolOrder);
const toolButtons = visibleTools.map(renderToolButton).join('\n');
return `<!DOCTYPE html>
<html>
@@ -127,6 +131,9 @@ function getHtml(version: string, betaToolsEnabled: boolean, tools: ToolDefiniti
cursor: pointer;
font: inherit;
}
.tool-btn.dragging {
opacity: 0.55;
}
.tool-btn:hover {
background: var(--vscode-list-hoverBackground, var(--vscode-button-secondaryHoverBackground));
border-color: var(--vscode-focusBorder, var(--vscode-panel-border));
@@ -217,9 +224,44 @@ ${toolButtons}
const vscode = acquireVsCodeApi();
document.querySelectorAll('.tool-btn').forEach((btn) => {
btn.addEventListener('click', () => {
if (btn.dataset.dragging === 'true') {
btn.dataset.dragging = 'false';
return;
}
vscode.postMessage({ type: 'open', command: btn.dataset.cmd });
});
});
const toolsEl = document.querySelector('.tools');
let dragged = null;
toolsEl.querySelectorAll('.tool-btn').forEach((btn) => {
btn.draggable = true;
btn.addEventListener('dragstart', (event) => {
dragged = btn;
btn.classList.add('dragging');
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', btn.dataset.id || '');
});
btn.addEventListener('dragend', () => {
btn.classList.remove('dragging');
btn.dataset.dragging = 'true';
dragged = null;
persistToolOrder();
setTimeout(() => { btn.dataset.dragging = 'false'; }, 0);
});
btn.addEventListener('dragover', (event) => {
event.preventDefault();
if (!dragged || dragged === btn) return;
const rect = btn.getBoundingClientRect();
const after = event.clientY > rect.top + rect.height / 2;
toolsEl.insertBefore(dragged, after ? btn.nextSibling : btn);
});
});
function persistToolOrder() {
vscode.postMessage({
type: 'setToolOrder',
order: Array.from(toolsEl.querySelectorAll('.tool-btn')).map(btn => btn.dataset.id).filter(Boolean),
});
}
document.getElementById('betaToggle').addEventListener('click', (event) => {
const enabled = event.currentTarget.dataset.enabled === 'true';
vscode.postMessage({ type: 'setBetaToolsEnabled', enabled: !enabled });
@@ -232,17 +274,29 @@ ${toolButtons}
</html>`;
}
function sortToolsForDisplay(tools: ToolDefinition[]): ToolDefinition[] {
return [...tools].sort((a, b) => Number(Boolean(a.beta)) - Number(Boolean(b.beta)));
function sortToolsForDisplay(tools: ToolDefinition[], toolOrder: string[]): ToolDefinition[] {
const order = new Map(toolOrder.map((id, index) => [id, index]));
return [...tools].sort((a, b) => {
const aId = toolId(a);
const bId = toolId(b);
const ai = order.has(aId) ? order.get(aId)! : Number.MAX_SAFE_INTEGER;
const bi = order.has(bId) ? order.get(bId)! : Number.MAX_SAFE_INTEGER;
if (ai !== bi) return ai - bi;
return Number(Boolean(a.beta)) - Number(Boolean(b.beta));
});
}
function renderToolButton(tool: ToolDefinition): string {
return ` <button class="tool-btn" data-cmd="${escapeHtml(tool.command)}">
return ` <button class="tool-btn" data-id="${escapeHtml(toolId(tool))}" data-cmd="${escapeHtml(tool.command)}">
<span class="tool-title">${escapeHtml(tool.title)}${tool.beta ? ' <span class="beta-badge">Beta</span>' : ''}</span>
<span class="tool-desc">${escapeHtml(tool.description)}</span>
</button>`;
}
function toolId(tool: ToolDefinition): string {
return tool.id || tool.command;
}
function escapeHtml(value: string): string {
return value.replace(/[&<>"']/g, ch => ({
'&': '&amp;',

View File

@@ -234,7 +234,7 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse, shar
'Content-Type': 'text/html; charset=utf-8',
'Cache-Control': 'no-store',
});
res.end(file.buf);
res.end(mobilePreviewPage(file));
return;
}
@@ -296,6 +296,35 @@ function chooserPage(files: SharedFile[]): string {
</body></html>`;
}
function mobilePreviewPage(file: SharedFile): string {
const playableJson = JSON.stringify(file.buf.toString('utf8')).replace(/<\/script/gi, '<\\/script');
const title = file.filename.replace(/[&<>"]/g, (c) =>
({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c] as string)
);
return `<!DOCTYPE html>
<html><head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>${title}</title>
<style>
html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; background: #000; }
iframe { position: fixed; inset: 0; width: 100%; height: 100%; border: 0; background: #000; }
.boot { position: fixed; inset: 0; display: grid; place-items: center; color: #777; font: 12px system-ui, sans-serif; }
</style>
</head><body>
<div id="boot" class="boot">Loading preview...</div>
<iframe id="playable" sandbox="allow-scripts allow-same-origin allow-pointer-lock allow-forms allow-popups allow-popups-to-escape-sandbox allow-top-navigation-by-user-activation"></iframe>
<script>
const playableHtml = ${playableJson};
setTimeout(() => {
const frame = document.getElementById('playable');
document.getElementById('boot').style.display = 'none';
frame.srcdoc = playableHtml;
}, 750);
</script>
</body></html>`;
}
interface LanIp { address: string; iface: string; }
function getLanIps(): LanIp[] {