Compare commits
5 Commits
f367b66b60
...
6be55255b2
| Author | SHA1 | Date | |
|---|---|---|---|
| 6be55255b2 | |||
| 9d6ed4b0a5 | |||
| 1336695068 | |||
| 40f13989c2 | |||
| 716dac31ef |
7
.claude/settings.local.json
Normal file
7
.claude/settings.local.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(go build *)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
4
LICENSE
Normal file
4
LICENSE
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
Copyright (c) 2026 HPL
|
||||||
|
|
||||||
|
All rights reserved. This software is proprietary and confidential.
|
||||||
|
Unauthorized copying, distribution, or use is prohibited.
|
||||||
15
build.ps1
15
build.ps1
@@ -25,8 +25,14 @@ if (-not $Extension -and -not $Standalone) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$distDir = Join-Path $repoRoot "dist"
|
$distDir = Join-Path $repoRoot "dist"
|
||||||
|
if (Test-Path $distDir) {
|
||||||
|
Remove-Item -Path (Join-Path $distDir "*") -Recurse -Force
|
||||||
|
}
|
||||||
New-Item -ItemType Directory -Path $distDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $distDir -Force | Out-Null
|
||||||
|
|
||||||
|
$pkg = Get-Content (Join-Path $repoRoot "package.json") -Raw | ConvertFrom-Json
|
||||||
|
$version = $pkg.version
|
||||||
|
|
||||||
function Write-Step($msg) {
|
function Write-Step($msg) {
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "==> $msg" -ForegroundColor Cyan
|
Write-Host "==> $msg" -ForegroundColor Cyan
|
||||||
@@ -104,13 +110,14 @@ if ($Standalone) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$exeOut = Join-Path $distDir "hpl-toolbox.exe"
|
$exeName = "hpl-toolbox-$version.exe"
|
||||||
go build -ldflags="-s -w -H windowsgui" -trimpath -o $exeOut .
|
$exeOut = Join-Path $distDir $exeName
|
||||||
|
go build -ldflags="-s -w -H windowsgui -X main.AppVersion=$version" -trimpath -o $exeOut .
|
||||||
if ($LASTEXITCODE -ne 0) { Write-Error "go build failed"; exit 1 }
|
if ($LASTEXITCODE -ne 0) { Write-Error "go build failed"; exit 1 }
|
||||||
|
|
||||||
$summary += [pscustomobject]@{
|
$summary += [pscustomobject]@{
|
||||||
Artifact = "hpl-toolbox.exe"
|
Artifact = $exeName
|
||||||
Path = "dist\hpl-toolbox.exe"
|
Path = "dist\$exeName"
|
||||||
Size = Get-FileSize $exeOut
|
Size = Get-FileSize $exeOut
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
BIN
dist/hpl-toolbox.exe → dist/hpl-toolbox-0.1.5.exe
vendored
BIN
dist/hpl-toolbox.exe → dist/hpl-toolbox-0.1.5.exe
vendored
Binary file not shown.
Binary file not shown.
18
package.json
18
package.json
@@ -2,8 +2,13 @@
|
|||||||
"name": "hpl-toolbox",
|
"name": "hpl-toolbox",
|
||||||
"displayName": "HPL Toolbox",
|
"displayName": "HPL Toolbox",
|
||||||
"description": "Bundled tools: PLEC Upload, AppLovin Demo Upload, Base64 Asset Scanner, Daily Update. Local HTML Host.",
|
"description": "Bundled tools: PLEC Upload, AppLovin Demo Upload, Base64 Asset Scanner, Daily Update. Local HTML Host.",
|
||||||
"version": "0.1.2",
|
"version": "0.1.5",
|
||||||
"publisher": "hesukastro",
|
"publisher": "hesukastro",
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://gitea.hesukastro.com/hesukastro/vsix-hpl-toolbox.git"
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"vscode": "^1.85.0"
|
"vscode": "^1.85.0"
|
||||||
},
|
},
|
||||||
@@ -85,6 +90,17 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "",
|
"default": "",
|
||||||
"description": "Value of the __Host-APPLOVINID cookie for p.applov.in (log in at https://p.applov.in/playablePreview?create=1&qr=1 and copy the cookie from DevTools)."
|
"description": "Value of the __Host-APPLOVINID cookie for p.applov.in (log in at https://p.applov.in/playablePreview?create=1&qr=1 and copy the cookie from DevTools)."
|
||||||
|
},
|
||||||
|
"hplToolbox.applovin.delayMs": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 5000,
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "Delay (in milliseconds) between consecutive file uploads, to avoid AppLovin rate limiting / 403 responses."
|
||||||
|
},
|
||||||
|
"hplToolbox.applovin.randomizeUserAgent": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "Randomize the User-Agent header on every request to p.applov.in to reduce bot detection."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export function activate(context: vscode.ExtensionContext) {
|
|||||||
vscode.commands.registerCommand('hplToolbox.openBase64Scanner', () => openBase64Scanner(context)),
|
vscode.commands.registerCommand('hplToolbox.openBase64Scanner', () => openBase64Scanner(context)),
|
||||||
vscode.commands.registerCommand('hplToolbox.openDailyUpdate', () => openDailyUpdate(context)),
|
vscode.commands.registerCommand('hplToolbox.openDailyUpdate', () => openDailyUpdate(context)),
|
||||||
vscode.commands.registerCommand('hplToolbox.openSendToMobile', () => openSendToMobile(context)),
|
vscode.commands.registerCommand('hplToolbox.openSendToMobile', () => openSendToMobile(context)),
|
||||||
vscode.window.registerWebviewViewProvider('hplToolbox.launcher', new LauncherViewProvider())
|
vscode.window.registerWebviewViewProvider('hplToolbox.launcher', new LauncherViewProvider(context))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
|
|
||||||
export class LauncherViewProvider implements vscode.WebviewViewProvider {
|
export class LauncherViewProvider implements vscode.WebviewViewProvider {
|
||||||
|
constructor(private readonly context: vscode.ExtensionContext) {}
|
||||||
|
|
||||||
resolveWebviewView(view: vscode.WebviewView) {
|
resolveWebviewView(view: vscode.WebviewView) {
|
||||||
view.webview.options = { enableScripts: true };
|
view.webview.options = { enableScripts: true };
|
||||||
view.webview.html = getHtml();
|
const version = this.context.extension.packageJSON.version as string;
|
||||||
|
view.webview.html = getHtml(version);
|
||||||
view.webview.onDidReceiveMessage((msg) => {
|
view.webview.onDidReceiveMessage((msg) => {
|
||||||
if (msg?.type === 'open' && typeof msg.command === 'string') {
|
if (msg?.type === 'open' && typeof msg.command === 'string') {
|
||||||
vscode.commands.executeCommand(msg.command);
|
vscode.commands.executeCommand(msg.command);
|
||||||
@@ -12,7 +15,7 @@ export class LauncherViewProvider implements vscode.WebviewViewProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHtml(): string {
|
function getHtml(version: string): string {
|
||||||
return `<!DOCTYPE html>
|
return `<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
@@ -35,7 +38,7 @@ function getHtml(): string {
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h3>HPL Toolbox</h3>
|
<h3>HPL Toolbox ${version}</h3>
|
||||||
<button class="tool-btn" data-cmd="hplToolbox.openPlecUpload">
|
<button class="tool-btn" data-cmd="hplToolbox.openPlecUpload">
|
||||||
PLEC Upload
|
PLEC Upload
|
||||||
<span class="tool-desc">Upload HTML to internal PLEC server</span>
|
<span class="tool-desc">Upload HTML to internal PLEC server</span>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export function openApplovinUpload(_context: vscode.ExtensionContext) {
|
|||||||
canSelectMany: true,
|
canSelectMany: true,
|
||||||
filters: { HTML: ['html', 'htm'] },
|
filters: { HTML: ['html', 'htm'] },
|
||||||
openLabel: 'Select',
|
openLabel: 'Select',
|
||||||
|
defaultUri: getPickerDefaultUri(),
|
||||||
});
|
});
|
||||||
if (picked && picked.length) {
|
if (picked && picked.length) {
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
@@ -34,15 +35,46 @@ export function openApplovinUpload(_context: vscode.ExtensionContext) {
|
|||||||
await handleUpload(panel, msg.paths);
|
await handleUpload(panel, msg.paths);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (msg.type === 'saveQr') {
|
||||||
|
await handleSaveQr(panel, msg.url, msg.name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === 'saveAllQrs') {
|
||||||
|
await handleSaveAllQrs(panel, msg.items || []);
|
||||||
|
return;
|
||||||
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
panel.webview.postMessage({ type: 'error', message: err?.message || String(err) });
|
panel.webview.postMessage({ type: 'error', message: err?.message || String(err) });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const USER_AGENTS = [
|
||||||
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
||||||
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
|
||||||
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0',
|
||||||
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
||||||
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.15',
|
||||||
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0',
|
||||||
|
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
||||||
|
];
|
||||||
|
|
||||||
|
function pickUserAgent(randomize: boolean): string {
|
||||||
|
if (!randomize) return USER_AGENTS[0];
|
||||||
|
return USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
async function handleUpload(panel: vscode.WebviewPanel, filePaths: string[]) {
|
async function handleUpload(panel: vscode.WebviewPanel, filePaths: string[]) {
|
||||||
const cfg = vscode.workspace.getConfiguration('hplToolbox.applovin');
|
const cfg = vscode.workspace.getConfiguration('hplToolbox.applovin');
|
||||||
const cookie = (cfg.get<string>('cookie') || '').trim();
|
const cookie = (cfg.get<string>('cookie') || '').trim();
|
||||||
|
const delayMs = Math.max(0, cfg.get<number>('delayMs') ?? 1500);
|
||||||
|
const randomizeUA = cfg.get<boolean>('randomizeUserAgent') ?? true;
|
||||||
|
|
||||||
const paths = Array.isArray(filePaths) ? filePaths : [filePaths];
|
const paths = Array.isArray(filePaths) ? filePaths : [filePaths];
|
||||||
if (!paths.length) {
|
if (!paths.length) {
|
||||||
@@ -50,14 +82,17 @@ async function handleUpload(panel: vscode.WebviewPanel, filePaths: string[]) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const buildHeaders = (): Record<string, string> => {
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
'Accept': 'application/json, text/javascript, */*; q=0.01',
|
'Accept': 'application/json, text/javascript, */*; q=0.01',
|
||||||
'Origin': 'https://p.applov.in',
|
'Origin': 'https://p.applov.in',
|
||||||
'Referer': 'https://p.applov.in/playablePreview?create=1&qr=1',
|
'Referer': 'https://p.applov.in/playablePreview?create=1&qr=1',
|
||||||
'X-Requested-With': 'XMLHttpRequest',
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
'User-Agent': 'Mozilla/5.0 (HPL-Toolbox-VSCode)',
|
'User-Agent': pickUserAgent(randomizeUA),
|
||||||
};
|
};
|
||||||
if (cookie) headers['Cookie'] = `__Host-APPLOVINID=${cookie}`;
|
if (cookie) headers['Cookie'] = `__Host-APPLOVINID=${cookie}`;
|
||||||
|
return headers;
|
||||||
|
};
|
||||||
|
|
||||||
const buildForm = (filePath: string): FormData => {
|
const buildForm = (filePath: string): FormData => {
|
||||||
const buf = fs.readFileSync(filePath);
|
const buf = fs.readFileSync(filePath);
|
||||||
@@ -80,12 +115,17 @@ async function handleUpload(panel: vscode.WebviewPanel, filePaths: string[]) {
|
|||||||
if (!filePath || !fs.existsSync(filePath)) { reportError('File missing'); continue; }
|
if (!filePath || !fs.existsSync(filePath)) { reportError('File missing'); continue; }
|
||||||
if (!/\.html?$/i.test(filePath)) { reportError('Must be .html'); continue; }
|
if (!/\.html?$/i.test(filePath)) { reportError('Must be .html'); continue; }
|
||||||
|
|
||||||
|
if (i > 0 && delayMs > 0) {
|
||||||
|
panel.webview.postMessage({ type: 'status', message: `[${idx}/${paths.length}] Waiting ${delayMs}ms before ${fileName}...` });
|
||||||
|
await sleep(delayMs);
|
||||||
|
}
|
||||||
|
|
||||||
panel.webview.postMessage({ type: 'status', message: `[${idx}/${paths.length}] Validating ${fileName}...` });
|
panel.webview.postMessage({ type: 'status', message: `[${idx}/${paths.length}] Validating ${fileName}...` });
|
||||||
|
|
||||||
let res: Response;
|
let res: Response;
|
||||||
try {
|
try {
|
||||||
res = await fetch('https://p.applov.in/validateHTMLContent', {
|
res = await fetch('https://p.applov.in/validateHTMLContent', {
|
||||||
method: 'POST', body: buildForm(filePath), headers,
|
method: 'POST', body: buildForm(filePath), headers: buildHeaders(),
|
||||||
});
|
});
|
||||||
} catch (err: any) { reportError('Network error (validate): ' + (err?.message || err)); continue; }
|
} catch (err: any) { reportError('Network error (validate): ' + (err?.message || err)); continue; }
|
||||||
const validateText = await res.text();
|
const validateText = await res.text();
|
||||||
@@ -100,7 +140,7 @@ async function handleUpload(panel: vscode.WebviewPanel, filePaths: string[]) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
res = await fetch('https://p.applov.in/getCachedAdURL', {
|
res = await fetch('https://p.applov.in/getCachedAdURL', {
|
||||||
method: 'POST', body: buildForm(filePath), headers,
|
method: 'POST', body: buildForm(filePath), headers: buildHeaders(),
|
||||||
});
|
});
|
||||||
} catch (err: any) { reportError('Network error (cache): ' + (err?.message || err)); continue; }
|
} catch (err: any) { reportError('Network error (cache): ' + (err?.message || err)); continue; }
|
||||||
const cacheText = await res.text();
|
const cacheText = await res.text();
|
||||||
@@ -124,6 +164,79 @@ async function handleUpload(panel: vscode.WebviewPanel, filePaths: string[]) {
|
|||||||
panel.webview.postMessage({ type: 'done' });
|
panel.webview.postMessage({ type: 'done' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPickerDefaultUri(): vscode.Uri | undefined {
|
||||||
|
const folders = vscode.workspace.workspaceFolders;
|
||||||
|
if (!folders || folders.length === 0) return undefined;
|
||||||
|
const root = folders[0].uri.fsPath;
|
||||||
|
const dist = path.join(root, 'dist');
|
||||||
|
if (fs.existsSync(dist) && fs.statSync(dist).isDirectory()) {
|
||||||
|
return vscode.Uri.file(dist);
|
||||||
|
}
|
||||||
|
return vscode.Uri.file(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
function qrBaseName(name: string): string {
|
||||||
|
return (name || 'qr').replace(/\.html?$/i, '').replace(/[\\/:*?"<>|]/g, '_') || 'qr';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadQr(url: string, destPath: string): Promise<void> {
|
||||||
|
const res = await fetch(url);
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
const buf = Buffer.from(await res.arrayBuffer());
|
||||||
|
await fs.promises.writeFile(destPath, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSaveQr(panel: vscode.WebviewPanel, url: string, name: string) {
|
||||||
|
if (!url) return;
|
||||||
|
const base = qrBaseName(name);
|
||||||
|
const defaultUri = vscode.Uri.file(path.join(require('os').homedir(), 'Downloads', `${base}.png`));
|
||||||
|
const target = await vscode.window.showSaveDialog({
|
||||||
|
defaultUri,
|
||||||
|
filters: { 'PNG Image': ['png'] },
|
||||||
|
saveLabel: 'Save QR',
|
||||||
|
});
|
||||||
|
if (!target) return;
|
||||||
|
try {
|
||||||
|
await downloadQr(url, target.fsPath);
|
||||||
|
panel.webview.postMessage({ type: 'status', message: `Saved QR: ${target.fsPath}` });
|
||||||
|
} catch (err: any) {
|
||||||
|
panel.webview.postMessage({ type: 'error', message: 'Save QR failed: ' + (err?.message || err) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSaveAllQrs(panel: vscode.WebviewPanel, items: Array<{ name: string; url: string }>) {
|
||||||
|
if (!items.length) return;
|
||||||
|
const picked = await vscode.window.showOpenDialog({
|
||||||
|
canSelectFiles: false,
|
||||||
|
canSelectFolders: true,
|
||||||
|
canSelectMany: false,
|
||||||
|
openLabel: 'Save QRs Here',
|
||||||
|
});
|
||||||
|
if (!picked || !picked.length) return;
|
||||||
|
const dir = picked[0].fsPath;
|
||||||
|
let saved = 0;
|
||||||
|
const errors: string[] = [];
|
||||||
|
for (const it of items) {
|
||||||
|
const base = qrBaseName(it.name);
|
||||||
|
let dest = path.join(dir, `${base}.png`);
|
||||||
|
let n = 1;
|
||||||
|
while (fs.existsSync(dest)) {
|
||||||
|
dest = path.join(dir, `${base} (${n++}).png`);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await downloadQr(it.url, dest);
|
||||||
|
saved++;
|
||||||
|
} catch (err: any) {
|
||||||
|
errors.push(`${it.name}: ${err?.message || err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (errors.length) {
|
||||||
|
panel.webview.postMessage({ type: 'error', message: `Saved ${saved}/${items.length}. Errors:\n` + errors.join('\n') });
|
||||||
|
} else {
|
||||||
|
panel.webview.postMessage({ type: 'status', message: `Saved ${saved} QR${saved === 1 ? '' : 's'} to ${dir}` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getHtml(): string {
|
function getHtml(): string {
|
||||||
return `<!DOCTYPE html>
|
return `<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
@@ -163,11 +276,15 @@ function getHtml(): string {
|
|||||||
<p style="opacity:0.8;font-size:12px;margin-top:0;">Upload HTML files to p.applov.in and get QR codes to scan with the AppLovin Playable Preview app (iOS/Android).</p>
|
<p style="opacity:0.8;font-size:12px;margin-top:0;">Upload HTML files to p.applov.in and get QR codes to scan with the AppLovin Playable Preview app (iOS/Android).</p>
|
||||||
|
|
||||||
<div class="file-cell" style="margin-bottom:8px;">
|
<div class="file-cell" style="margin-bottom:8px;">
|
||||||
<button id="pick" class="secondary">Choose files...</button>
|
<button id="pick" class="secondary">Add files...</button>
|
||||||
|
<button id="clear" class="secondary" style="display:none;">Clear</button>
|
||||||
<span class="file-name" id="fileName">(no files)</span>
|
<span class="file-name" id="fileName">(no files)</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="fileList" style="margin-bottom:8px;font-size:12px;opacity:0.85;"></div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button id="upload">Upload to AppLovin</button>
|
<button id="upload">Upload to AppLovin</button>
|
||||||
|
<button id="saveAll" class="secondary" style="display:none;">Save All QRs...</button>
|
||||||
|
<button id="regenAll" class="secondary" style="display:none;">Regenerate QRs</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="status"></div>
|
<div id="status"></div>
|
||||||
<div id="results"></div>
|
<div id="results"></div>
|
||||||
@@ -175,13 +292,51 @@ function getHtml(): string {
|
|||||||
<script>
|
<script>
|
||||||
const vscode = acquireVsCodeApi();
|
const vscode = acquireVsCodeApi();
|
||||||
const pickBtn = document.getElementById('pick');
|
const pickBtn = document.getElementById('pick');
|
||||||
|
const clearBtn = document.getElementById('clear');
|
||||||
const uploadBtn = document.getElementById('upload');
|
const uploadBtn = document.getElementById('upload');
|
||||||
const fileNameEl = document.getElementById('fileName');
|
const fileNameEl = document.getElementById('fileName');
|
||||||
|
const fileListEl = document.getElementById('fileList');
|
||||||
const statusEl = document.getElementById('status');
|
const statusEl = document.getElementById('status');
|
||||||
const resultsEl = document.getElementById('results');
|
const resultsEl = document.getElementById('results');
|
||||||
|
const saveAllBtn = document.getElementById('saveAll');
|
||||||
|
const regenAllBtn = document.getElementById('regenAll');
|
||||||
let selectedPaths = [];
|
let selectedPaths = [];
|
||||||
|
let resultItems = [];
|
||||||
|
|
||||||
|
function basename(p) {
|
||||||
|
const i = Math.max(p.lastIndexOf('/'), p.lastIndexOf('\\\\'));
|
||||||
|
return i >= 0 ? p.slice(i + 1) : p;
|
||||||
|
}
|
||||||
|
function renderSelection() {
|
||||||
|
if (!selectedPaths.length) {
|
||||||
|
fileNameEl.textContent = '(no files)';
|
||||||
|
fileListEl.innerHTML = '';
|
||||||
|
clearBtn.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fileNameEl.textContent = selectedPaths.length + ' file' + (selectedPaths.length === 1 ? '' : 's');
|
||||||
|
fileListEl.innerHTML = selectedPaths.map(p => '<div>' + escapeHtml(basename(p)) + '</div>').join('');
|
||||||
|
clearBtn.style.display = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
saveAllBtn.addEventListener('click', () => {
|
||||||
|
if (!resultItems.length) return;
|
||||||
|
vscode.postMessage({ type: 'saveAllQrs', items: resultItems });
|
||||||
|
});
|
||||||
|
|
||||||
|
regenAllBtn.addEventListener('click', () => {
|
||||||
|
const imgs = resultsEl.querySelectorAll('img[data-qr]');
|
||||||
|
imgs.forEach(img => {
|
||||||
|
const base = img.getAttribute('data-qr');
|
||||||
|
img.src = base + '&_=' + Date.now();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
pickBtn.addEventListener('click', () => vscode.postMessage({ type: 'pickFiles' }));
|
pickBtn.addEventListener('click', () => vscode.postMessage({ type: 'pickFiles' }));
|
||||||
|
clearBtn.addEventListener('click', () => {
|
||||||
|
selectedPaths = [];
|
||||||
|
renderSelection();
|
||||||
|
});
|
||||||
|
|
||||||
uploadBtn.addEventListener('click', () => {
|
uploadBtn.addEventListener('click', () => {
|
||||||
statusEl.textContent = '';
|
statusEl.textContent = '';
|
||||||
@@ -191,6 +346,9 @@ function getHtml(): string {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
uploadBtn.disabled = true;
|
uploadBtn.disabled = true;
|
||||||
|
resultItems = [];
|
||||||
|
saveAllBtn.style.display = 'none';
|
||||||
|
regenAllBtn.style.display = 'none';
|
||||||
vscode.postMessage({ type: 'upload', paths: selectedPaths });
|
vscode.postMessage({ type: 'upload', paths: selectedPaths });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -201,13 +359,14 @@ function getHtml(): string {
|
|||||||
window.addEventListener('message', (e) => {
|
window.addEventListener('message', (e) => {
|
||||||
const m = e.data;
|
const m = e.data;
|
||||||
if (m.type === 'filesSelected') {
|
if (m.type === 'filesSelected') {
|
||||||
selectedPaths = m.files.map(f => f.path);
|
const added = m.files.map(f => f.path).filter(p => !selectedPaths.includes(p));
|
||||||
const names = m.files.map(f => f.name);
|
selectedPaths = selectedPaths.concat(added);
|
||||||
fileNameEl.textContent = names.length === 1
|
renderSelection();
|
||||||
? names[0]
|
|
||||||
: names.length + ' files: ' + names.slice(0, 3).join(', ') + (names.length > 3 ? '...' : '');
|
|
||||||
} else if (m.type === 'start') {
|
} else if (m.type === 'start') {
|
||||||
resultsEl.innerHTML = '';
|
resultsEl.innerHTML = '';
|
||||||
|
resultItems = [];
|
||||||
|
saveAllBtn.style.display = 'none';
|
||||||
|
regenAllBtn.style.display = 'none';
|
||||||
} else if (m.type === 'status') {
|
} else if (m.type === 'status') {
|
||||||
statusEl.textContent = m.message;
|
statusEl.textContent = m.message;
|
||||||
statusEl.className = '';
|
statusEl.className = '';
|
||||||
@@ -230,27 +389,28 @@ function getHtml(): string {
|
|||||||
wrap.innerHTML =
|
wrap.innerHTML =
|
||||||
'<div style="font-weight:600;margin-bottom:8px;word-break:break-all;">' + escapeHtml(m.name) + '</div>' +
|
'<div style="font-weight:600;margin-bottom:8px;word-break:break-all;">' + escapeHtml(m.name) + '</div>' +
|
||||||
'<div style="text-align:center;margin-bottom:8px;">' +
|
'<div style="text-align:center;margin-bottom:8px;">' +
|
||||||
'<img src="' + m.qrImg + '" alt="QR" style="background:#fff;padding:8px;border-radius:4px;max-width:100%;" />' +
|
'<img src="' + m.qrImg + '" data-qr="' + m.qrImg + '" alt="QR" style="background:#fff;padding:8px;border-radius:4px;max-width:100%;" />' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div style="font-size:11px;opacity:0.8;word-break:break-all;">Hash: ' + escapeHtml(m.hash) + '</div>';
|
'<div style="font-size:11px;opacity:0.8;word-break:break-all;">Hash: ' + escapeHtml(m.hash) + '</div>';
|
||||||
const actions = document.createElement('div');
|
const actions = document.createElement('div');
|
||||||
actions.className = 'result-actions';
|
actions.className = 'result-actions';
|
||||||
const copyHash = document.createElement('button');
|
|
||||||
copyHash.textContent = 'Copy Hash';
|
|
||||||
copyHash.onclick = () => vscode.postMessage({ type: 'copy', text: m.hash });
|
|
||||||
const copyPreview = document.createElement('button');
|
|
||||||
copyPreview.textContent = 'Copy URL';
|
|
||||||
copyPreview.className = 'secondary';
|
|
||||||
copyPreview.onclick = () => vscode.postMessage({ type: 'copy', text: m.previewUrl });
|
|
||||||
const openPreview = document.createElement('button');
|
const openPreview = document.createElement('button');
|
||||||
openPreview.textContent = 'Open';
|
openPreview.textContent = 'Open';
|
||||||
openPreview.className = 'secondary';
|
openPreview.className = 'secondary';
|
||||||
openPreview.onclick = () => vscode.postMessage({ type: 'open', text: m.previewUrl });
|
openPreview.onclick = () => vscode.postMessage({ type: 'open', text: m.previewUrl });
|
||||||
actions.appendChild(copyHash);
|
const saveQr = document.createElement('button');
|
||||||
actions.appendChild(copyPreview);
|
saveQr.textContent = 'Save QR';
|
||||||
|
saveQr.className = 'secondary';
|
||||||
|
saveQr.onclick = () => vscode.postMessage({ type: 'saveQr', url: m.qrImg, name: m.name });
|
||||||
actions.appendChild(openPreview);
|
actions.appendChild(openPreview);
|
||||||
|
actions.appendChild(saveQr);
|
||||||
wrap.appendChild(actions);
|
wrap.appendChild(actions);
|
||||||
resultsEl.appendChild(wrap);
|
resultsEl.appendChild(wrap);
|
||||||
|
resultItems.push({ name: m.name, url: m.qrImg });
|
||||||
|
if (resultItems.length >= 2) {
|
||||||
|
saveAllBtn.style.display = '';
|
||||||
|
regenAllBtn.style.display = '';
|
||||||
|
}
|
||||||
} else if (m.type === 'done') {
|
} else if (m.type === 'done') {
|
||||||
statusEl.innerHTML = '<span class="ok">Done.</span>';
|
statusEl.innerHTML = '<span class="ok">Done.</span>';
|
||||||
uploadBtn.disabled = false;
|
uploadBtn.disabled = false;
|
||||||
|
|||||||
@@ -17,18 +17,16 @@ export function openPlecUpload(_context: vscode.ExtensionContext) {
|
|||||||
|
|
||||||
if (msg.type === 'pickFile') {
|
if (msg.type === 'pickFile') {
|
||||||
const picked = await vscode.window.showOpenDialog({
|
const picked = await vscode.window.showOpenDialog({
|
||||||
canSelectMany: false,
|
canSelectMany: true,
|
||||||
filters: { HTML: ['html', 'htm'] },
|
filters: { HTML: ['html', 'htm'] },
|
||||||
openLabel: 'Select',
|
openLabel: 'Select',
|
||||||
defaultUri: getPickerDefaultUri(),
|
defaultUri: getPickerDefaultUri(),
|
||||||
});
|
});
|
||||||
if (picked && picked[0]) {
|
if (picked && picked.length) {
|
||||||
const fp = picked[0].fsPath;
|
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
type: 'fileSelected',
|
type: 'filesSelected',
|
||||||
rowId: msg.rowId,
|
rowId: msg.rowId,
|
||||||
path: fp,
|
files: picked.map(u => ({ path: u.fsPath, name: path.basename(u.fsPath) })),
|
||||||
name: path.basename(fp),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -233,6 +231,15 @@ function getHtml(): string {
|
|||||||
const vscode = acquireVsCodeApi();
|
const vscode = acquireVsCodeApi();
|
||||||
let nextId = 1;
|
let nextId = 1;
|
||||||
const tbody = document.querySelector('#rows tbody');
|
const tbody = document.querySelector('#rows tbody');
|
||||||
|
|
||||||
|
function parseIterationName(filename) {
|
||||||
|
const base = filename.replace(/\\.html?$/i, '');
|
||||||
|
const tokens = base.split('_');
|
||||||
|
for (const t of tokens) {
|
||||||
|
if (/^(full|\\d+clk|\\d+s(?:ec)?)$/i.test(t)) return t.toLowerCase();
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
}
|
||||||
const statusEl = document.getElementById('status');
|
const statusEl = document.getElementById('status');
|
||||||
const resultsEl = document.getElementById('results');
|
const resultsEl = document.getElementById('results');
|
||||||
|
|
||||||
@@ -286,13 +293,32 @@ function getHtml(): string {
|
|||||||
|
|
||||||
window.addEventListener('message', (e) => {
|
window.addEventListener('message', (e) => {
|
||||||
const m = e.data;
|
const m = e.data;
|
||||||
if (m.type === 'fileSelected') {
|
if (m.type === 'filesSelected') {
|
||||||
const tr = tbody.querySelector('tr[data-id="' + m.rowId + '"]');
|
const firstTr = tbody.querySelector('tr[data-id="' + m.rowId + '"]');
|
||||||
|
const affected = [];
|
||||||
|
m.files.forEach(function(f, i) {
|
||||||
|
let tr;
|
||||||
|
if (i === 0) {
|
||||||
|
tr = firstTr;
|
||||||
|
} else {
|
||||||
|
addRow();
|
||||||
|
tr = tbody.lastElementChild;
|
||||||
|
}
|
||||||
if (!tr) return;
|
if (!tr) return;
|
||||||
tr.querySelector('[data-path]').value = m.path;
|
tr.querySelector('[data-path]').value = f.path;
|
||||||
tr.querySelector('[data-name]').textContent = m.name;
|
tr.querySelector('[data-name]').textContent = f.name;
|
||||||
const iter = tr.querySelector('[data-iter]');
|
const iter = tr.querySelector('[data-iter]');
|
||||||
if (!iter.value) iter.value = m.name.replace(/\\.html?$/i, '');
|
if (!iter.value) iter.value = parseIterationName(f.name);
|
||||||
|
affected.push(tr);
|
||||||
|
});
|
||||||
|
if (affected.length > 1) {
|
||||||
|
const iters = affected.map(r => r.querySelector('[data-iter]').value);
|
||||||
|
if (iters[0] !== '' && iters.every(n => n === iters[0])) {
|
||||||
|
affected.forEach((r, i) => {
|
||||||
|
r.querySelector('[data-iter]').value = String(i + 1).padStart(2, '0') + '_' + iters[0];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (m.type === 'status') {
|
} else if (m.type === 'status') {
|
||||||
statusEl.textContent = m.message;
|
statusEl.textContent = m.message;
|
||||||
statusEl.className = '';
|
statusEl.className = '';
|
||||||
|
|||||||
@@ -5,25 +5,49 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"math/rand"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
urlpkg "net/url"
|
urlpkg "net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var applovinUserAgents = []string{
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0",
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.15",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0",
|
||||||
|
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
||||||
|
}
|
||||||
|
|
||||||
|
func pickApplovinUserAgent(randomize bool) string {
|
||||||
|
if !randomize {
|
||||||
|
return applovinUserAgents[0]
|
||||||
|
}
|
||||||
|
return applovinUserAgents[rand.Intn(len(applovinUserAgents))]
|
||||||
|
}
|
||||||
|
|
||||||
func ApplovinPage(w http.ResponseWriter, r *http.Request) {
|
func ApplovinPage(w http.ResponseWriter, r *http.Request) {
|
||||||
body := `
|
body := `
|
||||||
<h2>AppLovin Playable Preview (QR)</h2>
|
<h2>AppLovin Playable Preview (QR)</h2>
|
||||||
<p class="hint" style="margin-top:0;">Upload HTML files to p.applov.in and get QR codes to scan with the AppLovin Playable Preview app.</p>
|
<p class="hint" style="margin-top:0;">Upload HTML files to p.applov.in and get QR codes to scan with the AppLovin Playable Preview app.</p>
|
||||||
|
|
||||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
|
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
|
||||||
<button id="pick" class="secondary">Choose files...</button>
|
<button id="pick" class="secondary">Add files...</button>
|
||||||
|
<button id="clear" class="secondary" style="display:none;">Clear</button>
|
||||||
<span id="fileName" class="hint">(no files)</span>
|
<span id="fileName" class="hint">(no files)</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="fileList" style="margin-bottom:8px;font-size:12px;opacity:0.85;"></div>
|
||||||
<div style="display:flex;gap:8px;">
|
<div style="display:flex;gap:8px;">
|
||||||
<button id="upload">Upload to AppLovin</button>
|
<button id="upload">Upload to AppLovin</button>
|
||||||
|
<button id="saveAll" class="secondary" style="display:none;">Save All QRs...</button>
|
||||||
|
<button id="regenAll" class="secondary" style="display:none;">Regenerate QRs</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="status" style="margin-top:12px;min-height:20px;"></div>
|
<div id="status" style="margin-top:12px;min-height:20px;"></div>
|
||||||
@@ -36,21 +60,70 @@ func ApplovinPage(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
const pickBtn = document.getElementById('pick');
|
const pickBtn = document.getElementById('pick');
|
||||||
|
const clearBtn = document.getElementById('clear');
|
||||||
const uploadBtn = document.getElementById('upload');
|
const uploadBtn = document.getElementById('upload');
|
||||||
const fileNameEl = document.getElementById('fileName');
|
const fileNameEl = document.getElementById('fileName');
|
||||||
|
const fileListEl = document.getElementById('fileList');
|
||||||
const statusEl = document.getElementById('status');
|
const statusEl = document.getElementById('status');
|
||||||
const resultsEl = document.getElementById('results');
|
const resultsEl = document.getElementById('results');
|
||||||
|
const saveAllBtn = document.getElementById('saveAll');
|
||||||
|
const regenAllBtn = document.getElementById('regenAll');
|
||||||
let selectedPaths = [];
|
let selectedPaths = [];
|
||||||
|
let resultItems = [];
|
||||||
|
|
||||||
|
function basename(p) {
|
||||||
|
const i = Math.max(p.lastIndexOf('/'), p.lastIndexOf('\\'));
|
||||||
|
return i >= 0 ? p.slice(i + 1) : p;
|
||||||
|
}
|
||||||
|
function renderSelection() {
|
||||||
|
if (!selectedPaths.length) {
|
||||||
|
fileNameEl.textContent = '(no files)';
|
||||||
|
fileListEl.innerHTML = '';
|
||||||
|
clearBtn.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fileNameEl.textContent = selectedPaths.length + ' file' + (selectedPaths.length === 1 ? '' : 's');
|
||||||
|
fileListEl.innerHTML = selectedPaths.map(p => '<div>' + escapeHtml(basename(p)) + '</div>').join('');
|
||||||
|
clearBtn.style.display = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
saveAllBtn.addEventListener('click', async () => {
|
||||||
|
if (!resultItems.length) return;
|
||||||
|
saveAllBtn.disabled = true;
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/applovin/saveAllQrs', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type':'application/json'},
|
||||||
|
body: JSON.stringify({ items: resultItems }),
|
||||||
|
});
|
||||||
|
const j = await r.json();
|
||||||
|
statusEl.innerHTML = j.ok
|
||||||
|
? '<span class="ok">' + escapeHtml(j.message || 'Saved.') + '</span>'
|
||||||
|
: '<span class="err">' + escapeHtml(j.message || 'Save failed.') + '</span>';
|
||||||
|
} finally {
|
||||||
|
saveAllBtn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
regenAllBtn.addEventListener('click', () => {
|
||||||
|
resultsEl.querySelectorAll('img[data-qr]').forEach(img => {
|
||||||
|
const base = img.getAttribute('data-qr');
|
||||||
|
img.src = base + '&_=' + Date.now();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
clearBtn.addEventListener('click', () => {
|
||||||
|
selectedPaths = [];
|
||||||
|
renderSelection();
|
||||||
|
});
|
||||||
|
|
||||||
pickBtn.addEventListener('click', async () => {
|
pickBtn.addEventListener('click', async () => {
|
||||||
const r = await fetch('/api/applovin/pick', { method: 'POST' });
|
const r = await fetch('/api/applovin/pick', { method: 'POST' });
|
||||||
const j = await r.json();
|
const j = await r.json();
|
||||||
if (j.files && j.files.length) {
|
if (j.files && j.files.length) {
|
||||||
selectedPaths = j.files.map(f => f.path);
|
const added = j.files.map(f => f.path).filter(p => !selectedPaths.includes(p));
|
||||||
const names = j.files.map(f => f.name);
|
selectedPaths = selectedPaths.concat(added);
|
||||||
fileNameEl.textContent = names.length === 1
|
renderSelection();
|
||||||
? names[0]
|
|
||||||
: names.length + ' files: ' + names.slice(0,3).join(', ') + (names.length > 3 ? '...' : '');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -62,6 +135,9 @@ uploadBtn.addEventListener('click', async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
uploadBtn.disabled = true;
|
uploadBtn.disabled = true;
|
||||||
|
resultItems = [];
|
||||||
|
saveAllBtn.style.display = 'none';
|
||||||
|
regenAllBtn.style.display = 'none';
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/applovin/upload', {
|
const res = await fetch('/api/applovin/upload', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -113,25 +189,42 @@ function handle(m) {
|
|||||||
wrap.innerHTML =
|
wrap.innerHTML =
|
||||||
'<div style="font-weight:600;margin-bottom:8px;">' + escapeHtml(m.name) + '</div>' +
|
'<div style="font-weight:600;margin-bottom:8px;">' + escapeHtml(m.name) + '</div>' +
|
||||||
'<div style="text-align:center;margin-bottom:8px;">' +
|
'<div style="text-align:center;margin-bottom:8px;">' +
|
||||||
'<img src="' + m.qrImg + '" style="background:#fff;padding:8px;border-radius:4px;max-width:100%;" />' +
|
'<img src="' + m.qrImg + '" data-qr="' + m.qrImg + '" style="background:#fff;padding:8px;border-radius:4px;max-width:100%;" />' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div style="font-size:11px;opacity:0.8;">Hash: ' + escapeHtml(m.hash) + '</div>';
|
'<div style="font-size:11px;opacity:0.8;">Hash: ' + escapeHtml(m.hash) + '</div>';
|
||||||
const actions = document.createElement('div');
|
const actions = document.createElement('div');
|
||||||
actions.className = 'result-actions';
|
actions.className = 'result-actions';
|
||||||
const copyHash = document.createElement('button');
|
|
||||||
copyHash.textContent = 'Copy Hash';
|
|
||||||
copyHash.onclick = () => fetch('/api/clipboard', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({text:m.hash})});
|
|
||||||
const copyUrl = document.createElement('button');
|
|
||||||
copyUrl.textContent = 'Copy URL';
|
|
||||||
copyUrl.className = 'secondary';
|
|
||||||
copyUrl.onclick = () => fetch('/api/clipboard', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({text:m.previewUrl})});
|
|
||||||
const openBtn = document.createElement('button');
|
const openBtn = document.createElement('button');
|
||||||
openBtn.textContent = 'Open';
|
openBtn.textContent = 'Open';
|
||||||
openBtn.className = 'secondary';
|
openBtn.className = 'secondary';
|
||||||
openBtn.onclick = () => fetch('/api/open', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url:m.previewUrl})});
|
openBtn.onclick = () => fetch('/api/open', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url:m.previewUrl})});
|
||||||
actions.appendChild(copyHash); actions.appendChild(copyUrl); actions.appendChild(openBtn);
|
const saveQr = document.createElement('button');
|
||||||
|
saveQr.textContent = 'Save QR';
|
||||||
|
saveQr.className = 'secondary';
|
||||||
|
saveQr.onclick = async () => {
|
||||||
|
saveQr.disabled = true;
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/applovin/saveQr', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type':'application/json'},
|
||||||
|
body: JSON.stringify({ name: m.name, url: m.qrImg }),
|
||||||
|
});
|
||||||
|
const j = await r.json();
|
||||||
|
statusEl.innerHTML = j.ok
|
||||||
|
? '<span class="ok">' + escapeHtml(j.message || 'Saved.') + '</span>'
|
||||||
|
: '<span class="err">' + escapeHtml(j.message || 'Save failed.') + '</span>';
|
||||||
|
} finally {
|
||||||
|
saveQr.disabled = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
actions.appendChild(openBtn); actions.appendChild(saveQr);
|
||||||
wrap.appendChild(actions);
|
wrap.appendChild(actions);
|
||||||
resultsEl.appendChild(wrap);
|
resultsEl.appendChild(wrap);
|
||||||
|
resultItems.push({ name: m.name, url: m.qrImg });
|
||||||
|
if (resultItems.length >= 2) {
|
||||||
|
saveAllBtn.style.display = '';
|
||||||
|
regenAllBtn.style.display = '';
|
||||||
|
}
|
||||||
} else if (m.type === 'done') {
|
} else if (m.type === 'done') {
|
||||||
statusEl.innerHTML = '<span class="ok">Done.</span>';
|
statusEl.innerHTML = '<span class="ok">Done.</span>';
|
||||||
}
|
}
|
||||||
@@ -143,11 +236,16 @@ function handle(m) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ApplovinPick(w http.ResponseWriter, r *http.Request) {
|
func ApplovinPick(w http.ResponseWriter, r *http.Request) {
|
||||||
|
cfg := LoadConfig()
|
||||||
picked := PickFiles(
|
picked := PickFiles(
|
||||||
"Select HTML files",
|
"Select HTML files",
|
||||||
[]FileFilter{{Name: "HTML", Extensions: []string{"html", "htm"}}},
|
[]FileFilter{{Name: "HTML", Extensions: []string{"html", "htm"}}},
|
||||||
true, "",
|
true, cfg.LastPickDir,
|
||||||
)
|
)
|
||||||
|
if len(picked) > 0 {
|
||||||
|
cfg.LastPickDir = filepath.Dir(picked[0])
|
||||||
|
_ = SaveConfig(cfg)
|
||||||
|
}
|
||||||
files := make([]map[string]string, 0, len(picked))
|
files := make([]map[string]string, 0, len(picked))
|
||||||
for _, p := range picked {
|
for _, p := range picked {
|
||||||
files = append(files, map[string]string{"path": p, "name": filepath.Base(p)})
|
files = append(files, map[string]string{"path": p, "name": filepath.Base(p)})
|
||||||
@@ -163,7 +261,16 @@ func ApplovinUpload(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cookie := LoadConfig().Applovin.Cookie
|
cfg := LoadConfig().Applovin
|
||||||
|
cookie := cfg.Cookie
|
||||||
|
delayMs := cfg.DelayMs
|
||||||
|
if delayMs < 0 {
|
||||||
|
delayMs = 0
|
||||||
|
}
|
||||||
|
randomizeUA := true
|
||||||
|
if cfg.RandomizeUserAgent != nil {
|
||||||
|
randomizeUA = *cfg.RandomizeUserAgent
|
||||||
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/x-ndjson")
|
w.Header().Set("Content-Type", "application/x-ndjson")
|
||||||
flusher, _ := w.(http.Flusher)
|
flusher, _ := w.(http.Flusher)
|
||||||
@@ -208,7 +315,7 @@ func ApplovinUpload(w http.ResponseWriter, r *http.Request) {
|
|||||||
req.Header.Set("Origin", "https://p.applov.in")
|
req.Header.Set("Origin", "https://p.applov.in")
|
||||||
req.Header.Set("Referer", "https://p.applov.in/playablePreview?create=1&qr=1")
|
req.Header.Set("Referer", "https://p.applov.in/playablePreview?create=1&qr=1")
|
||||||
req.Header.Set("X-Requested-With", "XMLHttpRequest")
|
req.Header.Set("X-Requested-With", "XMLHttpRequest")
|
||||||
req.Header.Set("User-Agent", "Mozilla/5.0 (HPL-Toolbox-Standalone)")
|
req.Header.Set("User-Agent", pickApplovinUserAgent(randomizeUA))
|
||||||
if cookie != "" {
|
if cookie != "" {
|
||||||
req.Header.Set("Cookie", "__Host-APPLOVINID="+cookie)
|
req.Header.Set("Cookie", "__Host-APPLOVINID="+cookie)
|
||||||
}
|
}
|
||||||
@@ -237,6 +344,11 @@ func ApplovinUpload(w http.ResponseWriter, r *http.Request) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if i > 0 && delayMs > 0 {
|
||||||
|
send(map[string]any{"type": "status", "message": fmt.Sprintf("[%d/%d] Waiting %dms before %s...", idx, len(req.Paths), delayMs, fileName)})
|
||||||
|
time.Sleep(time.Duration(delayMs) * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
send(map[string]any{"type": "status", "message": fmt.Sprintf("[%d/%d] Validating %s...", idx, len(req.Paths), fileName)})
|
send(map[string]any{"type": "status", "message": fmt.Sprintf("[%d/%d] Validating %s...", idx, len(req.Paths), fileName)})
|
||||||
|
|
||||||
body, ct, err := buildForm(filePath)
|
body, ct, err := buildForm(filePath)
|
||||||
@@ -300,3 +412,114 @@ func ApplovinUpload(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
send(map[string]any{"type": "done"})
|
send(map[string]any{"type": "done"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func qrBaseName(name string) string {
|
||||||
|
base := name
|
||||||
|
if base == "" {
|
||||||
|
base = "qr"
|
||||||
|
}
|
||||||
|
base = regexp.MustCompile(`(?i)\.html?$`).ReplaceAllString(base, "")
|
||||||
|
base = regexp.MustCompile(`[\\/:*?"<>|]`).ReplaceAllString(base, "_")
|
||||||
|
if base == "" {
|
||||||
|
base = "qr"
|
||||||
|
}
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadQR(url, destPath string) error {
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
return fmt.Errorf("HTTP %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
data, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(destPath, data, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApplovinSaveQR(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
writeJSON(w, map[string]any{"ok": false, "message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.URL == "" {
|
||||||
|
writeJSON(w, map[string]any{"ok": false, "message": "Missing URL"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
base := qrBaseName(req.Name)
|
||||||
|
dest := PickSaveFile("Save QR", base+".png", []FileFilter{{Name: "PNG Image", Extensions: []string{"png"}}}, "")
|
||||||
|
if dest == "" {
|
||||||
|
writeJSON(w, map[string]any{"ok": false, "message": "Cancelled"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := downloadQR(req.URL, dest); err != nil {
|
||||||
|
writeJSON(w, map[string]any{"ok": false, "message": "Save failed: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, map[string]any{"ok": true, "message": "Saved QR: " + dest})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApplovinSaveAllQRs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req struct {
|
||||||
|
Items []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
} `json:"items"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
writeJSON(w, map[string]any{"ok": false, "message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(req.Items) == 0 {
|
||||||
|
writeJSON(w, map[string]any{"ok": false, "message": "No items"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dir := PickFolder("Choose folder to save QRs", "")
|
||||||
|
if dir == "" {
|
||||||
|
writeJSON(w, map[string]any{"ok": false, "message": "Cancelled"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
saved := 0
|
||||||
|
errs := []string{}
|
||||||
|
for _, it := range req.Items {
|
||||||
|
base := qrBaseName(it.Name)
|
||||||
|
dest := filepath.Join(dir, base+".png")
|
||||||
|
n := 1
|
||||||
|
for fileExists(dest) {
|
||||||
|
dest = filepath.Join(dir, fmt.Sprintf("%s (%d).png", base, n))
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
if err := downloadQR(it.URL, dest); err != nil {
|
||||||
|
errs = append(errs, it.Name+": "+err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
saved++
|
||||||
|
}
|
||||||
|
if len(errs) > 0 {
|
||||||
|
writeJSON(w, map[string]any{
|
||||||
|
"ok": false,
|
||||||
|
"message": fmt.Sprintf("Saved %d/%d. Errors: %s", saved, len(req.Items), strings.Join(errs, "; ")),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, map[string]any{
|
||||||
|
"ok": true,
|
||||||
|
"message": fmt.Sprintf("Saved %d QR%s to %s", saved, plural(saved), dir),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func plural(n int) string {
|
||||||
|
if n == 1 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return "s"
|
||||||
|
}
|
||||||
|
|||||||
BIN
standalone/hpltoolbox.exe
Normal file
BIN
standalone/hpltoolbox.exe
Normal file
Binary file not shown.
@@ -17,6 +17,7 @@ const SharedCSS = `
|
|||||||
.topbar a { color: #9cdcfe; text-decoration: none; padding: 4px 10px; border-radius: 3px; }
|
.topbar a { color: #9cdcfe; text-decoration: none; padding: 4px 10px; border-radius: 3px; }
|
||||||
.topbar a:hover { background: #2a2d2e; }
|
.topbar a:hover { background: #2a2d2e; }
|
||||||
.topbar a.active { background: #094771; color: #fff; }
|
.topbar a.active { background: #094771; color: #fff; }
|
||||||
|
.topbar-version { margin-left: auto; font-size: 11px; opacity: 0.45; letter-spacing: 0.4px; }
|
||||||
.content { padding: 16px; max-width: 980px; margin: 0 auto; }
|
.content { padding: 16px; max-width: 980px; margin: 0 auto; }
|
||||||
h2 { margin-top: 0; font-weight: 500; }
|
h2 { margin-top: 0; font-weight: 500; }
|
||||||
input[type=text], input[type=date], select, textarea {
|
input[type=text], input[type=date], select, textarea {
|
||||||
@@ -67,7 +68,7 @@ func Page(activePath, title, body string) string {
|
|||||||
<title>` + title + ` — HPL Toolbox</title>
|
<title>` + title + ` — HPL Toolbox</title>
|
||||||
<style>` + SharedCSS + `</style>
|
<style>` + SharedCSS + `</style>
|
||||||
</head><body>
|
</head><body>
|
||||||
<div class="topbar">` + tabs.String() + `</div>
|
<div class="topbar">` + tabs.String() + `<span class="topbar-version">v` + AppVersion + `</span></div>
|
||||||
<div class="content">` + body + `</div>
|
<div class="content">` + body + `</div>
|
||||||
</body></html>`
|
</body></html>`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,12 @@ import (
|
|||||||
webview "github.com/jchv/go-webview2"
|
webview "github.com/jchv/go-webview2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var AppVersion = "dev"
|
||||||
|
|
||||||
var currentHwnd atomic.Uintptr
|
var currentHwnd atomic.Uintptr
|
||||||
|
|
||||||
func setupFileLogging() {
|
func setupFileLogging() {
|
||||||
logPath := filepath.Join(appDir(), "debug.log")
|
logPath := filepath.Join(userDataDir(), "debug.log")
|
||||||
f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -57,11 +59,18 @@ func main() {
|
|||||||
}
|
}
|
||||||
defer removeLockFile()
|
defer removeLockFile()
|
||||||
|
|
||||||
|
webviewTmp, err := os.MkdirTemp("", "hpltoolbox-wv2-*")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create webview temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(webviewTmp)
|
||||||
|
|
||||||
w := webview.NewWithOptions(webview.WebViewOptions{
|
w := webview.NewWithOptions(webview.WebViewOptions{
|
||||||
Debug: false,
|
Debug: false,
|
||||||
AutoFocus: true,
|
AutoFocus: true,
|
||||||
|
DataPath: webviewTmp,
|
||||||
WindowOptions: webview.WindowOptions{
|
WindowOptions: webview.WindowOptions{
|
||||||
Title: "HPL Toolbox",
|
Title: "HPL Toolbox " + AppVersion,
|
||||||
Width: 1100,
|
Width: 1100,
|
||||||
Height: 720,
|
Height: 720,
|
||||||
IconId: 1,
|
IconId: 1,
|
||||||
@@ -120,6 +129,8 @@ func buildMux() *http.ServeMux {
|
|||||||
// AppLovin
|
// AppLovin
|
||||||
mux.HandleFunc("POST /api/applovin/pick", ApplovinPick)
|
mux.HandleFunc("POST /api/applovin/pick", ApplovinPick)
|
||||||
mux.HandleFunc("POST /api/applovin/upload", ApplovinUpload)
|
mux.HandleFunc("POST /api/applovin/upload", ApplovinUpload)
|
||||||
|
mux.HandleFunc("POST /api/applovin/saveQr", ApplovinSaveQR)
|
||||||
|
mux.HandleFunc("POST /api/applovin/saveAllQrs", ApplovinSaveAllQRs)
|
||||||
|
|
||||||
// Base64
|
// Base64
|
||||||
mux.HandleFunc("POST /api/base64/pickFolder", Base64PickFolder)
|
mux.HandleFunc("POST /api/base64/pickFolder", Base64PickFolder)
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ type PlecConfig struct {
|
|||||||
|
|
||||||
type ApplovinConfig struct {
|
type ApplovinConfig struct {
|
||||||
Cookie string `json:"cookie"`
|
Cookie string `json:"cookie"`
|
||||||
|
DelayMs int `json:"delayMs"`
|
||||||
|
RandomizeUserAgent *bool `json:"randomizeUserAgent,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SendToMobileConfig struct {
|
type SendToMobileConfig struct {
|
||||||
@@ -34,10 +36,28 @@ type AppConfig struct {
|
|||||||
Applovin ApplovinConfig `json:"applovin"`
|
Applovin ApplovinConfig `json:"applovin"`
|
||||||
SendToMobile SendToMobileConfig `json:"sendToMobile"`
|
SendToMobile SendToMobileConfig `json:"sendToMobile"`
|
||||||
DailyUpdate DailyUpdateConfig `json:"dailyUpdate"`
|
DailyUpdate DailyUpdateConfig `json:"dailyUpdate"`
|
||||||
|
LastPickDir string `json:"lastPickDir,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var configMu sync.Mutex
|
var configMu sync.Mutex
|
||||||
|
|
||||||
|
// userDataDir returns %LOCALAPPDATA%\HPLToolbox, creating it if needed.
|
||||||
|
// All mutable app files (config, lock, logs, WebView2 cache) live here so
|
||||||
|
// they are never placed next to the executable, which may be on a synced or
|
||||||
|
// antivirus-watched drive and would cause message-loop stalls.
|
||||||
|
func userDataDir() string {
|
||||||
|
local := os.Getenv("LOCALAPPDATA")
|
||||||
|
if local == "" {
|
||||||
|
local = os.Getenv("APPDATA")
|
||||||
|
}
|
||||||
|
if local == "" {
|
||||||
|
return appDir()
|
||||||
|
}
|
||||||
|
dir := filepath.Join(local, "HPLToolbox")
|
||||||
|
_ = os.MkdirAll(dir, 0755)
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
func defaultConfig() AppConfig {
|
func defaultConfig() AppConfig {
|
||||||
return AppConfig{
|
return AppConfig{
|
||||||
Plec: PlecConfig{
|
Plec: PlecConfig{
|
||||||
@@ -46,7 +66,7 @@ func defaultConfig() AppConfig {
|
|||||||
PreviewBase: "https://playable.applovindemo.com/phaser/",
|
PreviewBase: "https://playable.applovindemo.com/phaser/",
|
||||||
SkipExistsCheck: false,
|
SkipExistsCheck: false,
|
||||||
},
|
},
|
||||||
Applovin: ApplovinConfig{Cookie: ""},
|
Applovin: ApplovinConfig{Cookie: "", DelayMs: 5000},
|
||||||
SendToMobile: SendToMobileConfig{Port: 0},
|
SendToMobile: SendToMobileConfig{Port: 0},
|
||||||
DailyUpdate: DailyUpdateConfig{LastProject: ""},
|
DailyUpdate: DailyUpdateConfig{LastProject: ""},
|
||||||
}
|
}
|
||||||
@@ -72,7 +92,7 @@ func appDir() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func configPath() string {
|
func configPath() string {
|
||||||
return filepath.Join(appDir(), "config.json")
|
return filepath.Join(userDataDir(), "config.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadConfig() AppConfig {
|
func LoadConfig() AppConfig {
|
||||||
@@ -159,6 +179,37 @@ if ($dlg.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func PickSaveFile(title, defaultName string, filters []FileFilter, initialDir string) string {
|
||||||
|
filterStr := "All files|*.*"
|
||||||
|
if len(filters) > 0 {
|
||||||
|
parts := make([]string, 0, len(filters))
|
||||||
|
for _, f := range filters {
|
||||||
|
exts := make([]string, 0, len(f.Extensions))
|
||||||
|
for _, e := range f.Extensions {
|
||||||
|
exts = append(exts, "*."+e)
|
||||||
|
}
|
||||||
|
parts = append(parts, fmt.Sprintf("%s|%s", f.Name, strings.Join(exts, ";")))
|
||||||
|
}
|
||||||
|
filterStr = strings.Join(parts, "|")
|
||||||
|
}
|
||||||
|
script := fmt.Sprintf(`
|
||||||
|
Add-Type -AssemblyName System.Windows.Forms
|
||||||
|
$dlg = New-Object System.Windows.Forms.SaveFileDialog
|
||||||
|
$dlg.Title = '%s'
|
||||||
|
$dlg.Filter = '%s'
|
||||||
|
$dlg.FileName = '%s'
|
||||||
|
if ('%s'.Length -gt 0) { $dlg.InitialDirectory = '%s' }
|
||||||
|
if ($dlg.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
|
||||||
|
Write-Output $dlg.FileName
|
||||||
|
}
|
||||||
|
`, psEscape(title), psEscape(filterStr), psEscape(defaultName), psEscape(initialDir), psEscape(initialDir))
|
||||||
|
out, err := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-STA", "-Command", script).Output()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(out))
|
||||||
|
}
|
||||||
|
|
||||||
func PickFolder(title, initialDir string) string {
|
func PickFolder(title, initialDir string) string {
|
||||||
script := fmt.Sprintf(`
|
script := fmt.Sprintf(`
|
||||||
Add-Type -AssemblyName System.Windows.Forms
|
Add-Type -AssemblyName System.Windows.Forms
|
||||||
|
|||||||
@@ -54,6 +54,15 @@ const tbody = document.querySelector('#rows tbody');
|
|||||||
const statusEl = document.getElementById('status');
|
const statusEl = document.getElementById('status');
|
||||||
const resultsEl = document.getElementById('results');
|
const resultsEl = document.getElementById('results');
|
||||||
|
|
||||||
|
function parseIterationName(filename) {
|
||||||
|
const base = filename.replace(/\.html?$/i, '');
|
||||||
|
const tokens = base.split('_');
|
||||||
|
for (const t of tokens) {
|
||||||
|
if (/^(full|\d+clk|\d+s(?:ec)?)$/i.test(t)) return t.toLowerCase();
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
function addRow() {
|
function addRow() {
|
||||||
const id = 'r' + (nextId++);
|
const id = 'r' + (nextId++);
|
||||||
const tr = document.createElement('tr');
|
const tr = document.createElement('tr');
|
||||||
@@ -73,11 +82,24 @@ function addRow() {
|
|||||||
tr.querySelector('.pick').addEventListener('click', async () => {
|
tr.querySelector('.pick').addEventListener('click', async () => {
|
||||||
const res = await fetch('/api/plec/pick', { method: 'POST' });
|
const res = await fetch('/api/plec/pick', { method: 'POST' });
|
||||||
const j = await res.json();
|
const j = await res.json();
|
||||||
if (j.path) {
|
if (j.files && j.files.length) {
|
||||||
tr.querySelector('[data-path]').value = j.path;
|
const affected = [];
|
||||||
tr.querySelector('[data-name]').textContent = j.name;
|
j.files.forEach(function(f, i) {
|
||||||
const iter = tr.querySelector('[data-iter]');
|
const target = i === 0 ? tr : (addRow(), tbody.lastElementChild);
|
||||||
if (!iter.value) iter.value = j.name.replace(/\.html?$/i, '');
|
target.querySelector('[data-path]').value = f.path;
|
||||||
|
target.querySelector('[data-name]').textContent = f.name;
|
||||||
|
const iter = target.querySelector('[data-iter]');
|
||||||
|
if (!iter.value) iter.value = parseIterationName(f.name);
|
||||||
|
affected.push(target);
|
||||||
|
});
|
||||||
|
if (affected.length > 1) {
|
||||||
|
const iters = affected.map(r => r.querySelector('[data-iter]').value);
|
||||||
|
if (iters[0] !== '' && iters.every(n => n === iters[0])) {
|
||||||
|
affected.forEach((r, i) => {
|
||||||
|
r.querySelector('[data-iter]').value = String(i + 1).padStart(2, '0') + '_' + iters[0];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
tr.querySelector('.remove-btn').addEventListener('click', () => {
|
tr.querySelector('.remove-btn').addEventListener('click', () => {
|
||||||
@@ -163,16 +185,23 @@ addRow();
|
|||||||
}
|
}
|
||||||
|
|
||||||
func PlecPick(w http.ResponseWriter, r *http.Request) {
|
func PlecPick(w http.ResponseWriter, r *http.Request) {
|
||||||
|
cfg := LoadConfig()
|
||||||
picked := PickFiles(
|
picked := PickFiles(
|
||||||
"Select HTML",
|
"Select HTML",
|
||||||
[]FileFilter{{Name: "HTML", Extensions: []string{"html", "htm"}}},
|
[]FileFilter{{Name: "HTML", Extensions: []string{"html", "htm"}}},
|
||||||
false, "",
|
true, cfg.LastPickDir,
|
||||||
)
|
)
|
||||||
if len(picked) == 0 {
|
if len(picked) == 0 {
|
||||||
writeJSON(w, map[string]any{"path": nil})
|
writeJSON(w, map[string]any{"files": []any{}})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSON(w, map[string]any{"path": picked[0], "name": filepath.Base(picked[0])})
|
cfg.LastPickDir = filepath.Dir(picked[0])
|
||||||
|
_ = SaveConfig(cfg)
|
||||||
|
files := make([]map[string]string, 0, len(picked))
|
||||||
|
for _, p := range picked {
|
||||||
|
files = append(files, map[string]string{"path": p, "name": filepath.Base(p)})
|
||||||
|
}
|
||||||
|
writeJSON(w, map[string]any{"files": files})
|
||||||
}
|
}
|
||||||
|
|
||||||
type plecRow struct {
|
type plecRow struct {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func lockFilePath() string {
|
func lockFilePath() string {
|
||||||
return filepath.Join(appDir(), ".hpltoolbox.lock")
|
return filepath.Join(userDataDir(), ".hpltoolbox.lock")
|
||||||
}
|
}
|
||||||
|
|
||||||
// focusExistingInstance returns true if another instance was found and
|
// focusExistingInstance returns true if another instance was found and
|
||||||
|
|||||||
Reference in New Issue
Block a user