v1.0.1
This commit is contained in:
131
PROJECT.md
Normal file
131
PROJECT.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# HPL Toolbox — Project Summary for AI
|
||||
|
||||
## What this project is
|
||||
|
||||
HPL Toolbox is an internal tool for a playable ad team. It ships as **two separate deliverables** from the same repo:
|
||||
|
||||
1. **VS Code Extension** (`src/`) — a `.vsix` that adds tool panels as webview tabs inside VS Code.
|
||||
2. **Standalone Windows App** (`standalone/`) — a Go + WebView2 desktop app that exposes the same tools through a local HTTP server rendered in a native window. It is a **real, complete, production app** — not a prototype or subset.
|
||||
|
||||
Both deliverables implement the same set of tools. New features are typically added to both.
|
||||
|
||||
---
|
||||
|
||||
## Directory structure
|
||||
|
||||
```
|
||||
HPLToolbox-Extension/
|
||||
├── src/
|
||||
│ ├── extension.ts # Extension entry point, registers commands
|
||||
│ ├── tools/ # One file per tool (VS Code webview panels)
|
||||
│ │ ├── shared.ts # Shared utilities: singletonPanel, webview styles, clipboard/open handlers
|
||||
│ │ ├── plecUpload.ts # PLEC Upload tool
|
||||
│ │ ├── applovin.ts # AppLovin Playable Preview
|
||||
│ │ ├── base64Scanner.ts # Base64 Scanner
|
||||
│ │ ├── mraidChecker.ts # MRAID Checker
|
||||
│ │ ├── mintegral.ts # Mintegral Checker
|
||||
│ │ ├── sendToMobile.ts # Send To Mobile
|
||||
│ │ ├── playworks.ts # Playworks Converter
|
||||
│ │ ├── deviceSimulator.ts# Device Simulator
|
||||
│ │ └── projectInit.ts # Initialize Project
|
||||
│ └── tools.json # Tool launcher config (name, command, icon, description)
|
||||
├── standalone/ # The Go standalone app — complete, production-grade
|
||||
│ ├── main.go # Entry point: HTTP server + WebView2 window + route registration
|
||||
│ ├── platform.go # AppConfig struct, defaultConfig(), LoadConfig/SaveConfig, OS helpers
|
||||
│ ├── settings.go # /api/settings POST endpoint; settings modal HTML
|
||||
│ ├── layout.go # Shared HTML layout, nav sidebar, CSS
|
||||
│ ├── home.go # Home page / tool launcher
|
||||
│ ├── plec.go # PLEC Upload: page HTML, pick endpoints, upload (legacy + WordPress)
|
||||
│ ├── applovin.go # AppLovin upload
|
||||
│ ├── base64.go # Base64 Scanner
|
||||
│ ├── mraid.go # MRAID Checker
|
||||
│ ├── mintegral.go # Mintegral Checker
|
||||
│ ├── mobile.go # Send To Mobile
|
||||
│ ├── playworks.go # Playworks Converter
|
||||
│ ├── device_simulator.go # Device Simulator
|
||||
│ ├── project_init.go # Initialize Project
|
||||
│ ├── changelog.go # Changelog page
|
||||
│ ├── json.go # newJSONStream helper (NDJSON streaming responses)
|
||||
│ ├── update_checker.go # Auto-update check against Gitea release API
|
||||
│ ├── single_instance.go # Single-instance lock via TCP port file
|
||||
│ └── assets/ # Embedded static assets (qrcode.min.js, mintegral preview util)
|
||||
├── scripts/
|
||||
│ ├── build.ps1 # Builds both .vsix and standalone exe into dist/
|
||||
│ └── run-standalone.ps1 # Runs the standalone app in dev mode (go run .)
|
||||
├── data/
|
||||
│ └── manifest.json # Project Init manifest (team template files list)
|
||||
├── aiAssets/ # Sample HTML files used for testing/reference
|
||||
├── package.json # Extension manifest, VS Code settings contributions, npm scripts
|
||||
├── tsconfig.json
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tools
|
||||
|
||||
| Tool | Description |
|
||||
|---|---|
|
||||
| PLEC Upload | Upload HTML playable files to internal PLEC servers. Supports two servers: Default (167.99.227.249, legacy PHP) and Backup (20.255.60.183, WordPress). Backup server uses auto-login via credentials. |
|
||||
| AppLovin Playable Preview | Upload HTML files to p.applov.in and generate QR preview links. |
|
||||
| Base64 Scanner | Scan HTML files for external or non-base64 asset references. |
|
||||
| Send To Mobile | Serve selected HTML files over LAN and display a QR code for mobile preview. |
|
||||
| MRAID Checker | Check HTML files against MRAID requirements and best practices. |
|
||||
| Mintegral Checker | Validate Mintegral playable ZIPs against PlayTurbo requirements with embedded runtime preview. |
|
||||
| Playworks Converter | Convert Playworks HTML into per-network variants. |
|
||||
| Device Simulator | Preview HTML playables inside accurate mobile device frames with notch/Dynamic Island overlays. |
|
||||
| Initialize Project | Download team template files from a shared manifest (Google Drive or direct URL). |
|
||||
|
||||
---
|
||||
|
||||
## Architecture notes
|
||||
|
||||
### VS Code Extension
|
||||
- Each tool is a singleton webview panel (`singletonPanel` in `shared.ts`).
|
||||
- The panel HTML is returned as a template string from `getHtml()` inside each tool file.
|
||||
- Communication between extension host and webview uses `panel.webview.postMessage` / `onDidReceiveMessage`.
|
||||
- Settings are read from `vscode.workspace.getConfiguration('hplToolbox.*')` and contributed in `package.json`.
|
||||
- Session-persistent UI state (e.g. last selected server) uses `context.globalState`.
|
||||
|
||||
### Standalone App
|
||||
- Go HTTP server on a random localhost port, served into a WebView2 window.
|
||||
- All pages return full HTML strings (no template files on disk).
|
||||
- Long-running operations (uploads, scans) use `newJSONStream` to stream NDJSON progress events — the browser reads these with `ReadableStream` and updates the UI incrementally.
|
||||
- Config is persisted to `%LOCALAPPDATA%\HPLToolbox\config.json` via `LoadConfig` / `SaveConfig`.
|
||||
- `localStorage` is used for lightweight UI-only state (e.g. last selected server in PLEC Upload).
|
||||
- Single-instance enforcement: on launch, tries to connect to a previous instance's port from a lock file and `POST /api/focus` to bring its window forward, then exits.
|
||||
|
||||
### PLEC Upload — dual server detail
|
||||
- **Default (legacy)**: `POST http://167.99.227.249/src/upload_HTML_Experimental.php` with `fileUpload_N` / `iterationNameUpload_N` fields. Filenames are timestamped.
|
||||
- **Backup (WordPress)**: `POST http://20.255.60.183/wp-admin/admin-ajax.php` with `action=plec_upload_files`, `nonce`, `rows` (JSON array), and `file_N` fields. Login flow: POST `/wp-login.php` → GET `/file-upload/` to extract nonce. Iteration name is used as the uploaded filename. Preview URL is constructed as `/preview?theme=calcite&n1=<original>&m1=<saved>`.
|
||||
|
||||
---
|
||||
|
||||
## Build
|
||||
|
||||
```powershell
|
||||
npm run build # builds both .vsix and standalone .exe into dist/
|
||||
npm run build -- -Extension # .vsix only
|
||||
npm run build -- -Standalone # .exe only
|
||||
npm run run-standalone # dev: go run . in standalone/
|
||||
npm run compile # tsc only
|
||||
npm run watch # tsc watch
|
||||
```
|
||||
|
||||
Requires: Node.js, TypeScript (`npm install`), Go, optionally `vsce` and `rsrc`.
|
||||
|
||||
---
|
||||
|
||||
## Key config (VS Code settings)
|
||||
|
||||
All under `hplToolbox.*`:
|
||||
|
||||
| Key | Default | Purpose |
|
||||
|---|---|---|
|
||||
| `plec.uploadUrl` | `http://167.99.227.249/src/upload_HTML_Experimental.php` | Legacy server endpoint |
|
||||
| `plec.originUrl` | `http://167.99.227.249` | Origin/Referer for legacy uploads |
|
||||
| `plec.wpUsername` | `pm` | WordPress username for backup server |
|
||||
| `plec.wpPassword` | _(set)_ | WordPress password for backup server |
|
||||
| `applovin.cookie` | _(empty)_ | `__Host-APPLOVINID` cookie for p.applov.in |
|
||||
| `deviceSimulator.devicesUrl` | Google Drive link | Remote devices.json URL |
|
||||
| `deviceSimulator.languagesUrl` | Google Drive link | Remote languages.json URL |
|
||||
13
README.md
13
README.md
@@ -151,3 +151,16 @@ Changed
|
||||
Fixed
|
||||
- Fixed Device Simulator not appearing in the VS Code extension launcher due to a trailing comma in tools.json causing JSON.parse to fail and fall back to a hardcoded list that excluded it.
|
||||
```
|
||||
|
||||
**v1.0.1**
|
||||
```
|
||||
Added
|
||||
- Added backup PLEC server (20.255.60.183) as a selectable upload target in PLEC Upload.
|
||||
- Server dropdown in the PLEC Upload inputs panel — Default (167.99.227.249) or Backup (20.255.60.183).
|
||||
- Backup server uses WordPress authentication (auto-login, nonce fetch) — no manual token management.
|
||||
- Iteration name is used as the uploaded filename on the backup server.
|
||||
- Selected server persists across sessions (VS Code globalState / localStorage in standalone).
|
||||
|
||||
Changed
|
||||
- Build scripts moved to scripts/ and added as npm run build and npm run run-standalone.
|
||||
```
|
||||
BIN
dist/hpl-toolbox-1.0.0.vsix
vendored
BIN
dist/hpl-toolbox-1.0.0.vsix
vendored
Binary file not shown.
Binary file not shown.
BIN
dist/hpl-toolbox-1.0.1.vsix
vendored
Normal file
BIN
dist/hpl-toolbox-1.0.1.vsix
vendored
Normal file
Binary file not shown.
16
package.json
16
package.json
@@ -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": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"publisher": "hesukastro",
|
||||
"license": "UNLICENSED",
|
||||
"repository": {
|
||||
@@ -103,6 +103,16 @@
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Skip the pre-check that verifies the filename does not already exist on the preview server."
|
||||
},
|
||||
"hplToolbox.plec.wpUsername": {
|
||||
"type": "string",
|
||||
"default": "pm",
|
||||
"description": "WordPress username for the backup PLEC server (20.255.60.183)."
|
||||
},
|
||||
"hplToolbox.plec.wpPassword": {
|
||||
"type": "string",
|
||||
"default": "nRLz#LV@y#)q@CW@z8fGEPyI",
|
||||
"description": "WordPress password for the backup PLEC server (20.255.60.183)."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -167,7 +177,9 @@
|
||||
"scripts": {
|
||||
"compile": "tsc -p ./",
|
||||
"watch": "tsc -watch -p ./",
|
||||
"vscode:prepublish": "npm run compile"
|
||||
"vscode:prepublish": "npm run compile",
|
||||
"build": "pwsh -File scripts/build.ps1",
|
||||
"run-standalone": "pwsh -File scripts/run-standalone.ps1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
|
||||
@@ -15,7 +15,7 @@ param(
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$repoRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$repoRoot = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
|
||||
Set-Location $repoRoot
|
||||
|
||||
# If neither was specified, build both.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$root = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
|
||||
$standalone = Join-Path $root 'standalone'
|
||||
|
||||
Push-Location $standalone
|
||||
|
||||
@@ -6,15 +6,25 @@ import { getToolWebviewStyles, handleClipboardAndOpen, singletonPanel } from './
|
||||
|
||||
const store: { panel: vscode.WebviewPanel | null } = { panel: null };
|
||||
|
||||
export function openPlecUpload(_context: vscode.ExtensionContext) {
|
||||
export function openPlecUpload(context: vscode.ExtensionContext) {
|
||||
const { panel, isNew } = singletonPanel(store, 'hplToolbox.plecUpload', 'PLEC Upload');
|
||||
if (!isNew) return;
|
||||
|
||||
panel.webview.html = getHtml();
|
||||
|
||||
// Restore last selected server
|
||||
const lastServer = context.globalState.get<string>('plec.lastServer', 'legacy');
|
||||
panel.webview.postMessage({ type: 'restoreServer', value: lastServer });
|
||||
|
||||
panel.webview.onDidReceiveMessage(async (msg) => {
|
||||
try {
|
||||
if (handleClipboardAndOpen(msg)) return;
|
||||
|
||||
if (msg.type === 'saveServer') {
|
||||
context.globalState.update('plec.lastServer', msg.value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.type === 'pickFolder') {
|
||||
const picked = await vscode.window.showOpenDialog({
|
||||
canSelectFolders: true,
|
||||
@@ -51,7 +61,7 @@ export function openPlecUpload(_context: vscode.ExtensionContext) {
|
||||
}
|
||||
|
||||
if (msg.type === 'upload') {
|
||||
await handleUpload(panel, msg.rows);
|
||||
await handleUpload(panel, msg.rows, msg.server);
|
||||
return;
|
||||
}
|
||||
} catch (err: any) {
|
||||
@@ -66,11 +76,7 @@ interface UploadRow {
|
||||
iteration: string;
|
||||
}
|
||||
|
||||
async function handleUpload(panel: vscode.WebviewPanel, rows: UploadRow[]) {
|
||||
const cfg = vscode.workspace.getConfiguration('hplToolbox.plec');
|
||||
const uploadUrl = cfg.get<string>('uploadUrl')!;
|
||||
const originUrl = cfg.get<string>('originUrl')!;
|
||||
|
||||
async function handleUpload(panel: vscode.WebviewPanel, rows: UploadRow[], server: string) {
|
||||
const valid: UploadRow[] = [];
|
||||
for (const r of rows) {
|
||||
if (!r.path || !fs.existsSync(r.path)) {
|
||||
@@ -92,11 +98,21 @@ async function handleUpload(panel: vscode.WebviewPanel, rows: UploadRow[]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (server === 'wordpress') {
|
||||
await handleUploadWordPress(panel, valid);
|
||||
} else {
|
||||
await handleUploadLegacy(panel, valid);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUploadLegacy(panel: vscode.WebviewPanel, valid: UploadRow[]) {
|
||||
const cfg = vscode.workspace.getConfiguration('hplToolbox.plec');
|
||||
const uploadUrl = cfg.get<string>('uploadUrl')!;
|
||||
const originUrl = cfg.get<string>('originUrl')!;
|
||||
|
||||
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];
|
||||
@@ -129,20 +145,14 @@ async function handleUpload(panel: vscode.WebviewPanel, rows: UploadRow[]) {
|
||||
|
||||
const text = await res.text();
|
||||
if (!res.ok) {
|
||||
panel.webview.postMessage({
|
||||
type: 'error',
|
||||
message: `HTTP ${res.status}\n${text.slice(0, 800)}`,
|
||||
});
|
||||
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)}`,
|
||||
});
|
||||
panel.webview.postMessage({ type: 'error', message: `Server returned non-JSON:\n${text.slice(0, 800)}` });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -150,6 +160,154 @@ async function handleUpload(panel: vscode.WebviewPanel, rows: UploadRow[]) {
|
||||
panel.webview.postMessage({ type: 'result', preview, raw: json, rawText: text });
|
||||
}
|
||||
|
||||
async function wpAuthenticate(panel: vscode.WebviewPanel): Promise<{ cookie: string; nonce: string }> {
|
||||
const cfg = vscode.workspace.getConfiguration('hplToolbox.plec');
|
||||
const username = cfg.get<string>('wpUsername') || '';
|
||||
const password = cfg.get<string>('wpPassword') || '';
|
||||
const baseUrl = 'http://20.255.60.183';
|
||||
|
||||
if (!username || !password) {
|
||||
throw new Error('WordPress credentials not set. Add hplToolbox.plec.wpUsername and wpPassword in VS Code settings.');
|
||||
}
|
||||
|
||||
panel.webview.postMessage({ type: 'status', message: 'Authenticating with backup server...' });
|
||||
|
||||
const loginBody = new URLSearchParams({
|
||||
log: username,
|
||||
pwd: password,
|
||||
'wp-submit': 'Log In',
|
||||
redirect_to: '/wp-admin/',
|
||||
testcookie: '1',
|
||||
});
|
||||
|
||||
const loginRes = await fetch(`${baseUrl}/wp-login.php`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Cookie': 'wordpress_test_cookie=WP Cookie check',
|
||||
'Origin': baseUrl,
|
||||
'Referer': `${baseUrl}/wp-login.php`,
|
||||
'User-Agent': 'Mozilla/5.0 (HPL-Toolbox-VSCode)',
|
||||
},
|
||||
body: loginBody.toString(),
|
||||
redirect: 'manual',
|
||||
});
|
||||
|
||||
const location = loginRes.headers.get('location') || '';
|
||||
|
||||
// Extract Set-Cookie headers — try getSetCookie() first (Node 18+), fall back to entries()
|
||||
let rawCookies: string[] = [];
|
||||
if (typeof (loginRes.headers as any).getSetCookie === 'function') {
|
||||
rawCookies = (loginRes.headers as any).getSetCookie();
|
||||
} else {
|
||||
for (const [k, v] of (loginRes.headers as any).entries()) {
|
||||
if (k.toLowerCase() === 'set-cookie') rawCookies.push(v);
|
||||
}
|
||||
}
|
||||
const cookie = rawCookies.map((c: string) => c.split(';')[0]).join('; ');
|
||||
|
||||
// WordPress sends a 302 to /wp-admin/ on success, or 200 (login page with error) on failure
|
||||
const loginOk = (loginRes.status === 301 || loginRes.status === 302) && !location.includes('wp-login');
|
||||
if (!loginOk) {
|
||||
const loginHtml = await loginRes.text();
|
||||
const errorMatch = loginHtml.match(/<div id="login_error">([\s\S]*?)<\/div>/);
|
||||
const wpError = errorMatch
|
||||
? errorMatch[1].replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim()
|
||||
: null;
|
||||
const bodyPreview = loginHtml.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 500);
|
||||
throw new Error(
|
||||
`WordPress login failed (HTTP ${loginRes.status}).\n` +
|
||||
(wpError ? `Server: "${wpError}"\n` : '') +
|
||||
`Body: ${bodyPreview}`
|
||||
);
|
||||
}
|
||||
if (!cookie) {
|
||||
throw new Error(`Login succeeded (redirected to ${location}) but no session cookies were returned. Check server config.`);
|
||||
}
|
||||
|
||||
panel.webview.postMessage({ type: 'status', message: 'Fetching upload token...' });
|
||||
|
||||
const pageRes = await fetch(`${baseUrl}/file-upload/`, {
|
||||
headers: { 'Cookie': cookie, 'User-Agent': 'Mozilla/5.0 (HPL-Toolbox-VSCode)' },
|
||||
});
|
||||
const pageText = await pageRes.text();
|
||||
const match = pageText.match(/"nonce"\s*:\s*"([a-f0-9]+)"/);
|
||||
if (!match) {
|
||||
throw new Error('Could not extract nonce from backup server page. Are you sure the account has access?');
|
||||
}
|
||||
|
||||
return { cookie, nonce: match[1] };
|
||||
}
|
||||
|
||||
async function handleUploadWordPress(panel: vscode.WebviewPanel, valid: UploadRow[]) {
|
||||
const { cookie, nonce } = await wpAuthenticate(panel);
|
||||
const baseUrl = 'http://20.255.60.183';
|
||||
|
||||
const form = new FormData();
|
||||
form.append('action', 'plec_upload_files');
|
||||
form.append('nonce', nonce);
|
||||
|
||||
const rowsMeta: { field: string; iterationName: string }[] = [];
|
||||
for (let i = 0; i < valid.length; i++) {
|
||||
const r = valid[i];
|
||||
const idx = i + 1;
|
||||
const fieldName = `file_${idx}`;
|
||||
const sanitized = r.iteration.replace(/\s+/g, '');
|
||||
panel.webview.postMessage({ type: 'status', message: `Preparing ${idx}/${valid.length} ${sanitized}.html` });
|
||||
const buf = fs.readFileSync(r.path);
|
||||
const file = new File([buf], `${sanitized}.html`, { type: 'text/html' });
|
||||
form.append(fieldName, file as any);
|
||||
rowsMeta.push({ field: fieldName, iterationName: sanitized });
|
||||
}
|
||||
form.append('rows', JSON.stringify(rowsMeta));
|
||||
|
||||
panel.webview.postMessage({ type: 'status', message: `Uploading ${valid.length} file${valid.length === 1 ? '' : 's'}...` });
|
||||
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(`${baseUrl}/wp-admin/admin-ajax.php`, {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
headers: {
|
||||
'Cookie': cookie,
|
||||
'Origin': baseUrl,
|
||||
'Referer': `${baseUrl}/file-upload/`,
|
||||
'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 files: { original: string; saved: string }[] = json.data?.files || [];
|
||||
let preview: string | null = json.preview || json.previewURL || null;
|
||||
if (!preview && files.length > 0) {
|
||||
const params = new URLSearchParams({ theme: 'calcite' });
|
||||
files.forEach((f, i) => {
|
||||
params.append(`n${i + 1}`, f.original.replace(/\.html?$/i, ''));
|
||||
params.append(`m${i + 1}`, f.saved);
|
||||
});
|
||||
preview = `${baseUrl}/preview?${params.toString()}`;
|
||||
}
|
||||
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 (
|
||||
@@ -225,6 +383,17 @@ ${getToolWebviewStyles()}
|
||||
.result a { color: var(--vscode-textLink-foreground); }
|
||||
.result-actions { margin-top: 8px; display: flex; gap: 8px; }
|
||||
.pager { margin-top: 8px; display: flex; align-items: center; gap: 8px; justify-content: flex-end; }
|
||||
.panel-header { display: flex; align-items: center; }
|
||||
#serverSelect {
|
||||
margin-left: auto;
|
||||
background: var(--vscode-dropdown-background);
|
||||
color: var(--vscode-dropdown-foreground);
|
||||
border: 1px solid var(--vscode-dropdown-border);
|
||||
padding: 3px 6px;
|
||||
border-radius: 2px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -237,6 +406,10 @@ ${getToolWebviewStyles()}
|
||||
<section class="tool-panel input-panel">
|
||||
<div class="panel-header">
|
||||
<h3 class="panel-title">Inputs</h3>
|
||||
<select id="serverSelect">
|
||||
<option value="legacy">167.99.227.249 (Default)</option>
|
||||
<option value="wordpress">20.255.60.183 (Backup)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="control-group">
|
||||
@@ -377,6 +550,10 @@ ${getToolWebviewStyles()}
|
||||
copyResultBtn.disabled = !previewUrl;
|
||||
}
|
||||
|
||||
document.getElementById('serverSelect').addEventListener('change', (e) => {
|
||||
vscode.postMessage({ type: 'saveServer', value: e.target.value });
|
||||
});
|
||||
|
||||
document.getElementById('pickFolder').addEventListener('click', () => {
|
||||
vscode.postMessage({ type: 'pickFolder' });
|
||||
});
|
||||
@@ -402,7 +579,7 @@ ${getToolWebviewStyles()}
|
||||
}
|
||||
statusEl.textContent = 'Uploading...';
|
||||
statusEl.classList.add('is-busy');
|
||||
vscode.postMessage({ type: 'upload', rows });
|
||||
vscode.postMessage({ type: 'upload', rows, server: document.getElementById('serverSelect').value });
|
||||
});
|
||||
openResultBtn.addEventListener('click', () => {
|
||||
if (previewUrl) vscode.postMessage({ type: 'open', text: previewUrl });
|
||||
@@ -418,6 +595,10 @@ ${getToolWebviewStyles()}
|
||||
|
||||
window.addEventListener('message', (e) => {
|
||||
const m = e.data;
|
||||
if (m.type === 'restoreServer') {
|
||||
document.getElementById('serverSelect').value = m.value;
|
||||
return;
|
||||
}
|
||||
if (m.type === 'filesSelected') {
|
||||
addFiles(m.files || []);
|
||||
} else if (m.type === 'status') {
|
||||
|
||||
Binary file not shown.
@@ -16,6 +16,8 @@ type PlecConfig struct {
|
||||
OriginUrl string `json:"originUrl"`
|
||||
PreviewBase string `json:"previewBase"`
|
||||
SkipExistsCheck bool `json:"skipExistsCheck"`
|
||||
WpUsername string `json:"wpUsername"`
|
||||
WpPassword string `json:"wpPassword"`
|
||||
}
|
||||
|
||||
type ApplovinConfig struct {
|
||||
@@ -69,6 +71,8 @@ func defaultConfig() AppConfig {
|
||||
OriginUrl: "http://167.99.227.249",
|
||||
PreviewBase: "https://playable.applovindemo.com/phaser/",
|
||||
SkipExistsCheck: false,
|
||||
WpUsername: "pm",
|
||||
WpPassword: "nRLz#LV@y#)q@CW@z8fGEPyI",
|
||||
},
|
||||
Applovin: ApplovinConfig{Cookie: "", DelayMs: 5000},
|
||||
SendToMobile: SendToMobileConfig{Port: 0},
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
urlpkg "net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -23,7 +24,13 @@ func PlecPage(w http.ResponseWriter, r *http.Request) {
|
||||
</header>
|
||||
|
||||
<section class="tool-panel">
|
||||
<div class="panel-header"><h3 class="panel-title">Inputs</h3></div>
|
||||
<div class="panel-header">
|
||||
<h3 class="panel-title">Inputs</h3>
|
||||
<select id="serverSelect">
|
||||
<option value="legacy">167.99.227.249 (Default)</option>
|
||||
<option value="wordpress">20.255.60.183 (Backup)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="control-group">
|
||||
<div id="dropZone" class="drop-zone">
|
||||
@@ -54,10 +61,15 @@ func PlecPage(w http.ResponseWriter, r *http.Request) {
|
||||
<style>
|
||||
.row-error { color:#f48771; font-size:12px; margin-top:4px; }
|
||||
#rows input[type=text] { width: 100%; }
|
||||
.panel-header { display: flex; align-items: center; }
|
||||
#serverSelect { margin-left:auto; padding:3px 6px; font-size:12px; cursor:pointer; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const PAGE_SIZE = 5;
|
||||
const serverSelect = document.getElementById('serverSelect');
|
||||
serverSelect.value = localStorage.getItem('plec.lastServer') || 'legacy';
|
||||
serverSelect.addEventListener('change', () => localStorage.setItem('plec.lastServer', serverSelect.value));
|
||||
let nextId = 1;
|
||||
let rows = [];
|
||||
let page = 0;
|
||||
@@ -206,7 +218,7 @@ document.getElementById('upload').addEventListener('click', async () => {
|
||||
const res = await fetch('/api/plec/upload', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ rows }),
|
||||
body: JSON.stringify({ rows, server: serverSelect.value }),
|
||||
});
|
||||
await readJSONStream(res, (msg) => {
|
||||
if (msg.type === 'status') {
|
||||
@@ -299,7 +311,8 @@ type plecRow struct {
|
||||
func PlecUpload(w http.ResponseWriter, r *http.Request) {
|
||||
send := newJSONStream(w)
|
||||
var req struct {
|
||||
Rows []plecRow `json:"rows"`
|
||||
Rows []plecRow `json:"rows"`
|
||||
Server string `json:"server"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
send(map[string]any{"type": "error", "message": err.Error()})
|
||||
@@ -335,6 +348,14 @@ func PlecUpload(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if req.Server == "wordpress" {
|
||||
plecUploadWordPress(send, cfg, valid)
|
||||
} else {
|
||||
plecUploadLegacy(send, cfg, valid)
|
||||
}
|
||||
}
|
||||
|
||||
func plecUploadLegacy(send func(any), cfg PlecConfig, valid []plecRow) {
|
||||
stamp := time.Now().Format("20060102150405")
|
||||
|
||||
var buf bytes.Buffer
|
||||
@@ -357,9 +378,7 @@ func PlecUpload(w http.ResponseWriter, r *http.Request) {
|
||||
send(map[string]any{"type": "error", "message": err.Error()})
|
||||
return
|
||||
}
|
||||
iter := urlpkg.QueryEscape(strings.ReplaceAll(row.Iteration, " ", ""))
|
||||
// Original uses encodeURI which keeps more chars than QueryEscape; emulate by un-escaping safe chars.
|
||||
iter = encodeURICompatible(strings.ReplaceAll(row.Iteration, " ", ""))
|
||||
iter := encodeURICompatible(strings.ReplaceAll(row.Iteration, " ", ""))
|
||||
if err := mw.WriteField(fmt.Sprintf("iterationNameUpload_%d", idx), iter); err != nil {
|
||||
send(map[string]any{"type": "error", "message": err.Error()})
|
||||
return
|
||||
@@ -408,6 +427,171 @@ func PlecUpload(w http.ResponseWriter, r *http.Request) {
|
||||
send(map[string]any{"type": "done", "preview": preview, "raw": parsed, "rawText": string(respBody)})
|
||||
}
|
||||
|
||||
func wpAuthenticate(send func(any), baseUrl, username, password string) (*http.Client, string, error) {
|
||||
jar, _ := cookiejar.New(nil)
|
||||
client := &http.Client{Jar: jar}
|
||||
|
||||
// Pre-seed the test cookie WordPress checks for
|
||||
testURL, _ := urlpkg.Parse(baseUrl)
|
||||
jar.SetCookies(testURL, []*http.Cookie{{Name: "wordpress_test_cookie", Value: "WP Cookie check"}})
|
||||
|
||||
send(map[string]any{"type": "status", "message": "Authenticating with backup server..."})
|
||||
|
||||
form := urlpkg.Values{
|
||||
"log": {username},
|
||||
"pwd": {password},
|
||||
"wp-submit": {"Log In"},
|
||||
"redirect_to": {"/wp-admin/"},
|
||||
"testcookie": {"1"},
|
||||
}
|
||||
loginReq, _ := http.NewRequest("POST", baseUrl+"/wp-login.php", strings.NewReader(form.Encode()))
|
||||
loginReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
loginReq.Header.Set("Origin", baseUrl)
|
||||
loginReq.Header.Set("Referer", baseUrl+"/wp-login.php")
|
||||
loginReq.Header.Set("User-Agent", "Mozilla/5.0 (HPL-Toolbox-Standalone)")
|
||||
|
||||
loginResp, err := client.Do(loginReq)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("login request failed: %w", err)
|
||||
}
|
||||
io.ReadAll(loginResp.Body)
|
||||
loginResp.Body.Close()
|
||||
|
||||
// After following redirects, check final URL — wp-admin means success
|
||||
if strings.Contains(loginResp.Request.URL.String(), "wp-login") {
|
||||
return nil, "", fmt.Errorf("WordPress login failed — check wpUsername / wpPassword in settings")
|
||||
}
|
||||
|
||||
send(map[string]any{"type": "status", "message": "Fetching upload token..."})
|
||||
|
||||
pageReq, _ := http.NewRequest("GET", baseUrl+"/file-upload/", nil)
|
||||
pageReq.Header.Set("User-Agent", "Mozilla/5.0 (HPL-Toolbox-Standalone)")
|
||||
pageResp, err := client.Do(pageReq)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("nonce fetch failed: %w", err)
|
||||
}
|
||||
pageBody, _ := io.ReadAll(pageResp.Body)
|
||||
pageResp.Body.Close()
|
||||
|
||||
nonceRx := regexp.MustCompile(`"nonce"\s*:\s*"([a-f0-9]+)"`)
|
||||
m := nonceRx.FindSubmatch(pageBody)
|
||||
if m == nil {
|
||||
return nil, "", fmt.Errorf("could not extract nonce from backup server page")
|
||||
}
|
||||
return client, string(m[1]), nil
|
||||
}
|
||||
|
||||
func plecUploadWordPress(send func(any), cfg PlecConfig, valid []plecRow) {
|
||||
const baseUrl = "http://20.255.60.183"
|
||||
|
||||
if cfg.WpUsername == "" || cfg.WpPassword == "" {
|
||||
send(map[string]any{"type": "error", "message": "WordPress credentials not set. Configure wpUsername / wpPassword in settings."})
|
||||
return
|
||||
}
|
||||
|
||||
client, nonce, err := wpAuthenticate(send, baseUrl, cfg.WpUsername, cfg.WpPassword)
|
||||
if err != nil {
|
||||
send(map[string]any{"type": "error", "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
mw := multipart.NewWriter(&buf)
|
||||
_ = mw.WriteField("action", "plec_upload_files")
|
||||
_ = mw.WriteField("nonce", nonce)
|
||||
|
||||
type rowMeta struct {
|
||||
Field string `json:"field"`
|
||||
IterationName string `json:"iterationName"`
|
||||
}
|
||||
var rowsMeta []rowMeta
|
||||
|
||||
for i, row := range valid {
|
||||
idx := i + 1
|
||||
fieldName := fmt.Sprintf("file_%d", idx)
|
||||
sanitized := strings.ReplaceAll(row.Iteration, " ", "")
|
||||
filename := sanitized + ".html"
|
||||
send(map[string]any{"type": "status", "message": fmt.Sprintf("Preparing %d/%d %s", idx, len(valid), filename)})
|
||||
data, err := os.ReadFile(row.Path)
|
||||
if err != nil {
|
||||
send(map[string]any{"type": "error", "message": "Read failed: " + err.Error()})
|
||||
return
|
||||
}
|
||||
fw, err := createFormFileWithType(mw, fieldName, filename, "text/html")
|
||||
if err != nil {
|
||||
send(map[string]any{"type": "error", "message": err.Error()})
|
||||
return
|
||||
}
|
||||
if _, err := fw.Write(data); err != nil {
|
||||
send(map[string]any{"type": "error", "message": err.Error()})
|
||||
return
|
||||
}
|
||||
rowsMeta = append(rowsMeta, rowMeta{Field: fieldName, IterationName: sanitized})
|
||||
}
|
||||
|
||||
rowsJSON, _ := json.Marshal(rowsMeta)
|
||||
_ = mw.WriteField("rows", string(rowsJSON))
|
||||
if err := mw.Close(); err != nil {
|
||||
send(map[string]any{"type": "error", "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
send(map[string]any{"type": "status", "message": fmt.Sprintf("Uploading %d file%s...", len(valid), pluralS(len(valid)))})
|
||||
|
||||
httpReq, _ := http.NewRequest("POST", baseUrl+"/wp-admin/admin-ajax.php", &buf)
|
||||
httpReq.Header.Set("Content-Type", mw.FormDataContentType())
|
||||
httpReq.Header.Set("Origin", baseUrl)
|
||||
httpReq.Header.Set("Referer", baseUrl+"/file-upload/")
|
||||
httpReq.Header.Set("User-Agent", "Mozilla/5.0 (HPL-Toolbox-Standalone)")
|
||||
httpReq.Header.Set("X-Requested-With", "XMLHttpRequest")
|
||||
httpReq.Header.Set("Accept", "*/*")
|
||||
|
||||
resp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
send(map[string]any{"type": "error", "message": "Network error: " + err.Error()})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
send(map[string]any{"type": "error", "message": fmt.Sprintf("HTTP %d\n%s", resp.StatusCode, truncate(string(respBody), 800))})
|
||||
return
|
||||
}
|
||||
var parsed map[string]any
|
||||
if err := json.Unmarshal(respBody, &parsed); err != nil {
|
||||
send(map[string]any{"type": "error", "message": "Server returned non-JSON:\n" + truncate(string(respBody), 800)})
|
||||
return
|
||||
}
|
||||
|
||||
var preview string
|
||||
if data, ok := parsed["data"].(map[string]any); ok {
|
||||
if files, ok := data["files"].([]any); ok && len(files) > 0 {
|
||||
params := urlpkg.Values{"theme": {"calcite"}}
|
||||
nonceRx := regexp.MustCompile(`(?i)\.html?$`)
|
||||
for i, f := range files {
|
||||
if fm, ok := f.(map[string]any); ok {
|
||||
orig, _ := fm["original"].(string)
|
||||
saved, _ := fm["saved"].(string)
|
||||
n := nonceRx.ReplaceAllString(orig, "")
|
||||
params.Set(fmt.Sprintf("n%d", i+1), n)
|
||||
params.Set(fmt.Sprintf("m%d", i+1), saved)
|
||||
}
|
||||
}
|
||||
preview = baseUrl + "/preview?" + params.Encode()
|
||||
}
|
||||
}
|
||||
if preview == "" {
|
||||
if v, ok := parsed["preview"].(string); ok {
|
||||
preview = v
|
||||
} else if v, ok := parsed["previewURL"].(string); ok {
|
||||
preview = v
|
||||
}
|
||||
}
|
||||
|
||||
send(map[string]any{"type": "done", "preview": preview, "raw": parsed, "rawText": string(respBody)})
|
||||
}
|
||||
|
||||
func ClipboardEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Text string `json:"text"`
|
||||
|
||||
@@ -15,6 +15,8 @@ func SettingsSaveEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
OriginUrl string `json:"originUrl"`
|
||||
PreviewBase string `json:"previewBase"`
|
||||
SkipExistsCheck bool `json:"skipExistsCheck"`
|
||||
WpUsername string `json:"wpUsername"`
|
||||
WpPassword string `json:"wpPassword"`
|
||||
} `json:"plec"`
|
||||
Applovin struct {
|
||||
Cookie string `json:"cookie"`
|
||||
@@ -40,6 +42,8 @@ func SettingsSaveEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
cfg.Plec.OriginUrl = strings.TrimSpace(req.Plec.OriginUrl)
|
||||
cfg.Plec.PreviewBase = strings.TrimSpace(req.Plec.PreviewBase)
|
||||
cfg.Plec.SkipExistsCheck = req.Plec.SkipExistsCheck
|
||||
cfg.Plec.WpUsername = strings.TrimSpace(req.Plec.WpUsername)
|
||||
cfg.Plec.WpPassword = req.Plec.WpPassword
|
||||
cfg.Applovin.Cookie = strings.TrimSpace(req.Applovin.Cookie)
|
||||
cfg.Applovin.DelayMs = req.Applovin.DelayMs
|
||||
cfg.Applovin.RandomizeUserAgent = req.Applovin.RandomizeUserAgent
|
||||
|
||||
Reference in New Issue
Block a user