This commit is contained in:
2026-06-22 11:45:36 +08:00
parent 68d15aa208
commit 8e5d56ab1d
13 changed files with 682 additions and 7 deletions

View File

@@ -9,6 +9,7 @@ import { openPlayworksConverter } from './tools/playworksConverter';
import { openMintegralChecker } from './tools/mintegralChecker';
import { openChangelog } from './changelogView';
import { openDeviceSimulator } from './tools/deviceSimulator';
import { openProjectInit } from './tools/projectInit';
import { checkForUpdates } from './updateChecker';
export function activate(context: vscode.ExtensionContext) {
@@ -21,6 +22,7 @@ export function activate(context: vscode.ExtensionContext) {
vscode.commands.registerCommand('hplToolbox.openPlayworksConverter', () => openPlayworksConverter(context)),
vscode.commands.registerCommand('hplToolbox.openMintegralChecker', () => openMintegralChecker(context)),
vscode.commands.registerCommand('hplToolbox.openDeviceSimulator', () => openDeviceSimulator(context)),
vscode.commands.registerCommand('hplToolbox.openProjectInit', () => openProjectInit(context)),
vscode.commands.registerCommand('hplToolbox.openChangelog', () => openChangelog(context)),
vscode.commands.registerCommand('hplToolbox.checkForUpdates', () => checkForUpdates(context)),
vscode.window.registerWebviewViewProvider('hplToolbox.launcher', new LauncherViewProvider(context))

View File

@@ -14,6 +14,12 @@ interface ToolDefinition {
}
const FALLBACK_TOOLS: ToolDefinition[] = [
{
id: 'project-init',
command: 'hplToolbox.openProjectInit',
title: 'Initialize Project',
description: 'Download AGENTS.md and .gitignore templates from Google Drive',
},
{
command: 'hplToolbox.openPlecUpload',
title: 'PLEC Upload',

266
src/tools/projectInit.ts Normal file
View File

@@ -0,0 +1,266 @@
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import { singletonPanel, getToolWebviewStyles, handleClipboardAndOpen } from './shared';
interface FileEntry { filename: string; url: string; description?: string; }
const store: { panel: vscode.WebviewPanel | null } = { panel: null };
function gdriveToDirectUrl(url: string): string {
const fileMatch = url.match(/\/file\/d\/([a-zA-Z0-9_-]+)/);
if (fileMatch) { return `https://drive.google.com/uc?export=download&id=${fileMatch[1]}`; }
const idMatch = url.match(/[?&]id=([a-zA-Z0-9_-]+)/);
if (idMatch) { return `https://drive.google.com/uc?export=download&id=${idMatch[1]}`; }
return url;
}
async function fetchManifest(url: string): Promise<FileEntry[]> {
const res = await fetch(gdriveToDirectUrl(url), { headers: { 'User-Agent': 'Mozilla/5.0' } });
if (!res.ok) { throw new Error(`HTTP ${res.status} ${res.statusText}`); }
const json = await res.json() as unknown;
if (!Array.isArray(json)) { throw new Error('Manifest must be a JSON array of { filename, url } objects.'); }
return (json as FileEntry[]).filter(e => e.filename && e.url);
}
async function downloadFile(url: string, destPath: string): Promise<void> {
const directUrl = gdriveToDirectUrl(url);
const res = await fetch(directUrl, { headers: { 'User-Agent': 'Mozilla/5.0' }, redirect: 'follow' });
if (!res.ok) { throw new Error(`HTTP ${res.status} ${res.statusText}`); }
const contentType = res.headers.get('content-type') ?? '';
const text = await res.text();
const ext = path.extname(destPath).toLowerCase();
if (contentType.includes('text/html') && ext !== '.html' && ext !== '.htm') {
throw new Error(
'Got an HTML page instead of a file — the Google Drive link may require sign-in or a download confirmation. Make sure the file is set to "Anyone with the link can view".'
);
}
await fs.promises.writeFile(destPath, text, 'utf8');
}
function getHtml(): string {
return `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<style>
${getToolWebviewStyles()}
</style>
</head>
<body>
<main class="tool-page">
<header class="tool-header">
<h2 class="tool-title">Initialize Project</h2>
<p class="tool-description">Fetches the shared file list from your team manifest and downloads selected files into the chosen folder.</p>
</header>
<section class="tool-panel input-panel">
<div class="panel-header">
<h3 class="panel-title">Files</h3>
<div style="display:flex;gap:var(--tool-gap-xs);align-items:center;">
<span id="manifestSource" class="muted" style="font-size:11px;margin-right:var(--tool-gap-xs);"></span>
<button class="secondary" style="min-height:22px;width:26px;padding:0;font-size:14px;" title="Refresh manifest" onclick="refresh()">&#8635;</button>
<button class="secondary" style="min-height:22px;width:26px;padding:0;font-size:14px;" title="Configure manifest URL" onclick="openManifestSettings()">&#9881;</button>
</div>
</div>
<div class="panel-body">
<div class="control-group">
<div id="fileList"></div>
</div>
<div class="control-group">
<p class="control-label">Download folder</p>
<div class="field-row">
<input type="text" id="destFolder" value="" placeholder="(workspace root)" readonly />
<button class="secondary" onclick="pickFolder()">Browse…</button>
</div>
</div>
<div class="action-row">
<button id="initBtn" onclick="initProject()" disabled>Initialize Project</button>
<button id="selectAllBtn" class="secondary" onclick="toggleSelectAll()" style="display:none;">Deselect all</button>
</div>
<div id="status" class="status-panel"></div>
</div>
</section>
</main>
<script>
const vscode = acquireVsCodeApi();
const savedState = vscode.getState() || {};
document.getElementById('destFolder').value = savedState.destFolder || '';
let files = [];
let allSelected = true;
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
function refresh() {
setStatus('', '');
document.getElementById('fileList').innerHTML = '<p class="muted" style="margin:0;">Loading manifest…</p>';
document.getElementById('initBtn').disabled = true;
document.getElementById('selectAllBtn').style.display = 'none';
vscode.postMessage({ type: 'fetchManifest' });
}
function openManifestSettings() { vscode.postMessage({ type: 'openSettings' }); }
function pickFolder() { vscode.postMessage({ type: 'pickFolder' }); }
function initProject() {
const checked = files.filter((_, i) => document.getElementById('cb_' + i)?.checked);
if (!checked.length) { setStatus('No files selected.', 'err'); return; }
const destFolder = document.getElementById('destFolder').value.trim();
const el = document.getElementById('status');
el.textContent = 'Downloading ' + checked.length + ' file(s)…';
el.className = 'status-panel is-busy';
vscode.postMessage({ type: 'init', files: checked, destFolder });
}
function toggleSelectAll() {
allSelected = !allSelected;
files.forEach((_, i) => { const cb = document.getElementById('cb_' + i); if (cb) cb.checked = allSelected; });
document.getElementById('selectAllBtn').textContent = allSelected ? 'Deselect all' : 'Select all';
}
function renderFiles(list, manifestUrl) {
files = list;
allSelected = true;
document.getElementById('manifestSource').textContent =
manifestUrl ? '(' + manifestUrl.replace(/^https?:\\/\\//, '').split('/')[0] + ')' : '';
if (!list.length) {
document.getElementById('fileList').innerHTML = '<p class="muted" style="margin:0;">Manifest loaded but contains no files.</p>';
document.getElementById('initBtn').disabled = true;
document.getElementById('selectAllBtn').style.display = 'none';
return;
}
const rows = list.map((f, i) =>
'<tr>' +
'<td style="width:20px;text-align:center;"><input type="checkbox" id="cb_' + i + '" checked /></td>' +
'<td class="mono wrap" style="width:20%;">' + escapeHtml(f.filename) + '</td>' +
'<td class="muted wrap" style="width:70%;">' + escapeHtml(f.description || '') + '</td>' +
'<td style="width:10%;text-align:right;"><button style="min-height:22px;padding:2px 8px;font-size:11px;" data-url="' + escapeHtml(f.url) + '" onclick="openUrl(this)">Open</button></td>' +
'</tr>'
).join('');
document.getElementById('fileList').innerHTML =
'<div class="results-panel" style="margin-top:0;overflow-x:hidden;">' +
'<table class="data-table">' +
'<thead><tr>' +
'<th style="width:20px;"></th>' +
'<th style="width:20%;">Filename</th>' +
'<th style="width:70%;">Description</th>' +
'<th style="width:10%;"></th>' +
'</tr></thead>' +
'<tbody>' + rows + '</tbody>' +
'</table>' +
'</div>';
document.getElementById('initBtn').disabled = false;
document.getElementById('selectAllBtn').style.display = '';
document.getElementById('selectAllBtn').textContent = 'Deselect all';
}
function openUrl(btn) {
vscode.postMessage({ type: 'open', text: btn.dataset.url });
}
window.addEventListener('message', e => {
const msg = e.data;
if (msg.type === 'manifest') { renderFiles(msg.files, msg.manifestUrl); setStatus('', ''); }
if (msg.type === 'manifestError') {
document.getElementById('fileList').innerHTML = '<p class="err" style="margin:0;">' + escapeHtml(msg.text) + '</p>';
document.getElementById('initBtn').disabled = true;
document.getElementById('selectAllBtn').style.display = 'none';
}
if (msg.type === 'folder') {
document.getElementById('destFolder').value = msg.path;
const s = vscode.getState() || {};
s.destFolder = msg.path;
vscode.setState(s);
}
if (msg.type === 'result') { setStatus(msg.text, msg.ok ? 'ok' : 'err'); }
});
function setStatus(text, cls) {
const el = document.getElementById('status');
el.textContent = text;
el.className = 'status-panel' + (cls ? ' ' + cls : '');
}
refresh();
</script>
</body>
</html>`;
}
export function openProjectInit(context: vscode.ExtensionContext) {
const { panel, isNew } = singletonPanel(store, 'hplToolbox.projectInit', 'Initialize Project');
if (!isNew) { return; }
panel.webview.html = getHtml();
async function sendManifest() {
const manifestUrl = vscode.workspace.getConfiguration('hplToolbox.projectInit').get<string>('manifestUrl', '');
if (!manifestUrl) {
panel.webview.postMessage({
type: 'manifestError',
text: 'No manifest URL configured. Click ⚙ to set it in Settings.',
});
return;
}
try {
const files = await fetchManifest(manifestUrl);
panel.webview.postMessage({ type: 'manifest', files, manifestUrl });
} catch (err: any) {
panel.webview.postMessage({ type: 'manifestError', text: `Failed to load manifest: ${err.message}` });
}
}
panel.webview.onDidReceiveMessage(async (msg) => {
if (handleClipboardAndOpen(msg)) { return; }
if (msg.type === 'fetchManifest') {
await sendManifest();
}
if (msg.type === 'openSettings') {
vscode.commands.executeCommand('workbench.action.openSettings', 'hplToolbox.projectInit.manifestUrl');
}
if (msg.type === 'pickFolder') {
const picked = await vscode.window.showOpenDialog({
canSelectFolders: true,
canSelectFiles: false,
canSelectMany: false,
openLabel: 'Select Destination Folder',
defaultUri: vscode.workspace.workspaceFolders?.[0]?.uri,
});
if (picked?.[0]) {
panel.webview.postMessage({ type: 'folder', path: picked[0].fsPath });
}
}
if (msg.type === 'init') {
const files: FileEntry[] = msg.files;
const dest: string = msg.destFolder || vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || '';
if (!dest) {
panel.webview.postMessage({ type: 'result', ok: false, text: 'No destination folder selected and no workspace is open.' });
return;
}
const errors: string[] = [];
for (const f of files) {
try {
await downloadFile(f.url, path.join(dest, f.filename));
} catch (err: any) {
errors.push(`${f.filename}: ${err.message}`);
}
}
if (errors.length) {
panel.webview.postMessage({ type: 'result', ok: false, text: `${files.length - errors.length}/${files.length} downloaded.\n\nErrors:\n${errors.join('\n')}` });
} else {
panel.webview.postMessage({ type: 'result', ok: true, text: `Done! ${files.length} file(s) downloaded to: ${dest}` });
}
}
}, undefined, context.subscriptions);
}