drop zone, and remove beta tag from mraid checker
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -2,7 +2,7 @@
|
||||
"name": "hpl-toolbox",
|
||||
"displayName": "HPL Toolbox",
|
||||
"description": "Bundled tools: PLEC Upload, AppLovin Playable Preview, Base64 Scanner, MRAID Checker. Local HTML Host.",
|
||||
"version": "0.1.6",
|
||||
"version": "0.1.7",
|
||||
"publisher": "hesukastro",
|
||||
"license": "UNLICENSED",
|
||||
"repository": {
|
||||
|
||||
11
run-standalone.ps1
Normal file
11
run-standalone.ps1
Normal file
@@ -0,0 +1,11 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$root = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$standalone = Join-Path $root 'standalone'
|
||||
|
||||
Push-Location $standalone
|
||||
try {
|
||||
go run .
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
@@ -1,15 +1,18 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const BETA_TOOLS_ENABLED_KEY = 'hplToolbox.betaTools.enabled';
|
||||
|
||||
interface ToolDefinition {
|
||||
id?: string;
|
||||
command: string;
|
||||
title: string;
|
||||
description: string;
|
||||
beta?: boolean;
|
||||
}
|
||||
|
||||
const TOOLS: ToolDefinition[] = [
|
||||
const FALLBACK_TOOLS: ToolDefinition[] = [
|
||||
{
|
||||
command: 'hplToolbox.openPlecUpload',
|
||||
title: 'PLEC Upload',
|
||||
@@ -25,17 +28,16 @@ const TOOLS: ToolDefinition[] = [
|
||||
title: 'Base64 Scanner',
|
||||
description: 'Find non-base64 assets in HTML',
|
||||
},
|
||||
{
|
||||
command: 'hplToolbox.openMraidChecker',
|
||||
title: 'MRAID Checker',
|
||||
description: 'Check MRAID requirements and best practices',
|
||||
beta: true,
|
||||
},
|
||||
{
|
||||
command: 'hplToolbox.openSendToMobile',
|
||||
title: 'Send To Mobile',
|
||||
description: 'Share HTML to a phone via LAN + QR',
|
||||
},
|
||||
{
|
||||
command: 'hplToolbox.openMraidChecker',
|
||||
title: 'MRAID Checker',
|
||||
description: 'Check MRAID requirements and best practices',
|
||||
},
|
||||
{
|
||||
command: 'hplToolbox.openPlayworksConverter',
|
||||
title: 'Playworks Converter',
|
||||
@@ -50,8 +52,9 @@ export class LauncherViewProvider implements vscode.WebviewViewProvider {
|
||||
resolveWebviewView(view: vscode.WebviewView) {
|
||||
view.webview.options = { enableScripts: true };
|
||||
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);
|
||||
view.webview.html = getHtml(version, betaToolsEnabled, tools);
|
||||
view.webview.onDidReceiveMessage(async (msg) => {
|
||||
if (msg?.type === 'open' && typeof msg.command === 'string') {
|
||||
vscode.commands.executeCommand(msg.command);
|
||||
@@ -59,14 +62,34 @@ 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);
|
||||
view.webview.html = getHtml(version, msg.enabled, tools);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getHtml(version: string, betaToolsEnabled: boolean): string {
|
||||
const visibleTools = sortToolsForDisplay(TOOLS.filter(tool => betaToolsEnabled || !tool.beta));
|
||||
function loadToolDefinitions(extensionPath: string): ToolDefinition[] {
|
||||
const toolsPath = path.join(extensionPath, 'tools.json');
|
||||
try {
|
||||
const raw = JSON.parse(fs.readFileSync(toolsPath, 'utf8'));
|
||||
if (!Array.isArray(raw)) return FALLBACK_TOOLS;
|
||||
const tools = raw
|
||||
.filter((tool: any) => tool && typeof tool.command === 'string' && typeof tool.title === 'string')
|
||||
.map((tool: any) => ({
|
||||
id: typeof tool.id === 'string' ? tool.id : undefined,
|
||||
command: tool.command,
|
||||
title: tool.title,
|
||||
description: typeof tool.description === 'string' ? tool.description : '',
|
||||
beta: Boolean(tool.beta),
|
||||
}));
|
||||
return tools.length ? tools : FALLBACK_TOOLS;
|
||||
} catch {
|
||||
return FALLBACK_TOOLS;
|
||||
}
|
||||
}
|
||||
|
||||
function getHtml(version: string, betaToolsEnabled: boolean, tools: ToolDefinition[]): string {
|
||||
const visibleTools = sortToolsForDisplay(tools.filter(tool => betaToolsEnabled || !tool.beta));
|
||||
const toolButtons = visibleTools.map(renderToolButton).join('\n');
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
@@ -460,6 +460,7 @@ ${getToolWebviewStyles()}
|
||||
|
||||
uploadBtn.addEventListener('click', () => {
|
||||
statusEl.textContent = '';
|
||||
statusEl.classList.remove('is-busy');
|
||||
resultsEl.innerHTML = '';
|
||||
outputPanel.classList.add('is-hidden');
|
||||
if (!selectedPaths.length) {
|
||||
@@ -467,6 +468,8 @@ ${getToolWebviewStyles()}
|
||||
return;
|
||||
}
|
||||
uploadBtn.disabled = true;
|
||||
statusEl.textContent = 'Uploading...';
|
||||
statusEl.classList.add('is-busy');
|
||||
resultItems = [];
|
||||
saveAllBtn.disabled = true;
|
||||
regenAllBtn.disabled = true;
|
||||
@@ -504,8 +507,9 @@ ${getToolWebviewStyles()}
|
||||
regenAllBtn.disabled = true;
|
||||
} else if (m.type === 'status') {
|
||||
statusEl.textContent = m.message;
|
||||
statusEl.className = 'status-panel';
|
||||
statusEl.className = 'status-panel is-busy';
|
||||
} else if (m.type === 'error') {
|
||||
statusEl.classList.remove('is-busy');
|
||||
const div = document.createElement('div');
|
||||
div.className = 'err';
|
||||
div.textContent = m.message;
|
||||
@@ -558,6 +562,7 @@ ${getToolWebviewStyles()}
|
||||
saveAllBtn.disabled = resultItems.length === 0;
|
||||
regenAllBtn.disabled = resultItems.length === 0;
|
||||
} else if (m.type === 'done') {
|
||||
statusEl.classList.remove('is-busy');
|
||||
statusEl.innerHTML = '<span class="ok">Done.</span>';
|
||||
uploadBtn.disabled = false;
|
||||
}
|
||||
|
||||
@@ -279,11 +279,13 @@ ${getToolWebviewStyles()}
|
||||
selectedPage = 0;
|
||||
renderSelection();
|
||||
statusEl.textContent = '';
|
||||
statusEl.classList.remove('is-busy');
|
||||
resultsEl.innerHTML = '';
|
||||
outputPanel.classList.add('is-hidden');
|
||||
});
|
||||
document.getElementById('scan').addEventListener('click', () => {
|
||||
statusEl.textContent = 'Scanning...';
|
||||
statusEl.classList.add('is-busy');
|
||||
resultsEl.innerHTML = '';
|
||||
outputPanel.classList.add('is-hidden');
|
||||
if (pickedFiles.length) {
|
||||
@@ -312,11 +314,13 @@ ${getToolWebviewStyles()}
|
||||
renderSelection();
|
||||
} else if (msg.type === 'results') {
|
||||
if (msg.error) {
|
||||
statusEl.classList.remove('is-busy');
|
||||
statusEl.textContent = 'Error: ' + msg.error;
|
||||
outputPanel.classList.add('is-hidden');
|
||||
return;
|
||||
}
|
||||
const total = msg.results.length;
|
||||
statusEl.classList.remove('is-busy');
|
||||
const flagged = msg.results.filter(r => !r.ok).length;
|
||||
statusEl.textContent = 'Scanned ' + total + ' HTML file(s). ' + flagged + ' contain non-base64 assets.';
|
||||
if (!total) {
|
||||
|
||||
@@ -796,11 +796,13 @@ ${getToolWebviewStyles()}
|
||||
selectedPage = 0;
|
||||
renderSelection();
|
||||
statusEl.textContent = '';
|
||||
statusEl.classList.remove('is-busy');
|
||||
resultsEl.innerHTML = '';
|
||||
outputPanel.classList.add('is-hidden');
|
||||
});
|
||||
document.getElementById('scan').addEventListener('click', () => {
|
||||
statusEl.textContent = 'Scanning...';
|
||||
statusEl.classList.add('is-busy');
|
||||
resultsEl.innerHTML = '';
|
||||
outputPanel.classList.add('is-hidden');
|
||||
if (pickedFiles.length) {
|
||||
@@ -829,11 +831,13 @@ ${getToolWebviewStyles()}
|
||||
renderSelection();
|
||||
} else if (msg.type === 'results') {
|
||||
if (msg.error) {
|
||||
statusEl.classList.remove('is-busy');
|
||||
statusEl.textContent = 'Error: ' + msg.error;
|
||||
outputPanel.classList.add('is-hidden');
|
||||
return;
|
||||
}
|
||||
const total = msg.results.length;
|
||||
statusEl.classList.remove('is-busy');
|
||||
const withIssues = msg.results.filter(r => !r.ok).length;
|
||||
const skipped = Number(msg.skipped || 0);
|
||||
const skippedReason = msg.skippedReason || 'file(s).';
|
||||
|
||||
@@ -569,6 +569,7 @@ ${getToolWebviewStyles()}
|
||||
const networks = [...document.querySelectorAll('.net-cb')]
|
||||
.filter(c => c.checked).map(c => c.dataset.tag);
|
||||
statusEl.textContent = 'Converting...';
|
||||
statusEl.classList.add('is-busy');
|
||||
resultsEl.innerHTML = '';
|
||||
openOutBtn.style.display = 'none';
|
||||
outputPanel.classList.add('is-hidden');
|
||||
@@ -603,11 +604,13 @@ ${getToolWebviewStyles()}
|
||||
convertBtn.disabled = false;
|
||||
updateConvertBtn();
|
||||
if (msg.error) {
|
||||
statusEl.classList.remove('is-busy');
|
||||
statusEl.textContent = 'Error: ' + msg.error;
|
||||
outputPanel.classList.add('is-hidden');
|
||||
return;
|
||||
}
|
||||
const results = msg.results || [];
|
||||
statusEl.classList.remove('is-busy');
|
||||
const ok = results.filter(r => r.ok).length;
|
||||
statusEl.textContent = ok + ' of ' + results.length + ' network(s) converted.';
|
||||
if (!results.length) {
|
||||
|
||||
@@ -392,11 +392,14 @@ ${getToolWebviewStyles()}
|
||||
document.getElementById('upload').addEventListener('click', () => {
|
||||
clearErrors();
|
||||
statusEl.textContent = '';
|
||||
statusEl.classList.remove('is-busy');
|
||||
setPreview('');
|
||||
if (rows.length === 0) {
|
||||
statusEl.innerHTML = '<span class="err">Select at least one file.</span>';
|
||||
return;
|
||||
}
|
||||
statusEl.textContent = 'Uploading...';
|
||||
statusEl.classList.add('is-busy');
|
||||
vscode.postMessage({ type: 'upload', rows });
|
||||
});
|
||||
openResultBtn.addEventListener('click', () => {
|
||||
@@ -417,19 +420,22 @@ ${getToolWebviewStyles()}
|
||||
addFiles(m.files || []);
|
||||
} else if (m.type === 'status') {
|
||||
statusEl.textContent = m.message;
|
||||
statusEl.className = 'status-panel';
|
||||
statusEl.className = 'status-panel is-busy';
|
||||
} else if (m.type === 'error') {
|
||||
statusEl.classList.remove('is-busy');
|
||||
statusEl.innerHTML = '';
|
||||
const div = document.createElement('div');
|
||||
div.className = 'err';
|
||||
div.textContent = m.message;
|
||||
statusEl.appendChild(div);
|
||||
} else if (m.type === 'rowError') {
|
||||
statusEl.classList.remove('is-busy');
|
||||
const row = rows.find(r => r.id === m.rowId);
|
||||
if (row) row.error = m.message;
|
||||
renderRows();
|
||||
statusEl.innerHTML = '<span class="err">Fix row errors and retry.</span>';
|
||||
} else if (m.type === 'result') {
|
||||
statusEl.classList.remove('is-busy');
|
||||
statusEl.innerHTML = '<span class="ok">Upload complete.</span>';
|
||||
setPreview(m.preview);
|
||||
}
|
||||
|
||||
@@ -421,6 +421,7 @@ ${getToolWebviewStyles()}
|
||||
selectedPage = 0;
|
||||
renderSelection();
|
||||
statusEl.textContent = '';
|
||||
statusEl.classList.remove('is-busy');
|
||||
});
|
||||
|
||||
startBtn.addEventListener('click', () => {
|
||||
@@ -432,6 +433,7 @@ ${getToolWebviewStyles()}
|
||||
}
|
||||
startBtn.disabled = true;
|
||||
statusEl.textContent = 'Starting server...';
|
||||
statusEl.classList.add('is-busy');
|
||||
vscode.postMessage({ type: 'startShare', paths: selectedPaths });
|
||||
});
|
||||
|
||||
@@ -548,6 +550,7 @@ ${getToolWebviewStyles()}
|
||||
selectedPage = 0;
|
||||
renderSelection();
|
||||
} else if (m.type === 'sharing') {
|
||||
statusEl.classList.remove('is-busy');
|
||||
currentUrls = m.urls;
|
||||
selectedIdx = 0;
|
||||
statusEl.innerHTML = '<span class="ok">Sharing: ' + m.filename + '</span>';
|
||||
@@ -558,6 +561,7 @@ ${getToolWebviewStyles()}
|
||||
startBtn.disabled = true;
|
||||
stopBtn.disabled = false;
|
||||
} else if (m.type === 'stopped') {
|
||||
statusEl.classList.remove('is-busy');
|
||||
statusEl.textContent = 'Stopped.';
|
||||
shareInfo.style.display = 'none';
|
||||
ifaceWrap.style.display = 'none';
|
||||
@@ -566,6 +570,7 @@ ${getToolWebviewStyles()}
|
||||
startBtn.disabled = false;
|
||||
stopBtn.disabled = true;
|
||||
} else if (m.type === 'error') {
|
||||
statusEl.classList.remove('is-busy');
|
||||
statusEl.innerHTML = '<span class="err">' + (m.message || 'Error').replace(/</g, '<') + '</span>';
|
||||
startBtn.disabled = false;
|
||||
}
|
||||
|
||||
@@ -207,6 +207,24 @@ export function getToolWebviewStyles(): string {
|
||||
padding: 8px 10px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.status-panel.is-busy {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.status-panel.is-busy::before {
|
||||
content: "";
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
flex: 0 0 auto;
|
||||
border: 2px solid color-mix(in srgb, var(--vscode-descriptionForeground) 45%, transparent);
|
||||
border-top-color: var(--vscode-foreground);
|
||||
border-radius: 50%;
|
||||
animation: status-spin 800ms linear infinite;
|
||||
}
|
||||
@keyframes status-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.status-panel:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -44,11 +44,14 @@ func ApplovinPage(w http.ResponseWriter, r *http.Request) {
|
||||
<div class="panel-header"><h3 class="panel-title">Inputs</h3></div>
|
||||
<div class="panel-body">
|
||||
<div class="control-group">
|
||||
<div id="dropZone" class="drop-zone">
|
||||
<div class="file-row">
|
||||
<button id="pickFolder" class="secondary">Select Folder</button>
|
||||
<button id="pick" class="secondary">Select File(s)</button>
|
||||
<button id="clear" class="secondary">Clear</button>
|
||||
</div>
|
||||
<div class="drop-zone-text">or drop HTML files or folders here</div>
|
||||
</div>
|
||||
<div id="fileName" class="file-name" style="margin-top:8px;">(no files selected)</div>
|
||||
<div id="fileList" class="selected-files"></div>
|
||||
</div>
|
||||
@@ -154,6 +157,15 @@ regenAllBtn.addEventListener('click', () => {
|
||||
img.src = base + '&_=' + Date.now();
|
||||
});
|
||||
});
|
||||
setupDropZone('dropZone', statusEl, (j) => {
|
||||
if (j.files && j.files.length) {
|
||||
const added = j.files.map(f => f.path).filter(p => !selectedPaths.includes(p));
|
||||
selectedPaths = selectedPaths.concat(added);
|
||||
selectedPage = Math.max(0, Math.ceil(selectedPaths.length / PAGE_SIZE) - 1);
|
||||
renderSelection();
|
||||
}
|
||||
if (j.skipped) statusEl.textContent = 'Skipped ' + j.skipped + ' non-HTML or unavailable item(s).';
|
||||
});
|
||||
|
||||
clearBtn.addEventListener('click', () => {
|
||||
selectedPaths = [];
|
||||
@@ -184,12 +196,15 @@ pickBtn.addEventListener('click', async () => {
|
||||
|
||||
uploadBtn.addEventListener('click', async () => {
|
||||
statusEl.textContent = '';
|
||||
statusEl.classList.remove('is-busy');
|
||||
resultsEl.innerHTML = '';
|
||||
if (!selectedPaths.length) {
|
||||
statusEl.innerHTML = '<span class="err">Pick at least one HTML file first.</span>';
|
||||
return;
|
||||
}
|
||||
uploadBtn.disabled = true;
|
||||
statusEl.textContent = 'Uploading...';
|
||||
statusEl.classList.add('is-busy');
|
||||
resultItems = [];
|
||||
saveAllBtn.disabled = true;
|
||||
regenAllBtn.disabled = true;
|
||||
@@ -200,6 +215,7 @@ uploadBtn.addEventListener('click', async () => {
|
||||
body: JSON.stringify({ paths: selectedPaths }),
|
||||
});
|
||||
if (!res.ok || !res.body) {
|
||||
statusEl.classList.remove('is-busy');
|
||||
statusEl.innerHTML = '<span class="err">Upload failed.</span>';
|
||||
uploadBtn.disabled = false;
|
||||
return;
|
||||
@@ -219,6 +235,7 @@ uploadBtn.addEventListener('click', async () => {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
statusEl.classList.remove('is-busy');
|
||||
statusEl.innerHTML = '<span class="err">' + (e.message || e) + '</span>';
|
||||
} finally {
|
||||
uploadBtn.disabled = false;
|
||||
@@ -240,6 +257,7 @@ qrModal.addEventListener('click', () => qrModal.classList.remove('open'));
|
||||
function handle(m) {
|
||||
if (m.type === 'status') {
|
||||
statusEl.textContent = m.message;
|
||||
statusEl.classList.add('is-busy');
|
||||
} else if (m.type === 'fileError') {
|
||||
const tbody = ensureResultsTable();
|
||||
resultsEl.querySelector('table').classList.add('has-errors');
|
||||
@@ -296,6 +314,7 @@ function handle(m) {
|
||||
saveAllBtn.disabled = resultItems.length === 0;
|
||||
regenAllBtn.disabled = resultItems.length === 0;
|
||||
} else if (m.type === 'done') {
|
||||
statusEl.classList.remove('is-busy');
|
||||
statusEl.innerHTML = '<span class="ok">Done.</span>';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,11 +21,14 @@ func Base64Page(w http.ResponseWriter, r *http.Request) {
|
||||
<div class="panel-header"><h3 class="panel-title">Inputs</h3></div>
|
||||
<div class="panel-body">
|
||||
<div class="control-group">
|
||||
<div id="dropZone" class="drop-zone">
|
||||
<div class="file-row">
|
||||
<button id="pickFolder" class="secondary">Select Folder</button>
|
||||
<button id="pickFiles" class="secondary">Select File(s)</button>
|
||||
<button id="clear" class="secondary">Clear</button>
|
||||
</div>
|
||||
<div class="drop-zone-text">or drop HTML files or folders here</div>
|
||||
</div>
|
||||
<div id="selection" class="selected-list"></div>
|
||||
</div>
|
||||
<div class="action-row"><button id="scan">Scan</button></div>
|
||||
@@ -87,6 +90,10 @@ function addPaths(paths) {
|
||||
renderSelection();
|
||||
}
|
||||
renderSelection();
|
||||
setupDropZone('dropZone', statusEl, (j) => {
|
||||
addPaths(j.paths || []);
|
||||
if (j.skipped) statusEl.textContent = 'Skipped ' + j.skipped + ' non-HTML or unavailable item(s).';
|
||||
});
|
||||
|
||||
document.getElementById('pickFolder').addEventListener('click', async () => {
|
||||
const r = await fetch('/api/base64/pickFolder', { method: 'POST' });
|
||||
@@ -102,19 +109,22 @@ document.getElementById('clear').addEventListener('click', () => {
|
||||
pickedFiles = [];
|
||||
selectedPage = 0;
|
||||
statusEl.textContent = '';
|
||||
statusEl.classList.remove('is-busy');
|
||||
resultsEl.innerHTML = '';
|
||||
renderSelection();
|
||||
});
|
||||
document.getElementById('scan').addEventListener('click', async () => {
|
||||
statusEl.textContent = 'Scanning...';
|
||||
statusEl.classList.add('is-busy');
|
||||
resultsEl.innerHTML = '';
|
||||
if (!pickedFiles.length) { statusEl.textContent = 'Pick files first.'; return; }
|
||||
if (!pickedFiles.length) { statusEl.classList.remove('is-busy'); statusEl.textContent = 'Pick files first.'; return; }
|
||||
const payload = { files: pickedFiles };
|
||||
const r = await fetch('/api/base64/scan', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload) });
|
||||
const j = await r.json();
|
||||
if (j.error) { statusEl.textContent = 'Error: ' + j.error; return; }
|
||||
if (j.error) { statusEl.classList.remove('is-busy'); statusEl.textContent = 'Error: ' + j.error; return; }
|
||||
const total = j.results.length;
|
||||
const flagged = j.results.filter(r => !r.ok).length;
|
||||
statusEl.classList.remove('is-busy');
|
||||
statusEl.textContent = 'Scanned ' + total + ' HTML file(s). ' + flagged + ' contain non-base64 assets.';
|
||||
if (!total) { resultsEl.innerHTML = ''; return; }
|
||||
resultsEl.innerHTML = '<table class="data-table"><thead><tr><th style="width:90px;">Status</th><th>File</th><th style="width:90px;">Issues</th><th>Non-base64 assets</th></tr></thead><tbody></tbody></table>';
|
||||
|
||||
@@ -2,8 +2,13 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const SharedCSS = `
|
||||
@@ -107,6 +112,9 @@ const SharedCSS = `
|
||||
button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.status-panel, .results-panel, .result-card { border: 1px solid var(--tool-border); border-radius: var(--tool-radius); background: rgba(128,128,128,0.08); }
|
||||
.status-panel { min-height: 30px; margin-top: var(--tool-gap-md); padding: 8px 10px; white-space: pre-wrap; }
|
||||
.status-panel.is-busy { display: flex; align-items: center; gap: 8px; }
|
||||
.status-panel.is-busy::before { content: ""; width: 12px; height: 12px; flex: 0 0 auto; border: 2px solid rgba(167,167,167,0.35); border-top-color: #ddd; border-radius: 50%; animation: status-spin 800ms linear infinite; }
|
||||
@keyframes status-spin { to { transform: rotate(360deg); } }
|
||||
.status-panel:empty { display: none; }
|
||||
.results-panel { margin-top: var(--tool-gap-md); overflow: auto; }
|
||||
.results-panel:empty { display: none; }
|
||||
@@ -118,6 +126,28 @@ const SharedCSS = `
|
||||
.mono { font-family: Consolas, "Courier New", monospace; font-size: 12px; }
|
||||
.wrap { overflow-wrap: anywhere; word-break: break-word; }
|
||||
.file-name { color: #a7a7a7; font-size: 12px; overflow-wrap: anywhere; }
|
||||
.drop-zone {
|
||||
margin-top: 8px;
|
||||
min-height: 44px;
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
border: 1px dashed #444;
|
||||
border-radius: var(--tool-radius);
|
||||
color: #a7a7a7;
|
||||
background: rgba(128,128,128,0.08);
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
transition: border-color 120ms ease, background-color 120ms ease, color 120ms ease;
|
||||
}
|
||||
.drop-zone .file-row { justify-content: center; }
|
||||
.drop-zone-text { color: #a7a7a7; font-size: 12px; }
|
||||
.drop-zone.is-dragover {
|
||||
border-color: #007fd4;
|
||||
color: #ddd;
|
||||
background: rgba(0,127,212,0.12);
|
||||
}
|
||||
.selected-list, .selected-files { margin-top: 8px; }
|
||||
.remove-selected, .remove-btn { min-width: 28px; width: 28px; padding: 3px 0; color: #f48771; font-size: 16px; line-height: 1; }
|
||||
.pager { margin-top: 8px; display: flex; align-items: center; gap: 8px; justify-content: flex-end; }
|
||||
@@ -131,6 +161,160 @@ const SharedCSS = `
|
||||
.hint { font-size: 12px; opacity: 0.7; }
|
||||
`
|
||||
|
||||
const SharedDropZoneScript = `
|
||||
function extractDroppedPaths(dataTransfer) {
|
||||
const paths = [];
|
||||
const add = (value) => {
|
||||
if (!value) return;
|
||||
const trimmed = String(value).trim();
|
||||
if (trimmed && !paths.includes(trimmed)) paths.push(trimmed);
|
||||
};
|
||||
const addPathText = (text) => {
|
||||
const value = String(text || '');
|
||||
if (!value) return;
|
||||
const uriMatches = value.match(/file:\/\/(?:\/[A-Za-z]:)?[^\\r\\n\\0]+/gi) || [];
|
||||
uriMatches.forEach(add);
|
||||
const quotedMatches = value.match(/"([^"]+)"|'([^']+)'/g) || [];
|
||||
quotedMatches.forEach(match => add(match.replace(/^["']|["']$/g, '')));
|
||||
value.split(/[\r\n\0]+/).forEach(line => {
|
||||
let rest = line.trim();
|
||||
if (!rest || rest.startsWith('#')) return;
|
||||
while (rest.length) {
|
||||
const driveMatches = [...rest.matchAll(/[A-Za-z]:\\/g)].map(m => m.index).filter(i => typeof i === 'number');
|
||||
if (driveMatches.length <= 1) {
|
||||
add(rest);
|
||||
break;
|
||||
}
|
||||
const next = driveMatches[1];
|
||||
add(rest.slice(0, next).trim());
|
||||
rest = rest.slice(next).trim();
|
||||
}
|
||||
});
|
||||
};
|
||||
const addLines = (text) => {
|
||||
addPathText(text);
|
||||
};
|
||||
try { addLines(dataTransfer.getData('text/uri-list')); } catch {}
|
||||
try { addLines(dataTransfer.getData('text/plain')); } catch {}
|
||||
for (const file of Array.from(dataTransfer.files || [])) {
|
||||
add(file.path);
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
async function extractDroppedHtmlFiles(dataTransfer) {
|
||||
const files = [];
|
||||
const seen = new Set();
|
||||
const droppedFiles = Array.from(dataTransfer.files || []);
|
||||
const entries = [];
|
||||
const handlePromises = [];
|
||||
for (const item of Array.from(dataTransfer.items || [])) {
|
||||
if (typeof item.getAsFileSystemHandle === 'function') {
|
||||
handlePromises.push(item.getAsFileSystemHandle().catch(() => null));
|
||||
}
|
||||
let entry = typeof item.webkitGetAsEntry === 'function' ? item.webkitGetAsEntry() : null;
|
||||
if (!entry && typeof item.getAsEntry === 'function') entry = item.getAsEntry();
|
||||
if (entry) entries.push(entry);
|
||||
}
|
||||
const addFile = async (file, name) => {
|
||||
const fileName = name || file.name || 'dropped.html';
|
||||
if (!/\.html?$/i.test(fileName)) return;
|
||||
const key = fileName + ':' + file.size + ':' + file.lastModified;
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
files.push({ name: fileName, content: await file.text() });
|
||||
};
|
||||
const readEntry = async (entry, prefix) => {
|
||||
if (!entry) return;
|
||||
if (entry.isFile) {
|
||||
await new Promise((resolve) => {
|
||||
entry.file(async (file) => {
|
||||
await addFile(file, (prefix || '') + file.name);
|
||||
resolve();
|
||||
}, () => resolve());
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (entry.isDirectory) {
|
||||
const reader = entry.createReader();
|
||||
while (true) {
|
||||
const entries = await new Promise(resolve => reader.readEntries(resolve, () => resolve([])));
|
||||
if (!entries.length) break;
|
||||
for (const child of entries) await readEntry(child, (prefix || '') + entry.name + '/');
|
||||
}
|
||||
}
|
||||
};
|
||||
const readHandle = async (handle, prefix) => {
|
||||
if (!handle) return;
|
||||
if (handle.kind === 'file') {
|
||||
try {
|
||||
const file = await handle.getFile();
|
||||
await addFile(file, (prefix || '') + file.name);
|
||||
} catch {}
|
||||
return;
|
||||
}
|
||||
if (handle.kind === 'directory') {
|
||||
try {
|
||||
for await (const child of handle.values()) {
|
||||
await readHandle(child, (prefix || '') + handle.name + '/');
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
};
|
||||
for (const file of droppedFiles) await addFile(file, file.name);
|
||||
for (const entry of entries) await readEntry(entry, '');
|
||||
for (const handle of await Promise.all(handlePromises)) await readHandle(handle, '');
|
||||
return files;
|
||||
}
|
||||
|
||||
function setupDropZone(id, statusElement, onResolved) {
|
||||
const zone = document.getElementById(id);
|
||||
if (!zone) return;
|
||||
const fallback = 'This view could not read dropped file paths. Use Select File(s) or Select Folder instead.';
|
||||
const setMessage = (text) => {
|
||||
if (statusElement) statusElement.textContent = text || '';
|
||||
};
|
||||
['dragenter', 'dragover'].forEach(type => {
|
||||
zone.addEventListener(type, (event) => {
|
||||
if (event.target && event.target.closest && event.target.closest('button, input')) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
zone.classList.add('is-dragover');
|
||||
});
|
||||
});
|
||||
['dragleave', 'dragend'].forEach(type => {
|
||||
zone.addEventListener(type, (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
zone.classList.remove('is-dragover');
|
||||
});
|
||||
});
|
||||
zone.addEventListener('drop', async (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
zone.classList.remove('is-dragover');
|
||||
const paths = extractDroppedPaths(event.dataTransfer);
|
||||
const files = await extractDroppedHtmlFiles(event.dataTransfer);
|
||||
if (!paths.length && !files.length) {
|
||||
setMessage(fallback);
|
||||
return;
|
||||
}
|
||||
setMessage('');
|
||||
try {
|
||||
const r = await fetch('/api/drop/resolve', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ paths, files }),
|
||||
});
|
||||
const j = await r.json();
|
||||
if (typeof onResolved === 'function') onResolved(j);
|
||||
} catch (e) {
|
||||
setMessage('Could not read dropped items: ' + (e.message || e));
|
||||
}
|
||||
});
|
||||
}
|
||||
`
|
||||
|
||||
type navItem struct {
|
||||
Path string
|
||||
Label string
|
||||
@@ -143,8 +327,8 @@ var navItems = []navItem{
|
||||
{Path: "/plec", Label: "PLEC Upload", Description: "Upload HTML to the internal PLEC server"},
|
||||
{Path: "/applovin", Label: "AppLovin Playable Preview", Description: "Upload to p.applov.in (QR preview)"},
|
||||
{Path: "/base64", Label: "Base64 Scanner", Description: "Find non-base64 assets in HTML"},
|
||||
{Path: "/mraid", Label: "MRAID Checker", Description: "Check MRAID requirements and best practices", Beta: true},
|
||||
{Path: "/mobile", Label: "Send To Mobile", Description: "Share HTML to a phone via LAN + QR"},
|
||||
{Path: "/mraid", Label: "MRAID Checker", Description: "Check MRAID requirements and best practices"},
|
||||
{Path: "/playworks", Label: "Playworks Converter", Description: "Convert Playworks HTML to per-network variants", Beta: true},
|
||||
}
|
||||
|
||||
@@ -207,6 +391,7 @@ func Page(activePath, title, body string) string {
|
||||
<meta charset="UTF-8" />
|
||||
<title>` + title + ` — HPL Toolbox</title>
|
||||
<style>` + SharedCSS + `</style>
|
||||
<script>` + SharedDropZoneScript + `</script>
|
||||
</head><body>
|
||||
<div class="topbar">` + tabs.String() + `<span class="topbar-spacer"></span></div>
|
||||
<div class="content">` + body + `</div>
|
||||
@@ -242,6 +427,153 @@ func boolAttr(v bool) string {
|
||||
return "false"
|
||||
}
|
||||
|
||||
func DropResolveEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
type droppedFile struct {
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
var req struct {
|
||||
Paths []string `json:"paths"`
|
||||
Files []droppedFile `json:"files"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, map[string]any{"files": []any{}, "paths": []string{}, "skipped": 0, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
seen := map[string]bool{}
|
||||
var outPaths []string
|
||||
skipped := 0
|
||||
addFile := func(p string) {
|
||||
if !isHTMLPath(p) {
|
||||
skipped++
|
||||
return
|
||||
}
|
||||
key := strings.ToLower(filepath.Clean(p))
|
||||
if seen[key] {
|
||||
return
|
||||
}
|
||||
seen[key] = true
|
||||
outPaths = append(outPaths, p)
|
||||
}
|
||||
|
||||
for _, raw := range req.Paths {
|
||||
p := normalizeDroppedPath(raw)
|
||||
if p == "" {
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
info, err := os.Stat(p)
|
||||
if err != nil {
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
if info.IsDir() {
|
||||
for _, filePath := range collectHTMLFiles(p) {
|
||||
addFile(filePath)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if info.Mode().IsRegular() {
|
||||
addFile(p)
|
||||
continue
|
||||
}
|
||||
skipped++
|
||||
}
|
||||
|
||||
if len(req.Files) > 0 {
|
||||
cleanupStaleStandaloneDropDirs()
|
||||
dir, err := os.MkdirTemp("", "hpltoolbox-drop-")
|
||||
if err != nil {
|
||||
skipped += len(req.Files)
|
||||
} else {
|
||||
for _, file := range req.Files {
|
||||
if !isHTMLPath(file.Name) {
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
name := safeStandaloneDropName(file.Name)
|
||||
target := filepath.Join(dir, name)
|
||||
ext := filepath.Ext(name)
|
||||
base := strings.TrimSuffix(name, ext)
|
||||
for i := 1; fileExists(target); i++ {
|
||||
target = filepath.Join(dir, fmt.Sprintf("%s_%d%s", base, i, ext))
|
||||
}
|
||||
if err := os.WriteFile(target, []byte(file.Content), 0644); err != nil {
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
addFile(target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
files := make([]map[string]string, 0, len(outPaths))
|
||||
for _, p := range outPaths {
|
||||
files = append(files, map[string]string{"path": p, "name": filepath.Base(p)})
|
||||
}
|
||||
writeJSON(w, map[string]any{"files": files, "paths": outPaths, "skipped": skipped})
|
||||
}
|
||||
|
||||
func safeStandaloneDropName(name string) string {
|
||||
base := filepath.Base(name)
|
||||
base = strings.Map(func(r rune) rune {
|
||||
switch r {
|
||||
case '<', '>', ':', '"', '/', '\\', '|', '?', '*':
|
||||
return '_'
|
||||
default:
|
||||
if r < 32 {
|
||||
return '_'
|
||||
}
|
||||
return r
|
||||
}
|
||||
}, base)
|
||||
base = strings.TrimSpace(base)
|
||||
if base == "" {
|
||||
return "dropped.html"
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
func cleanupStaleStandaloneDropDirs() {
|
||||
root := os.TempDir()
|
||||
entries, err := os.ReadDir(root)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cutoff := time.Now().Add(-24 * time.Hour)
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() || !strings.HasPrefix(entry.Name(), "hpltoolbox-drop-") {
|
||||
continue
|
||||
}
|
||||
dir := filepath.Join(root, entry.Name())
|
||||
info, err := entry.Info()
|
||||
if err == nil && info.ModTime().Before(cutoff) {
|
||||
_ = os.RemoveAll(dir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeDroppedPath(value string) string {
|
||||
result := strings.Trim(strings.TrimSpace(value), `"'`)
|
||||
if strings.HasPrefix(strings.ToLower(result), "file://") {
|
||||
result = strings.TrimPrefix(result, "file://")
|
||||
if decoded, err := url.PathUnescape(result); err == nil {
|
||||
result = decoded
|
||||
}
|
||||
if len(result) >= 3 && result[0] == '/' && result[2] == ':' {
|
||||
result = result[1:]
|
||||
}
|
||||
result = filepath.FromSlash(result)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func isHTMLPath(p string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(p))
|
||||
return ext == ".html" || ext == ".htm"
|
||||
}
|
||||
|
||||
func BetaToolsEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
@@ -121,6 +121,7 @@ func buildMux() *http.ServeMux {
|
||||
mux.HandleFunc("POST /api/open", OpenEndpoint)
|
||||
mux.HandleFunc("POST /api/focus", FocusEndpoint)
|
||||
mux.HandleFunc("POST /api/betaTools", BetaToolsEndpoint)
|
||||
mux.HandleFunc("POST /api/drop/resolve", DropResolveEndpoint)
|
||||
|
||||
// PLEC
|
||||
mux.HandleFunc("POST /api/plec/pick", PlecPick)
|
||||
|
||||
@@ -67,11 +67,14 @@ func MobilePage(w http.ResponseWriter, r *http.Request) {
|
||||
<div class="panel-header"><h3 class="panel-title">Inputs</h3></div>
|
||||
<div class="panel-body">
|
||||
<div class="control-group">
|
||||
<div id="dropZone" class="drop-zone">
|
||||
<div class="file-row">
|
||||
<button id="pickFolder" class="secondary">Select Folder</button>
|
||||
<button id="pick" class="secondary">Select File(s)</button>
|
||||
<button id="clear" class="secondary">Clear</button>
|
||||
</div>
|
||||
<div class="drop-zone-text">or drop HTML files or folders here</div>
|
||||
</div>
|
||||
<div id="fileName" class="file-name" style="margin-top:8px;">(no files selected)</div>
|
||||
<div id="fileList" class="selected-list"></div>
|
||||
</div>
|
||||
@@ -165,6 +168,10 @@ function addFiles(files) {
|
||||
renderSelection();
|
||||
}
|
||||
renderSelection();
|
||||
setupDropZone('dropZone', statusEl, (j) => {
|
||||
addFiles(j.files || []);
|
||||
if (j.skipped) statusEl.textContent = 'Skipped ' + j.skipped + ' non-HTML or unavailable item(s).';
|
||||
});
|
||||
|
||||
pickFolderBtn.addEventListener('click', async () => {
|
||||
const r = await fetch('/api/mobile/pickFolder', { method:'POST' });
|
||||
@@ -181,6 +188,7 @@ clearBtn.addEventListener('click', () => {
|
||||
selectedPaths = [];
|
||||
selectedPage = 0;
|
||||
statusEl.textContent = '';
|
||||
statusEl.classList.remove('is-busy');
|
||||
renderSelection();
|
||||
});
|
||||
|
||||
@@ -192,17 +200,20 @@ startBtn.addEventListener('click', async () => {
|
||||
}
|
||||
startBtn.disabled = true;
|
||||
statusEl.textContent = 'Starting server...';
|
||||
statusEl.classList.add('is-busy');
|
||||
const r = await fetch('/api/mobile/start', {
|
||||
method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ paths: selectedPaths }),
|
||||
});
|
||||
const j = await r.json();
|
||||
if (j.error) {
|
||||
statusEl.classList.remove('is-busy');
|
||||
statusEl.innerHTML = '<span class="err">' + j.error + '</span>';
|
||||
startBtn.disabled = false;
|
||||
return;
|
||||
}
|
||||
currentUrls = j.urls;
|
||||
statusEl.classList.remove('is-busy');
|
||||
selectedIdx = 0;
|
||||
statusEl.innerHTML = '<span class="ok">Sharing: ' + j.filename + '</span>';
|
||||
shareInfo.style.display = '';
|
||||
@@ -213,6 +224,7 @@ startBtn.addEventListener('click', async () => {
|
||||
|
||||
stopBtn.addEventListener('click', async () => {
|
||||
await fetch('/api/mobile/stop', { method:'POST' });
|
||||
statusEl.classList.remove('is-busy');
|
||||
statusEl.textContent = 'Stopped.';
|
||||
shareInfo.style.display = 'none';
|
||||
currentUrls = [];
|
||||
|
||||
@@ -50,11 +50,14 @@ func MraidPage(w http.ResponseWriter, r *http.Request) {
|
||||
<div class="panel-header"><h3 class="panel-title">Inputs</h3></div>
|
||||
<div class="panel-body">
|
||||
<div class="control-group">
|
||||
<div id="dropZone" class="drop-zone">
|
||||
<div class="file-row">
|
||||
<button id="pickFolder" class="secondary">Select Folder</button>
|
||||
<button id="pickFiles" class="secondary">Select File(s)</button>
|
||||
<button id="clear" class="secondary">Clear</button>
|
||||
</div>
|
||||
<div class="drop-zone-text">or drop HTML files or folders here</div>
|
||||
</div>
|
||||
<div id="selection" class="selected-list"></div>
|
||||
</div>
|
||||
<div class="action-row"><button id="scan">Scan</button></div>
|
||||
@@ -129,6 +132,10 @@ function addPaths(paths) {
|
||||
renderSelection();
|
||||
}
|
||||
renderSelection();
|
||||
setupDropZone('dropZone', statusEl, (j) => {
|
||||
addPaths(j.paths || []);
|
||||
if (j.skipped) statusEl.textContent = 'Skipped ' + j.skipped + ' non-HTML or unavailable item(s).';
|
||||
});
|
||||
|
||||
document.getElementById('pickFolder').addEventListener('click', async () => {
|
||||
const r = await fetch('/api/mraid/pickFolder', { method: 'POST' });
|
||||
@@ -144,17 +151,19 @@ document.getElementById('clear').addEventListener('click', () => {
|
||||
pickedFiles = [];
|
||||
selectedPage = 0;
|
||||
statusEl.textContent = '';
|
||||
statusEl.classList.remove('is-busy');
|
||||
resultsEl.innerHTML = '';
|
||||
renderSelection();
|
||||
});
|
||||
document.getElementById('scan').addEventListener('click', async () => {
|
||||
statusEl.textContent = 'Scanning...';
|
||||
statusEl.classList.add('is-busy');
|
||||
resultsEl.innerHTML = '';
|
||||
if (!pickedFiles.length) { statusEl.textContent = 'Pick files first.'; return; }
|
||||
if (!pickedFiles.length) { statusEl.classList.remove('is-busy'); statusEl.textContent = 'Pick files first.'; return; }
|
||||
const payload = { files: pickedFiles };
|
||||
const r = await fetch('/api/mraid/scan', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload) });
|
||||
const j = await r.json();
|
||||
if (j.error) { statusEl.textContent = 'Error: ' + j.error; return; }
|
||||
if (j.error) { statusEl.classList.remove('is-busy'); statusEl.textContent = 'Error: ' + j.error; return; }
|
||||
const total = j.results.length;
|
||||
const withIssues = j.results.filter(r => !r.ok).length;
|
||||
const requirements = j.results.reduce((n, r) => n + r.issues.filter(i => i.severity === 'requirement').length, 0);
|
||||
@@ -162,6 +171,7 @@ document.getElementById('scan').addEventListener('click', async () => {
|
||||
const skipped = Number(j.skipped || 0);
|
||||
const skippedReason = j.skippedReason || 'file(s).';
|
||||
const skippedText = skipped ? ' Skipped ' + skipped + ' ' + skippedReason : '';
|
||||
statusEl.classList.remove('is-busy');
|
||||
statusEl.textContent = 'Scanned ' + total + ' HTML file(s). ' + withIssues + ' file(s) need review.' + skippedText;
|
||||
if (!total) { resultsEl.innerHTML = ''; return; }
|
||||
resultsEl.innerHTML = '<table class="data-table"><thead><tr><th>File</th><th>Results</th></tr></thead><tbody></tbody></table>';
|
||||
|
||||
@@ -47,10 +47,15 @@ func PlayworksPage(w http.ResponseWriter, r *http.Request) {
|
||||
<div class="panel-body">
|
||||
<div class="control-group">
|
||||
<div class="control-label">Source HTML (Playworks export)</div>
|
||||
<div class="field-row">
|
||||
<input id="src" type="text" placeholder="Path to Playworks HTML..." readonly style="flex:1;" />
|
||||
<div id="dropZone" class="drop-zone">
|
||||
<div class="file-row">
|
||||
<button id="pickSrc" class="secondary">Select File</button>
|
||||
<button id="clearSrc" class="secondary">Clear</button>
|
||||
</div>
|
||||
<div class="drop-zone-text">or drop a Playworks HTML file here</div>
|
||||
</div>
|
||||
<input id="src" type="hidden" />
|
||||
<div id="sourceSelection" class="selected-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
@@ -106,22 +111,59 @@ const convertBtn = document.getElementById('convert');
|
||||
const openOutBtn = document.getElementById('openOut');
|
||||
const statusEl = document.getElementById('status');
|
||||
const resultsEl = document.getElementById('results');
|
||||
const sourceSelectionEl = document.getElementById('sourceSelection');
|
||||
|
||||
function updateConvertBtn() {
|
||||
const hasNet = [...document.querySelectorAll('.net-cb')].some(c => c.checked);
|
||||
convertBtn.disabled = !srcEl.value || !outEl.value || !baseNameEl.value.trim() || !hasNet;
|
||||
}
|
||||
|
||||
function basename(p) {
|
||||
const i = Math.max(p.lastIndexOf('/'), p.lastIndexOf('\\'));
|
||||
return i >= 0 ? p.slice(i + 1) : p;
|
||||
}
|
||||
|
||||
function escapeHtml(s){
|
||||
return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
}
|
||||
|
||||
function renderSourceSelection() {
|
||||
if (!srcEl.value) {
|
||||
sourceSelectionEl.innerHTML = '<span class="file-name">(no file selected)</span>';
|
||||
return;
|
||||
}
|
||||
sourceSelectionEl.innerHTML = '<div class="results-panel" style="margin-top:0;"><table class="data-table">' +
|
||||
'<thead><tr><th>Filename</th><th style="width:44px;"></th></tr></thead><tbody>' +
|
||||
'<tr><td class="mono wrap">' + escapeHtml(basename(srcEl.value)) + '</td><td><button id="removeSource" class="remove-selected danger" title="Remove">×</button></td></tr>' +
|
||||
'</tbody></table></div>';
|
||||
document.getElementById('removeSource').addEventListener('click', clearSource);
|
||||
}
|
||||
|
||||
function setSource(path, baseName, dir) {
|
||||
srcEl.value = path || '';
|
||||
baseNameEl.value = baseName || '';
|
||||
if (!outEl.value && dir) { outEl.value = dir.replace(/[\\\/]+$/, '') + '\\\\Output'; outputDir = outEl.value; }
|
||||
renderSourceSelection();
|
||||
updateConvertBtn();
|
||||
}
|
||||
|
||||
function clearSource() {
|
||||
srcEl.value = '';
|
||||
baseNameEl.value = '';
|
||||
statusEl.textContent = '';
|
||||
statusEl.classList.remove('is-busy');
|
||||
renderSourceSelection();
|
||||
updateConvertBtn();
|
||||
}
|
||||
|
||||
document.getElementById('pickSrc').addEventListener('click', async () => {
|
||||
const r = await fetch('/api/playworks/pickSource', { method:'POST' });
|
||||
const j = await r.json();
|
||||
if (j.path) {
|
||||
srcEl.value = j.path;
|
||||
baseNameEl.value = j.baseName || '';
|
||||
if (!outEl.value && j.dir) { outEl.value = j.dir; outputDir = j.dir; }
|
||||
updateConvertBtn();
|
||||
setSource(j.path, j.baseName || '', j.dir || '');
|
||||
}
|
||||
});
|
||||
document.getElementById('clearSrc').addEventListener('click', clearSource);
|
||||
document.getElementById('pickOut').addEventListener('click', async () => {
|
||||
const r = await fetch('/api/playworks/pickOutput', { method:'POST' });
|
||||
const j = await r.json();
|
||||
@@ -146,10 +188,25 @@ function outputDisplayName(file) {
|
||||
const parts = file.split(/[\\\\/]/).filter(Boolean);
|
||||
return parts.slice(-2).join('/');
|
||||
}
|
||||
setupDropZone('dropZone', statusEl, (j) => {
|
||||
const paths = j.paths || [];
|
||||
if (!paths.length) {
|
||||
if (j.skipped) statusEl.textContent = 'Skipped ' + j.skipped + ' non-HTML or unavailable item(s).';
|
||||
return;
|
||||
}
|
||||
const sourcePath = paths[0];
|
||||
const droppedBase = basename(sourcePath).replace(/\\.html?$/i, '').replace(/_(?:un|al|is|fb|gg|ap|mo|vu|mtg|tt)$/i, '');
|
||||
const i = Math.max(sourcePath.lastIndexOf('/'), sourcePath.lastIndexOf('\\\\'));
|
||||
setSource(sourcePath, droppedBase, i >= 0 ? sourcePath.slice(0, i) : '');
|
||||
const skipped = Number(j.skipped || 0) + Math.max(0, paths.length - 1);
|
||||
if (skipped) statusEl.textContent = 'Using first dropped HTML file. Skipped ' + skipped + ' other item(s).';
|
||||
});
|
||||
renderSourceSelection();
|
||||
|
||||
document.getElementById('convert').addEventListener('click', async () => {
|
||||
const networks = [...document.querySelectorAll('.net-cb')].filter(c => c.checked).map(c => c.dataset.tag);
|
||||
statusEl.textContent = 'Converting...';
|
||||
statusEl.classList.add('is-busy');
|
||||
resultsEl.innerHTML = '';
|
||||
openOutBtn.disabled = true;
|
||||
convertBtn.disabled = true;
|
||||
@@ -161,9 +218,10 @@ document.getElementById('convert').addEventListener('click', async () => {
|
||||
const msg = await res.json();
|
||||
convertBtn.disabled = false;
|
||||
updateConvertBtn();
|
||||
if (msg.error) { statusEl.textContent = 'Error: ' + msg.error; return; }
|
||||
if (msg.error) { statusEl.classList.remove('is-busy'); statusEl.textContent = 'Error: ' + msg.error; return; }
|
||||
const results = msg.results || [];
|
||||
const ok = results.filter(r => r.ok).length;
|
||||
statusEl.classList.remove('is-busy');
|
||||
statusEl.textContent = ok + ' of ' + results.length + ' network(s) converted.';
|
||||
if (!results.length) { resultsEl.innerHTML = ''; return; }
|
||||
resultsEl.innerHTML = '<table class="data-table"><thead><tr><th style="width:120px;">Network</th><th style="width:90px;">Status</th><th>Output Path</th><th>Error</th></tr></thead><tbody></tbody></table>';
|
||||
|
||||
@@ -26,11 +26,14 @@ func PlecPage(w http.ResponseWriter, r *http.Request) {
|
||||
<div class="panel-header"><h3 class="panel-title">Inputs</h3></div>
|
||||
<div class="panel-body">
|
||||
<div class="control-group">
|
||||
<div id="dropZone" class="drop-zone">
|
||||
<div class="file-row">
|
||||
<button id="pickFolder" class="secondary">Select Folder</button>
|
||||
<button id="pickFiles" class="secondary">Select File(s)</button>
|
||||
<button id="clear" class="secondary">Clear</button>
|
||||
</div>
|
||||
<div class="drop-zone-text">or drop HTML files or folders here</div>
|
||||
</div>
|
||||
<div id="emptySelection" class="file-name" style="margin-top:8px;">(no files selected)</div>
|
||||
<div id="rowsPanel" class="results-panel is-hidden" style="margin-top:8px;">
|
||||
<table id="rows" class="data-table">
|
||||
@@ -138,6 +141,10 @@ function setPreview(url) {
|
||||
openResultBtn.disabled = !previewUrl;
|
||||
copyResultBtn.disabled = !previewUrl;
|
||||
}
|
||||
setupDropZone('dropZone', statusEl, (j) => {
|
||||
addFiles(j.files || []);
|
||||
if (j.skipped) statusEl.textContent = 'Skipped ' + j.skipped + ' non-HTML or unavailable item(s).';
|
||||
});
|
||||
|
||||
document.getElementById('pickFolder').addEventListener('click', async () => {
|
||||
const res = await fetch('/api/plec/pickFolder', { method: 'POST' });
|
||||
@@ -154,6 +161,7 @@ document.getElementById('clear').addEventListener('click', () => {
|
||||
page = 0;
|
||||
setPreview('');
|
||||
statusEl.textContent = '';
|
||||
statusEl.classList.remove('is-busy');
|
||||
renderRows();
|
||||
});
|
||||
openResultBtn.addEventListener('click', () => {
|
||||
@@ -167,12 +175,14 @@ document.getElementById('upload').addEventListener('click', async () => {
|
||||
rows.forEach(row => row.error = '');
|
||||
renderRows();
|
||||
statusEl.textContent = '';
|
||||
statusEl.classList.remove('is-busy');
|
||||
setPreview('');
|
||||
if (rows.length === 0) {
|
||||
statusEl.innerHTML = '<span class="err">Select at least one file.</span>';
|
||||
return;
|
||||
}
|
||||
statusEl.textContent = 'Uploading...';
|
||||
statusEl.classList.add('is-busy');
|
||||
const res = await fetch('/api/plec/upload', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -180,6 +190,7 @@ document.getElementById('upload').addEventListener('click', async () => {
|
||||
});
|
||||
const j = await res.json();
|
||||
if (j.rowErrors) {
|
||||
statusEl.classList.remove('is-busy');
|
||||
for (const re of j.rowErrors) {
|
||||
const row = rows.find(r => r.id === re.id);
|
||||
if (row) row.error = re.message;
|
||||
@@ -189,9 +200,11 @@ document.getElementById('upload').addEventListener('click', async () => {
|
||||
return;
|
||||
}
|
||||
if (j.error) {
|
||||
statusEl.classList.remove('is-busy');
|
||||
statusEl.innerHTML = '<span class="err">' + j.error + '</span>';
|
||||
return;
|
||||
}
|
||||
statusEl.classList.remove('is-busy');
|
||||
statusEl.innerHTML = '<span class="ok">Upload complete.</span>';
|
||||
setPreview(j.preview);
|
||||
});
|
||||
|
||||
50
tools.json
Normal file
50
tools.json
Normal file
@@ -0,0 +1,50 @@
|
||||
[
|
||||
{
|
||||
"id": "plec",
|
||||
"title": "PLEC Upload",
|
||||
"description": "Upload HTML to internal PLEC server",
|
||||
"command": "hplToolbox.openPlecUpload",
|
||||
"path": "/plec",
|
||||
"beta": false
|
||||
},
|
||||
{
|
||||
"id": "applovin",
|
||||
"title": "AppLovin Playable Preview",
|
||||
"description": "Upload to p.applov.in (QR preview)",
|
||||
"command": "hplToolbox.openApplovinUpload",
|
||||
"path": "/applovin",
|
||||
"beta": false
|
||||
},
|
||||
{
|
||||
"id": "base64",
|
||||
"title": "Base64 Scanner",
|
||||
"description": "Find non-base64 assets in HTML",
|
||||
"command": "hplToolbox.openBase64Scanner",
|
||||
"path": "/base64",
|
||||
"beta": false
|
||||
},
|
||||
{
|
||||
"id": "mobile",
|
||||
"title": "Send To Mobile",
|
||||
"description": "Share HTML to a phone via LAN + QR",
|
||||
"command": "hplToolbox.openSendToMobile",
|
||||
"path": "/mobile",
|
||||
"beta": false
|
||||
},
|
||||
{
|
||||
"id": "mraid",
|
||||
"title": "MRAID Checker",
|
||||
"description": "Check MRAID requirements and best practices",
|
||||
"command": "hplToolbox.openMraidChecker",
|
||||
"path": "/mraid",
|
||||
"beta": false
|
||||
},
|
||||
{
|
||||
"id": "playworks",
|
||||
"title": "Playworks Converter",
|
||||
"description": "Convert Playworks HTML to per-network variants",
|
||||
"command": "hplToolbox.openPlayworksConverter",
|
||||
"path": "/playworks",
|
||||
"beta": true
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user