diff --git a/aiAssets/LAYOUT.md b/aiAssets/LAYOUT.md new file mode 100644 index 0000000..8e474b7 --- /dev/null +++ b/aiAssets/LAYOUT.md @@ -0,0 +1,19 @@ +# Unified Layout + +[Select Folder] [Select Files] [Clear] + +(PLEC UPLOAD) +| FILENAME |ITERATION NAME| | +|-----------------|--------------|---| +|pato-to-file.html| 01_full | X | +|pato-to-file.html| 01_full | X | +|pato-to-file.html| 01_full | X | + +(AppLovin Playable Preview)(Base64 Scanner)(Send To Mobile)(MRAID Checker) +| FILENAME | | +|-----------------|-------| +|pato-to-file.html| X | +|pato-to-file.html| X | +|pato-to-file.html| X | + +[Scan]/[Upload]/[Start] \ No newline at end of file diff --git a/package.json b/package.json index 8a443c5..bc73b91 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "hpl-toolbox", "displayName": "HPL Toolbox", - "description": "Bundled tools: PLEC Upload, AppLovin Demo Upload, Base64 Asset Scanner, MRAID Checker. Local HTML Host.", + "description": "Bundled tools: PLEC Upload, AppLovin Playable Preview, Base64 Scanner, MRAID Checker. Local HTML Host.", "version": "0.1.6", "publisher": "hesukastro", "license": "UNLICENSED", @@ -26,7 +26,7 @@ }, { "command": "hplToolbox.openApplovinUpload", - "title": "HPL Toolbox: Open AppLovin Demo Upload" + "title": "HPL Toolbox: Open AppLovin Playable Preview" }, { "command": "hplToolbox.openBase64Scanner", @@ -59,7 +59,7 @@ { "type": "webview", "id": "hplToolbox.launcher", - "name": "Tools" + "name": "" } ] }, @@ -132,4 +132,4 @@ "@types/vscode": "^1.85.0", "typescript": "^5.4.0" } -} \ No newline at end of file +} diff --git a/src/launcherView.ts b/src/launcherView.ts index 2ba0936..4c0a9e6 100644 --- a/src/launcherView.ts +++ b/src/launcherView.ts @@ -17,7 +17,7 @@ const TOOLS: ToolDefinition[] = [ }, { command: 'hplToolbox.openApplovinUpload', - title: 'AppLovin Demo Upload', + title: 'AppLovin Playable Preview', description: 'Upload to p.applov.in (QR preview)', }, { @@ -80,7 +80,6 @@ function getHtml(version: string, betaToolsEnabled: boolean): string { color: var(--vscode-foreground); padding: 12px 8px; } - h3 { margin: 0 0 12px 0; font-size: 11px; text-transform: uppercase; letter-spacing: 0.6px; opacity: 0.7; } .tools { flex: 1; } .tool-btn { display: block; width: 100%; text-align: left; @@ -122,10 +121,15 @@ function getHtml(version: string, betaToolsEnabled: boolean): string { background: var(--vscode-button-secondaryHoverBackground); } .beta-note { display: block; margin-top: 3px; color: var(--vscode-descriptionForeground); font-size: 11px; } + .footer { + margin-top: 10px; + color: var(--vscode-descriptionForeground); + font-size: 8px; + text-align: right; + } -

HPL Toolbox ${version}

${toolButtons}
@@ -135,6 +139,7 @@ ${toolButtons} ${betaToolsEnabled ? 'Beta tools are visible in this launcher.' : `${hiddenBetaCount} beta tool(s) hidden until enabled.`} + `; diff --git a/src/tools/sendToMobile.ts b/src/tools/sendToMobile.ts index 2d03180..e92d8b1 100644 --- a/src/tools/sendToMobile.ts +++ b/src/tools/sendToMobile.ts @@ -4,7 +4,7 @@ import * as path from 'path'; import * as http from 'http'; import * as os from 'os'; import * as crypto from 'crypto'; -import { handleClipboardAndOpen } from './shared'; +import { getToolWebviewStyles, handleClipboardAndOpen } from './shared'; const store: { panel: vscode.WebviewPanel | null } = { panel: null }; @@ -12,7 +12,11 @@ interface ActiveShare { server: http.Server; port: number; token: string; - fileBuf: Buffer; + files: SharedFile[]; +} + +interface SharedFile { + buf: Buffer; filename: string; } @@ -57,19 +61,35 @@ export function openSendToMobile(context: vscode.ExtensionContext) { 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: 'fileSelected', + files: files.map(filePath => ({ path: filePath, name: path.basename(filePath) })), + }); + } + return; + } + if (msg.type === 'pickFile') { const picked = await vscode.window.showOpenDialog({ - canSelectMany: false, + canSelectMany: true, filters: { HTML: ['html', 'htm'] }, openLabel: 'Select', defaultUri: getPickerDefaultUri(), }); - if (picked && picked[0]) { - const fp = picked[0].fsPath; + if (picked && picked.length) { panel.webview.postMessage({ type: 'fileSelected', - path: fp, - name: path.basename(fp), + files: picked.map(u => ({ path: u.fsPath, name: path.basename(u.fsPath) })), }); } return; @@ -77,13 +97,19 @@ export function openSendToMobile(context: vscode.ExtensionContext) { if (msg.type === 'startShare') { stopActive(); - const filePath: string = msg.path; - if (!filePath || !fs.existsSync(filePath)) { + const filePaths: string[] = Array.isArray(msg.paths) ? msg.paths : (msg.path ? [msg.path] : []); + if (!filePaths.length) { panel.webview.postMessage({ type: 'error', message: 'File missing.' }); return; } - const fileBuf = fs.readFileSync(filePath); - const filename = path.basename(filePath); + const files: SharedFile[] = []; + for (const filePath of filePaths) { + if (!filePath || !fs.existsSync(filePath)) { + panel.webview.postMessage({ type: 'error', message: 'File missing.' }); + return; + } + files.push({ buf: fs.readFileSync(filePath), filename: path.basename(filePath) }); + } const token = crypto.randomBytes(6).toString('base64url'); const cfg = vscode.workspace.getConfiguration('hplToolbox.sendToMobile'); @@ -93,8 +119,7 @@ export function openSendToMobile(context: vscode.ExtensionContext) { server: http.createServer(), port: 0, token, - fileBuf, - filename, + files, }; share.server.on('request', (req, res) => handleRequest(req, res, share)); share.server.on('error', (err) => { @@ -125,7 +150,11 @@ export function openSendToMobile(context: vscode.ExtensionContext) { iface: ip.iface, url: `http://${ip.address}:${share.port}/s/${token}/`, })); - panel.webview.postMessage({ type: 'sharing', urls, filename }); + panel.webview.postMessage({ + type: 'sharing', + urls, + filename: files.length === 1 ? files[0].filename : `${files.length} files`, + }); }); return; } @@ -141,6 +170,30 @@ export function openSendToMobile(context: vscode.ExtensionContext) { }); } +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 handleRequest(req: http.IncomingMessage, res: http.ServerResponse, share: ActiveShare) { if (req.method !== 'GET' || !req.url) { res.statusCode = 404; @@ -156,7 +209,7 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse, shar const rest = req.url.slice(prefix.length).replace(/\?.*$/, ''); if (rest === '' || rest === '/') { - const body = chooserPage(share.filename); + const body = chooserPage(share.files); res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store', @@ -165,24 +218,40 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse, shar return; } - if (rest === '/view') { + const viewMatch = rest.match(/^\/view\/(\d+)$/); + if (rest === '/view' || viewMatch) { + const index = viewMatch ? Number(viewMatch[1]) : 0; + const file = share.files[index]; + if (!file) { + res.statusCode = 404; + res.end(); + return; + } res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store', }); - res.end(share.fileBuf); + res.end(file.buf); return; } - if (rest === '/file') { - const safeName = share.filename.replace(/[^A-Za-z0-9._-]/g, '_'); + const fileMatch = rest.match(/^\/file\/(\d+)$/); + if (rest === '/file' || fileMatch) { + const index = fileMatch ? Number(fileMatch[1]) : 0; + const file = share.files[index]; + if (!file) { + res.statusCode = 404; + res.end(); + return; + } + const safeName = file.filename.replace(/[^A-Za-z0-9._-]/g, '_'); res.writeHead(200, { 'Content-Type': 'application/octet-stream', 'Content-Disposition': `attachment; filename="${safeName}"`, - 'Content-Length': String(share.fileBuf.length), + 'Content-Length': String(file.buf.length), 'Cache-Control': 'no-store', }); - res.end(share.fileBuf); + res.end(file.buf); return; } @@ -190,10 +259,18 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse, shar res.end(); } -function chooserPage(filename: string): string { - const escName = filename.replace(/[&<>"]/g, (c) => +function chooserPage(files: SharedFile[]): string { + const esc = (value: string) => value.replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c] as string) ); + const items = files.map((file, index) => { + const escName = esc(file.filename); + return `
+
${escName}
+ View in browser + Download .html +
`; + }).join(''); return ` @@ -203,7 +280,8 @@ function chooserPage(filename: string): string { body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; margin: 0; padding: 24px; background: #111; color: #eee; } h1 { font-size: 18px; font-weight: 500; margin: 0 0 4px; opacity: 0.85; } - .file { font-size: 13px; opacity: 0.6; margin-bottom: 28px; word-break: break-all; } + .item { margin-bottom: 26px; } + .file { font-size: 13px; opacity: 0.6; margin-bottom: 10px; word-break: break-all; } a.btn { display: block; padding: 18px; margin-bottom: 14px; border-radius: 10px; font-size: 17px; font-weight: 500; text-align: center; text-decoration: none; } a.view { background: #2d7dff; color: white; } @@ -211,9 +289,7 @@ function chooserPage(filename: string): string {

HPL Toolbox

-
${escName}
- View in browser - Download .html + ${items} `; } @@ -250,26 +326,8 @@ function getHtml(qrScriptUri: string): string { -

Send To Mobile

-

- Pick an HTML file, then start sharing. Scan the QR with a phone on the same WiFi. - The phone gets a choice of View or Download. -

+
+
+

Send To Mobile

+

Pick an HTML file, then start sharing. Scan the QR with a phone on the same WiFi. The phone gets View and Download options.

+
-
-
- - (no file) +
+
+

Inputs

- -
- -
- - -
- - - -