import * as vscode from 'vscode'; import * as fs from 'fs'; import * as path from 'path'; import { File } from 'node:buffer'; import { getToolWebviewStyles, handleClipboardAndOpen, singletonPanel } from './shared'; const store: { panel: vscode.WebviewPanel | null } = { panel: null }; export function openPlecUpload(_context: vscode.ExtensionContext) { const { panel, isNew } = singletonPanel(store, 'hplToolbox.plecUpload', 'PLEC Upload'); if (!isNew) return; panel.webview.html = getHtml(); panel.webview.onDidReceiveMessage(async (msg) => { try { if (handleClipboardAndOpen(msg)) return; if (msg.type === 'pickFolder') { const picked = await vscode.window.showOpenDialog({ canSelectFolders: true, canSelectFiles: false, canSelectMany: false, openLabel: 'Select Folder', defaultUri: getPickerDefaultUri(), }); if (picked && picked[0]) { const files = await collectHtmlFiles(picked[0].fsPath); panel.webview.postMessage({ type: 'filesSelected', files: files.map(filePath => ({ path: filePath, name: path.basename(filePath) })), }); } return; } if (msg.type === 'pickFile') { const picked = await vscode.window.showOpenDialog({ canSelectMany: true, filters: { HTML: ['html', 'htm'] }, openLabel: 'Select', defaultUri: getPickerDefaultUri(), }); if (picked && picked.length) { panel.webview.postMessage({ type: 'filesSelected', rowId: msg.rowId, files: picked.map(u => ({ path: u.fsPath, name: path.basename(u.fsPath) })), }); } return; } if (msg.type === 'upload') { await handleUpload(panel, msg.rows); return; } } catch (err: any) { panel.webview.postMessage({ type: 'error', message: err?.message || String(err) }); } }); } interface UploadRow { id: string; path: string; iteration: string; } async function handleUpload(panel: vscode.WebviewPanel, rows: UploadRow[]) { const cfg = vscode.workspace.getConfiguration('hplToolbox.plec'); const uploadUrl = cfg.get('uploadUrl')!; const originUrl = cfg.get('originUrl')!; const valid: UploadRow[] = []; for (const r of rows) { if (!r.path || !fs.existsSync(r.path)) { panel.webview.postMessage({ type: 'rowError', rowId: r.id, message: 'File missing' }); return; } if (!r.iteration || !r.iteration.trim()) { panel.webview.postMessage({ type: 'rowError', rowId: r.id, message: 'Iteration name required' }); return; } if (!/\.html?$/i.test(r.path)) { panel.webview.postMessage({ type: 'rowError', rowId: r.id, message: 'Must be .html' }); return; } valid.push(r); } if (valid.length === 0) { panel.webview.postMessage({ type: 'error', message: 'Nothing to upload.' }); return; } const stamp = timestampSuffix(); const stampedNames = valid.map((r) => appendTimestamp(path.basename(r.path), stamp)); panel.webview.postMessage({ type: 'status', message: 'Uploading...' }); const form = new FormData(); for (let i = 0; i < valid.length; i++) { const r = valid[i]; const idx = i + 1; panel.webview.postMessage({ type: 'status', message: `Preparing ${idx}/${valid.length} ${path.basename(r.path)}` }); const buf = fs.readFileSync(r.path); const file = new File([buf], stampedNames[i], { type: 'text/html' }); form.append(`fileUpload_${idx}`, file as any); form.append(`iterationNameUpload_${idx}`, encodeURI(r.iteration.replace(/\s+/g, ''))); } panel.webview.postMessage({ type: 'status', message: `Uploading ${valid.length} file${valid.length === 1 ? '' : 's'}...` }); let res: Response; try { res = await fetch(uploadUrl, { method: 'POST', body: form, headers: { 'Origin': originUrl, 'Referer': originUrl + '/', 'User-Agent': 'Mozilla/5.0 (HPL-Toolbox-VSCode)', 'X-Requested-With': 'XMLHttpRequest', 'Accept': '*/*', }, }); } catch (err: any) { panel.webview.postMessage({ type: 'error', message: 'Network error: ' + (err?.message || err) }); return; } const text = await res.text(); if (!res.ok) { panel.webview.postMessage({ type: 'error', message: `HTTP ${res.status}\n${text.slice(0, 800)}`, }); return; } let json: any; try { json = JSON.parse(text); } catch { panel.webview.postMessage({ type: 'error', message: `Server returned non-JSON:\n${text.slice(0, 800)}`, }); return; } const preview = json.preview || json.previewURL || null; panel.webview.postMessage({ type: 'result', preview, raw: json, rawText: text }); } function timestampSuffix(d: Date = new Date()): string { const pad = (n: number, w = 2) => String(n).padStart(w, '0'); return ( d.getFullYear().toString() + pad(d.getMonth() + 1) + pad(d.getDate()) + pad(d.getHours()) + pad(d.getMinutes()) + pad(d.getSeconds()) ); } function appendTimestamp(filename: string, stamp: string): string { const ext = path.extname(filename); const base = ext ? filename.slice(0, -ext.length) : filename; return `${base}_${stamp}${ext}`; } function getPickerDefaultUri(): vscode.Uri | undefined { const folders = vscode.workspace.workspaceFolders; if (!folders || folders.length === 0) return undefined; const root = folders[0].uri.fsPath; const dist = path.join(root, 'dist'); if (fs.existsSync(dist) && fs.statSync(dist).isDirectory()) { return vscode.Uri.file(dist); } return vscode.Uri.file(root); } async function collectHtmlFiles(root: string): Promise { const out: string[] = []; const stack: string[] = [root]; while (stack.length) { const dir = stack.pop()!; let entries: fs.Dirent[]; try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; } for (const e of entries) { const full = path.join(dir, e.name); if (e.isDirectory()) { if (e.name === 'node_modules' || e.name === '.git') continue; stack.push(full); } else if (e.isFile() && /\.html?$/i.test(e.name)) { out.push(full); } } } return out; } function getHtml(): string { return `

PLEC Upload

Pick HTML files, give each an iteration name, then upload them to PLEC.

Inputs

(no files selected)
`; }