commit def3dfb60d03be47b7d1a16f269ac750d1bbdd97 Author: HPL-JesusCastro Date: Thu May 7 20:53:31 2026 +0800 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3224a90 --- /dev/null +++ b/.gitignore @@ -0,0 +1,144 @@ +# Created by https://www.toptal.com/developers/gitignore/api/node +# Edit at https://www.toptal.com/developers/gitignore?templates=node + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit + +# End of https://www.toptal.com/developers/gitignore/api/node \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..02621b4 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run Extension", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ], + "preLaunchTask": "npm: compile" + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..c12701d --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,19 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "compile", + "group": { "kind": "build", "isDefault": true }, + "problemMatcher": ["$tsc"], + "label": "npm: compile" + }, + { + "type": "npm", + "script": "watch", + "isBackground": true, + "problemMatcher": ["$tsc-watch"], + "label": "npm: watch" + } + ] +} diff --git a/hpl-toolbox-0.1.0.vsix b/hpl-toolbox-0.1.0.vsix new file mode 100644 index 0000000..d806e72 Binary files /dev/null and b/hpl-toolbox-0.1.0.vsix differ diff --git a/media/hpl.png b/media/hpl.png new file mode 100644 index 0000000..ef89528 Binary files /dev/null and b/media/hpl.png differ diff --git a/media/hpl.svg b/media/hpl.svg new file mode 100644 index 0000000..8c20dab --- /dev/null +++ b/media/hpl.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/media/icon.svg b/media/icon.svg new file mode 100644 index 0000000..671148a --- /dev/null +++ b/media/icon.svg @@ -0,0 +1 @@ + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..cd2fb86 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,58 @@ +{ + "name": "hpl-toolbox", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "hpl-toolbox", + "version": "0.1.0", + "devDependencies": { + "@types/node": "^20.0.0", + "@types/vscode": "^1.85.0", + "typescript": "^5.4.0" + }, + "engines": { + "vscode": "^1.85.0" + } + }, + "node_modules/@types/node": { + "version": "20.19.39", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", + "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/vscode": { + "version": "1.118.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.118.0.tgz", + "integrity": "sha512-Ah6eTlqDcwIMELEVwQMO++rJAFBRz/oLluLD/vWdYrH1KuI9kfpaM+7pg0OvvascgcJy+ghLCERAYouM4QbzGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..924220e --- /dev/null +++ b/package.json @@ -0,0 +1,99 @@ +{ + "name": "hpl-toolbox", + "displayName": "HPL Toolbox", + "description": "Bundled tools: PLEC Upload, AppLovin Demo Upload, Base64 Asset Scanner, Daily Update.", + "version": "0.1.0", + "publisher": "hesukastro", + "engines": { + "vscode": "^1.85.0" + }, + "categories": ["Other"], + "icon": "media/hpl.png", + "main": "./out/extension.js", + "activationEvents": [], + "contributes": { + "commands": [ + { + "command": "hplToolbox.openPlecUpload", + "title": "HPL Toolbox: Open PLEC Upload" + }, + { + "command": "hplToolbox.openApplovinUpload", + "title": "HPL Toolbox: Open AppLovin Demo Upload" + }, + { + "command": "hplToolbox.openBase64Scanner", + "title": "HPL Toolbox: Open Base64 Scanner" + }, + { + "command": "hplToolbox.openDailyUpdate", + "title": "HPL Toolbox: Open Daily Update" + } + ], + "viewsContainers": { + "activitybar": [ + { + "id": "hplToolbox", + "title": "HPL Toolbox", + "icon": "media/hpl.png" + } + ] + }, + "views": { + "hplToolbox": [ + { + "type": "webview", + "id": "hplToolbox.launcher", + "name": "Tools" + } + ] + }, + "configuration": [ + { + "title": "HPL Toolbox — PLEC Upload", + "properties": { + "hplToolbox.plec.uploadUrl": { + "type": "string", + "default": "http://167.99.227.249/src/upload_HTML_Experimental.php", + "description": "Endpoint URL for the PLEC HTML upload script." + }, + "hplToolbox.plec.originUrl": { + "type": "string", + "default": "http://167.99.227.249", + "description": "Origin/Referer header sent with upload requests." + }, + "hplToolbox.plec.previewBase": { + "type": "string", + "default": "https://playable.applovindemo.com/phaser/", + "description": "Preview server base URL used to check if a filename already exists." + }, + "hplToolbox.plec.skipExistsCheck": { + "type": "boolean", + "default": false, + "description": "Skip the pre-check that verifies the filename does not already exist on the preview server." + } + } + }, + { + "title": "HPL Toolbox — AppLovin Upload", + "properties": { + "hplToolbox.applovin.cookie": { + "type": "string", + "default": "", + "description": "Value of the __Host-APPLOVINID cookie for p.applov.in (log in at https://p.applov.in/playablePreview?create=1&qr=1 and copy the cookie from DevTools)." + } + } + } + ] + }, + "scripts": { + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./", + "vscode:prepublish": "npm run compile" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/vscode": "^1.85.0", + "typescript": "^5.4.0" + } +} diff --git a/src/extension.ts b/src/extension.ts new file mode 100644 index 0000000..4f05bab --- /dev/null +++ b/src/extension.ts @@ -0,0 +1,18 @@ +import * as vscode from 'vscode'; +import { LauncherViewProvider } from './launcherView'; +import { openPlecUpload } from './tools/plecUpload'; +import { openApplovinUpload } from './tools/applovinUpload'; +import { openBase64Scanner } from './tools/base64Scanner'; +import { openDailyUpdate } from './tools/dailyUpdate'; + +export function activate(context: vscode.ExtensionContext) { + context.subscriptions.push( + vscode.commands.registerCommand('hplToolbox.openPlecUpload', () => openPlecUpload(context)), + vscode.commands.registerCommand('hplToolbox.openApplovinUpload', () => openApplovinUpload(context)), + vscode.commands.registerCommand('hplToolbox.openBase64Scanner', () => openBase64Scanner(context)), + vscode.commands.registerCommand('hplToolbox.openDailyUpdate', () => openDailyUpdate(context)), + vscode.window.registerWebviewViewProvider('hplToolbox.launcher', new LauncherViewProvider()) + ); +} + +export function deactivate() {} diff --git a/src/launcherView.ts b/src/launcherView.ts new file mode 100644 index 0000000..ecfddf1 --- /dev/null +++ b/src/launcherView.ts @@ -0,0 +1,65 @@ +import * as vscode from 'vscode'; + +export class LauncherViewProvider implements vscode.WebviewViewProvider { + resolveWebviewView(view: vscode.WebviewView) { + view.webview.options = { enableScripts: true }; + view.webview.html = getHtml(); + view.webview.onDidReceiveMessage((msg) => { + if (msg?.type === 'open' && typeof msg.command === 'string') { + vscode.commands.executeCommand(msg.command); + } + }); + } +} + +function getHtml(): string { + return ` + + + + + + +

HPL Toolbox

+ + + + + + +`; +} diff --git a/src/tools/applovinUpload.ts b/src/tools/applovinUpload.ts new file mode 100644 index 0000000..6c9ba3d --- /dev/null +++ b/src/tools/applovinUpload.ts @@ -0,0 +1,262 @@ +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; +import { File } from 'node:buffer'; +import { handleClipboardAndOpen, singletonPanel } from './shared'; + +const store: { panel: vscode.WebviewPanel | null } = { panel: null }; + +export function openApplovinUpload(_context: vscode.ExtensionContext) { + const { panel, isNew } = singletonPanel(store, 'hplToolbox.applovinUpload', 'AppLovin Demo Upload'); + if (!isNew) return; + + panel.webview.html = getHtml(); + panel.webview.onDidReceiveMessage(async (msg) => { + try { + if (handleClipboardAndOpen(msg)) return; + + if (msg.type === 'pickFiles') { + const picked = await vscode.window.showOpenDialog({ + canSelectMany: true, + filters: { HTML: ['html', 'htm'] }, + openLabel: 'Select', + }); + if (picked && picked.length) { + panel.webview.postMessage({ + type: 'filesSelected', + files: picked.map(u => ({ path: u.fsPath, name: path.basename(u.fsPath) })), + }); + } + return; + } + + if (msg.type === 'upload') { + await handleUpload(panel, msg.paths); + return; + } + } catch (err: any) { + panel.webview.postMessage({ type: 'error', message: err?.message || String(err) }); + } + }); +} + +async function handleUpload(panel: vscode.WebviewPanel, filePaths: string[]) { + const cfg = vscode.workspace.getConfiguration('hplToolbox.applovin'); + const cookie = (cfg.get('cookie') || '').trim(); + + const paths = Array.isArray(filePaths) ? filePaths : [filePaths]; + if (!paths.length) { + panel.webview.postMessage({ type: 'error', message: 'No files selected.' }); + return; + } + + const headers: Record = { + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Origin': 'https://p.applov.in', + 'Referer': 'https://p.applov.in/playablePreview?create=1&qr=1', + 'X-Requested-With': 'XMLHttpRequest', + 'User-Agent': 'Mozilla/5.0 (HPL-Toolbox-VSCode)', + }; + if (cookie) headers['Cookie'] = `__Host-APPLOVINID=${cookie}`; + + const buildForm = (filePath: string): FormData => { + const buf = fs.readFileSync(filePath); + const file = new File([buf], path.basename(filePath), { type: 'text/html' }); + const form = new FormData(); + form.append('file', file as any); + return form; + }; + + panel.webview.postMessage({ type: 'start', total: paths.length }); + + for (let i = 0; i < paths.length; i++) { + const filePath = paths[i]; + const fileName = path.basename(filePath); + const idx = i + 1; + const reportError = (message: string) => { + panel.webview.postMessage({ type: 'fileError', name: fileName, message }); + }; + + if (!filePath || !fs.existsSync(filePath)) { reportError('File missing'); continue; } + if (!/\.html?$/i.test(filePath)) { reportError('Must be .html'); continue; } + + panel.webview.postMessage({ type: 'status', message: `[${idx}/${paths.length}] Validating ${fileName}...` }); + + let res: Response; + try { + res = await fetch('https://p.applov.in/validateHTMLContent', { + method: 'POST', body: buildForm(filePath), headers, + }); + } catch (err: any) { reportError('Network error (validate): ' + (err?.message || err)); continue; } + const validateText = await res.text(); + if (!res.ok) { reportError(`Validate HTTP ${res.status}: ${validateText.slice(0, 300)}`); continue; } + let validateJson: any; + try { validateJson = JSON.parse(validateText); } catch { validateJson = null; } + if (validateJson && validateJson.code && validateJson.code !== 200) { + reportError(`Validation failed: ${validateText.slice(0, 300)}`); continue; + } + + panel.webview.postMessage({ type: 'status', message: `[${idx}/${paths.length}] Uploading ${fileName}...` }); + + try { + res = await fetch('https://p.applov.in/getCachedAdURL', { + method: 'POST', body: buildForm(filePath), headers, + }); + } catch (err: any) { reportError('Network error (cache): ' + (err?.message || err)); continue; } + const cacheText = await res.text(); + if (!res.ok) { reportError(`Cache HTTP ${res.status}: ${cacheText.slice(0, 300)}`); continue; } + let cacheJson: any; + try { cacheJson = JSON.parse(cacheText); } + catch { reportError(`Non-JSON response: ${cacheText.slice(0, 300)}`); continue; } + const hash = cacheJson.results; + if (!hash) { reportError(`No hash in response: ${cacheText.slice(0, 300)}`); continue; } + + panel.webview.postMessage({ + type: 'fileResult', + name: fileName, + hash, + previewUrl: `https://p.applov.in/getPreviewHTML?preview=${hash}`, + pageUrl: `https://p.applov.in/playablePreview?preview=${hash}&qr=1`, + qrImg: `https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=${encodeURIComponent(hash)}`, + }); + } + + panel.webview.postMessage({ type: 'done' }); +} + +function getHtml(): string { + return ` + + + + + + +

AppLovin Playable Preview (QR)

+

Upload HTML files to p.applov.in and get QR codes to scan with the AppLovin Playable Preview app (iOS/Android). Requires the __Host-APPLOVINID cookie set in HPL Toolbox — AppLovin Upload settings.

+ +
+ + (no files) +
+
+ +
+
+
+ + + +`; +} diff --git a/src/tools/base64Scanner.ts b/src/tools/base64Scanner.ts new file mode 100644 index 0000000..66959ee --- /dev/null +++ b/src/tools/base64Scanner.ts @@ -0,0 +1,192 @@ +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; +import { 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'); + if (!isNew) return; + + panel.webview.html = getHtml(); + panel.webview.onDidReceiveMessage(async (msg) => { + if (msg.type === 'ready') { + const ws = vscode.workspace.workspaceFolders?.[0]; + 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 }); + } + } + } else if (msg.type === 'pickFolder') { + const picked = await vscode.window.showOpenDialog({ + canSelectFolders: true, + canSelectFiles: false, + canSelectMany: false, + openLabel: 'Select Folder', + }); + if (picked && picked[0]) { + panel.webview.postMessage({ type: 'folderPicked', path: picked[0].fsPath }); + } + } else if (msg.type === 'scan') { + const folder: string = msg.folder; + if (!folder || !fs.existsSync(folder)) { + panel.webview.postMessage({ type: 'results', results: [], error: 'Invalid folder' }); + return; + } + const htmlFiles = await collectHtmlFiles(folder); + const results: { file: string; ok: boolean; assets: string[] }[] = []; + for (const file of htmlFiles) { + try { + const content = fs.readFileSync(file, 'utf8'); + const offenders = findNonBase64Assets(content); + results.push({ file: path.relative(folder, file), ok: offenders.length === 0, assets: offenders }); + } catch { + // skip unreadable + } + } + panel.webview.postMessage({ type: 'results', results }); + } + }); +} + +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 findNonBase64Assets(html: string): string[] { + const markup = html.replace(/]*>[\s\S]*?<\/script>/gi, ''); + const offenders = new Set(); + const attrRegex = /\b(?:src|href|poster|data-src)\s*=\s*("([^"]*)"|'([^']*)')/gi; + let m: RegExpExecArray | null; + while ((m = attrRegex.exec(markup)) !== null) { + const url = (m[2] ?? m[3] ?? '').trim(); + if (!url) continue; + if (isAssetReference(url) && !isBase64DataUri(url)) { + offenders.add(url); + } + } + const cssUrlRegex = /url\(\s*(?:"([^"]*)"|'([^']*)'|([^)]*))\s*\)/gi; + while ((m = cssUrlRegex.exec(markup)) !== null) { + const url = (m[1] ?? m[2] ?? m[3] ?? '').trim(); + if (!url) continue; + if (!isBase64DataUri(url)) { + offenders.add(url); + } + } + return [...offenders]; +} + +function isBase64DataUri(url: string): boolean { + return /^data:[^;,]*;base64,/i.test(url); +} + +function isAssetReference(url: string): boolean { + if (url.startsWith('#') || url.toLowerCase().startsWith('javascript:')) return false; + if (/^(mailto:|tel:)/i.test(url)) return false; + return true; +} + +function getHtml(): string { + return ` + + + + + + +

Base64 Asset Scanner

+
+ + + +
+
+
+ + +`; +} diff --git a/src/tools/dailyUpdate.ts b/src/tools/dailyUpdate.ts new file mode 100644 index 0000000..e678384 --- /dev/null +++ b/src/tools/dailyUpdate.ts @@ -0,0 +1,121 @@ +import * as vscode from 'vscode'; +import { singletonPanel } from './shared'; + +const PROJECT_KEY = 'hplToolbox.dailyUpdate.lastProject'; +const store: { panel: vscode.WebviewPanel | null } = { panel: null }; + +export function openDailyUpdate(context: vscode.ExtensionContext) { + const { panel, isNew } = singletonPanel(store, 'hplToolbox.dailyUpdate', 'Daily Update'); + if (!isNew) return; + + const lastProject = context.globalState.get(PROJECT_KEY, ''); + panel.webview.html = getHtml(lastProject); + + panel.webview.onDidReceiveMessage(async (msg) => { + if (msg.type === 'submit') { + const text = formatMessage(msg.payload); + await vscode.env.clipboard.writeText(text); + await context.globalState.update(PROJECT_KEY, msg.payload.project); + vscode.window.showInformationMessage('Daily update copied to clipboard.'); + panel.dispose(); + } + }); +} + +function formatMessage(p: { date: string; status: string; project: string; remarks: string }): string { + const bullets = p.remarks + .split(/\r?\n/) + .map((l) => l.replace(/^\s*[-*]\s*/, '').trim()) + .filter((l) => l.length > 0) + .map((l) => `- ${l}`) + .join('\n'); + + const lines = [ + `Date: ${formatDate(p.date)}`, + `Status: ${p.status}`, + `Project: ${p.project}`, + `Remarks:`, + bullets, + ]; + + if (p.status === 'For OT') { + lines.push('@Joyce Lanot , @Reland Pigte, @Anthony Castor, @Angela Grace'); + } + + return lines.join('\n'); +} + +function formatDate(iso: string): string { + const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso); + if (!m) return iso; + return `${m[2]}/${m[3]}/${m[1]}`; +} + +function escapeAttr(s: string): string { + return s.replace(/&/g, '&').replace(/"/g, '"').replace(//g, '>'); +} + +function getHtml(lastProject: string): string { + const today = new Date().toISOString().slice(0, 10); + return ` + + + + + + + +

Daily Update

+
Submit copies the formatted text to your clipboard.
+ + + + + + + + + + + + + + + + + +`; +} diff --git a/src/tools/plecUpload.ts b/src/tools/plecUpload.ts new file mode 100644 index 0000000..6cc6df8 --- /dev/null +++ b/src/tools/plecUpload.ts @@ -0,0 +1,347 @@ +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; +import { File } from 'node:buffer'; +import { 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 === 'pickFile') { + const picked = await vscode.window.showOpenDialog({ + canSelectMany: false, + filters: { HTML: ['html', 'htm'] }, + openLabel: 'Select', + }); + if (picked && picked[0]) { + const fp = picked[0].fsPath; + panel.webview.postMessage({ + type: 'fileSelected', + rowId: msg.rowId, + path: fp, + name: path.basename(fp), + }); + } + 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 previewBase = cfg.get('previewBase')!; + const skipExistsCheck = cfg.get('skipExistsCheck')!; + + 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; + } + + panel.webview.postMessage({ type: 'status', message: 'Checking filename availability...' }); + + if (!skipExistsCheck) { + for (const r of valid) { + const fname = path.basename(r.path); + const exists = await checkFileExists(previewBase + encodeURIComponent(fname)); + if (exists === true) { + panel.webview.postMessage({ + type: 'rowError', + rowId: r.id, + message: `Filename "${fname}" already exists on preview server. Rename and retry.`, + }); + return; + } + } + } + + 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; + const buf = fs.readFileSync(r.path); + const file = new File([buf], path.basename(r.path), { type: 'text/html' }); + form.append(`fileUpload_${idx}`, file as any); + form.append(`iterationNameUpload_${idx}`, encodeURI(r.iteration.replace(/\s+/g, ''))); + } + + 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 }); +} + +async function checkFileExists(url: string): Promise { + try { + const res = await fetch(url, { method: 'HEAD' }); + if (res.status === 200) return true; + if (res.status === 404) return false; + return null; + } catch { + return null; + } +} + +function getHtml(): string { + return ` + + + + + + +

PLEC Upload

+

Pick HTML files, give each an iteration name, then upload. The preview link is returned by the server.

+ + + + + + + + + + +
HTML FileIteration Name
+ +
+ + +
+ +
+
+ + + +`; +} diff --git a/src/tools/shared.ts b/src/tools/shared.ts new file mode 100644 index 0000000..6e1a374 --- /dev/null +++ b/src/tools/shared.ts @@ -0,0 +1,34 @@ +import * as vscode from 'vscode'; + +export function handleClipboardAndOpen(msg: any): boolean { + if (msg?.type === 'copy' && typeof msg.text === 'string') { + vscode.env.clipboard.writeText(msg.text); + vscode.window.showInformationMessage('Copied to clipboard.'); + return true; + } + if (msg?.type === 'open' && typeof msg.text === 'string') { + vscode.env.openExternal(vscode.Uri.parse(msg.text)); + return true; + } + return false; +} + +export function singletonPanel( + store: { panel: vscode.WebviewPanel | null }, + viewType: string, + title: string +): { panel: vscode.WebviewPanel; isNew: boolean } { + if (store.panel) { + store.panel.reveal(); + return { panel: store.panel, isNew: false }; + } + const panel = vscode.window.createWebviewPanel( + viewType, + title, + vscode.ViewColumn.Active, + { enableScripts: true, retainContextWhenHidden: true } + ); + store.panel = panel; + panel.onDidDispose(() => { store.panel = null; }); + return { panel, isNew: true }; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..312f630 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2020", + "outDir": "out", + "lib": ["ES2020", "DOM"], + "sourceMap": true, + "rootDir": "src", + "strict": true, + "esModuleInterop": true + }, + "exclude": ["node_modules", ".vscode-test"] +}