update applovin demo upload

This commit is contained in:
2026-05-14 14:33:44 +08:00
parent f367b66b60
commit 716dac31ef
11 changed files with 385 additions and 32 deletions

View File

@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(go build *)"
]
}
}

4
LICENSE Normal file
View 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.

BIN
dist/hpl-toolbox-0.1.2.exe vendored Normal file

Binary file not shown.

BIN
dist/hpl-toolbox-0.1.3.vsix vendored Normal file

Binary file not shown.

BIN
dist/hpl-toolbox.exe vendored

Binary file not shown.

View File

@@ -2,8 +2,13 @@
"name": "hpl-toolbox",
"displayName": "HPL Toolbox",
"description": "Bundled tools: PLEC Upload, AppLovin Demo Upload, Base64 Asset Scanner, Daily Update. Local HTML Host.",
"version": "0.1.2",
"version": "0.1.3",
"publisher": "hesukastro",
"license": "UNLICENSED",
"repository": {
"type": "git",
"url": "https://gitea.hesukastro.com/hesukastro/vsix-hpl-toolbox.git"
},
"engines": {
"vscode": "^1.85.0"
},

View File

@@ -34,6 +34,16 @@ export function openApplovinUpload(_context: vscode.ExtensionContext) {
await handleUpload(panel, msg.paths);
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) {
panel.webview.postMessage({ type: 'error', message: err?.message || String(err) });
}
@@ -124,6 +134,68 @@ async function handleUpload(panel: vscode.WebviewPanel, filePaths: string[]) {
panel.webview.postMessage({ type: 'done' });
}
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 {
return `<!DOCTYPE html>
<html>
@@ -163,11 +235,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>
<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>
</div>
<div id="fileList" style="margin-bottom:8px;font-size:12px;opacity:0.85;"></div>
<div class="actions">
<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 id="status"></div>
<div id="results"></div>
@@ -175,13 +251,51 @@ function getHtml(): string {
<script>
const vscode = acquireVsCodeApi();
const pickBtn = document.getElementById('pick');
const clearBtn = document.getElementById('clear');
const uploadBtn = document.getElementById('upload');
const fileNameEl = document.getElementById('fileName');
const fileListEl = document.getElementById('fileList');
const statusEl = document.getElementById('status');
const resultsEl = document.getElementById('results');
const saveAllBtn = document.getElementById('saveAll');
const regenAllBtn = document.getElementById('regenAll');
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' }));
clearBtn.addEventListener('click', () => {
selectedPaths = [];
renderSelection();
});
uploadBtn.addEventListener('click', () => {
statusEl.textContent = '';
@@ -191,6 +305,9 @@ function getHtml(): string {
return;
}
uploadBtn.disabled = true;
resultItems = [];
saveAllBtn.style.display = 'none';
regenAllBtn.style.display = 'none';
vscode.postMessage({ type: 'upload', paths: selectedPaths });
});
@@ -201,13 +318,14 @@ function getHtml(): string {
window.addEventListener('message', (e) => {
const m = e.data;
if (m.type === 'filesSelected') {
selectedPaths = m.files.map(f => f.path);
const names = m.files.map(f => f.name);
fileNameEl.textContent = names.length === 1
? names[0]
: names.length + ' files: ' + names.slice(0, 3).join(', ') + (names.length > 3 ? '...' : '');
const added = m.files.map(f => f.path).filter(p => !selectedPaths.includes(p));
selectedPaths = selectedPaths.concat(added);
renderSelection();
} else if (m.type === 'start') {
resultsEl.innerHTML = '';
resultItems = [];
saveAllBtn.style.display = 'none';
regenAllBtn.style.display = 'none';
} else if (m.type === 'status') {
statusEl.textContent = m.message;
statusEl.className = '';
@@ -230,27 +348,28 @@ function getHtml(): string {
wrap.innerHTML =
'<div style="font-weight:600;margin-bottom:8px;word-break:break-all;">' + escapeHtml(m.name) + '</div>' +
'<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 style="font-size:11px;opacity:0.8;word-break:break-all;">Hash: ' + escapeHtml(m.hash) + '</div>';
const actions = document.createElement('div');
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');
openPreview.textContent = 'Open';
openPreview.className = 'secondary';
openPreview.onclick = () => vscode.postMessage({ type: 'open', text: m.previewUrl });
actions.appendChild(copyHash);
actions.appendChild(copyPreview);
const saveQr = document.createElement('button');
saveQr.textContent = 'Save QR';
saveQr.className = 'secondary';
saveQr.onclick = () => vscode.postMessage({ type: 'saveQr', url: m.qrImg, name: m.name });
actions.appendChild(openPreview);
actions.appendChild(saveQr);
wrap.appendChild(actions);
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') {
statusEl.innerHTML = '<span class="ok">Done.</span>';
uploadBtn.disabled = false;

View File

@@ -11,6 +11,7 @@ import (
"os"
"path/filepath"
"regexp"
"strings"
)
func ApplovinPage(w http.ResponseWriter, r *http.Request) {
@@ -19,11 +20,15 @@ func ApplovinPage(w http.ResponseWriter, r *http.Request) {
<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;">
<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>
</div>
<div id="fileList" style="margin-bottom:8px;font-size:12px;opacity:0.85;"></div>
<div style="display:flex;gap:8px;">
<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 id="status" style="margin-top:12px;min-height:20px;"></div>
@@ -36,21 +41,70 @@ func ApplovinPage(w http.ResponseWriter, r *http.Request) {
<script>
const pickBtn = document.getElementById('pick');
const clearBtn = document.getElementById('clear');
const uploadBtn = document.getElementById('upload');
const fileNameEl = document.getElementById('fileName');
const fileListEl = document.getElementById('fileList');
const statusEl = document.getElementById('status');
const resultsEl = document.getElementById('results');
const saveAllBtn = document.getElementById('saveAll');
const regenAllBtn = document.getElementById('regenAll');
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 () => {
const r = await fetch('/api/applovin/pick', { method: 'POST' });
const j = await r.json();
if (j.files && j.files.length) {
selectedPaths = j.files.map(f => f.path);
const names = j.files.map(f => f.name);
fileNameEl.textContent = names.length === 1
? names[0]
: names.length + ' files: ' + names.slice(0,3).join(', ') + (names.length > 3 ? '...' : '');
const added = j.files.map(f => f.path).filter(p => !selectedPaths.includes(p));
selectedPaths = selectedPaths.concat(added);
renderSelection();
}
});
@@ -62,6 +116,9 @@ uploadBtn.addEventListener('click', async () => {
return;
}
uploadBtn.disabled = true;
resultItems = [];
saveAllBtn.style.display = 'none';
regenAllBtn.style.display = 'none';
try {
const res = await fetch('/api/applovin/upload', {
method: 'POST',
@@ -113,25 +170,42 @@ function handle(m) {
wrap.innerHTML =
'<div style="font-weight:600;margin-bottom:8px;">' + escapeHtml(m.name) + '</div>' +
'<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 style="font-size:11px;opacity:0.8;">Hash: ' + escapeHtml(m.hash) + '</div>';
const actions = document.createElement('div');
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');
openBtn.textContent = 'Open';
openBtn.className = 'secondary';
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);
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') {
statusEl.innerHTML = '<span class="ok">Done.</span>';
}
@@ -300,3 +374,114 @@ func ApplovinUpload(w http.ResponseWriter, r *http.Request) {
}
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

Binary file not shown.

View File

@@ -120,6 +120,8 @@ func buildMux() *http.ServeMux {
// AppLovin
mux.HandleFunc("POST /api/applovin/pick", ApplovinPick)
mux.HandleFunc("POST /api/applovin/upload", ApplovinUpload)
mux.HandleFunc("POST /api/applovin/saveQr", ApplovinSaveQR)
mux.HandleFunc("POST /api/applovin/saveAllQrs", ApplovinSaveAllQRs)
// Base64
mux.HandleFunc("POST /api/base64/pickFolder", Base64PickFolder)

View File

@@ -159,6 +159,37 @@ if ($dlg.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
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 {
script := fmt.Sprintf(`
Add-Type -AssemblyName System.Windows.Forms