first commit
This commit is contained in:
262
src/tools/applovinUpload.ts
Normal file
262
src/tools/applovinUpload.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { File } from 'node:buffer';
|
||||
import { handleClipboardAndOpen, singletonPanel } from './shared';
|
||||
|
||||
const store: { panel: vscode.WebviewPanel | null } = { panel: null };
|
||||
|
||||
export function openApplovinUpload(_context: vscode.ExtensionContext) {
|
||||
const { panel, isNew } = singletonPanel(store, 'hplToolbox.applovinUpload', 'AppLovin Demo Upload');
|
||||
if (!isNew) return;
|
||||
|
||||
panel.webview.html = getHtml();
|
||||
panel.webview.onDidReceiveMessage(async (msg) => {
|
||||
try {
|
||||
if (handleClipboardAndOpen(msg)) return;
|
||||
|
||||
if (msg.type === 'pickFiles') {
|
||||
const picked = await vscode.window.showOpenDialog({
|
||||
canSelectMany: true,
|
||||
filters: { HTML: ['html', 'htm'] },
|
||||
openLabel: 'Select',
|
||||
});
|
||||
if (picked && picked.length) {
|
||||
panel.webview.postMessage({
|
||||
type: 'filesSelected',
|
||||
files: picked.map(u => ({ path: u.fsPath, name: path.basename(u.fsPath) })),
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.type === 'upload') {
|
||||
await handleUpload(panel, msg.paths);
|
||||
return;
|
||||
}
|
||||
} catch (err: any) {
|
||||
panel.webview.postMessage({ type: 'error', message: err?.message || String(err) });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function handleUpload(panel: vscode.WebviewPanel, filePaths: string[]) {
|
||||
const cfg = vscode.workspace.getConfiguration('hplToolbox.applovin');
|
||||
const cookie = (cfg.get<string>('cookie') || '').trim();
|
||||
|
||||
const paths = Array.isArray(filePaths) ? filePaths : [filePaths];
|
||||
if (!paths.length) {
|
||||
panel.webview.postMessage({ type: 'error', message: 'No files selected.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Accept': 'application/json, text/javascript, */*; q=0.01',
|
||||
'Origin': 'https://p.applov.in',
|
||||
'Referer': 'https://p.applov.in/playablePreview?create=1&qr=1',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'User-Agent': 'Mozilla/5.0 (HPL-Toolbox-VSCode)',
|
||||
};
|
||||
if (cookie) headers['Cookie'] = `__Host-APPLOVINID=${cookie}`;
|
||||
|
||||
const buildForm = (filePath: string): FormData => {
|
||||
const buf = fs.readFileSync(filePath);
|
||||
const file = new File([buf], path.basename(filePath), { type: 'text/html' });
|
||||
const form = new FormData();
|
||||
form.append('file', file as any);
|
||||
return form;
|
||||
};
|
||||
|
||||
panel.webview.postMessage({ type: 'start', total: paths.length });
|
||||
|
||||
for (let i = 0; i < paths.length; i++) {
|
||||
const filePath = paths[i];
|
||||
const fileName = path.basename(filePath);
|
||||
const idx = i + 1;
|
||||
const reportError = (message: string) => {
|
||||
panel.webview.postMessage({ type: 'fileError', name: fileName, message });
|
||||
};
|
||||
|
||||
if (!filePath || !fs.existsSync(filePath)) { reportError('File missing'); continue; }
|
||||
if (!/\.html?$/i.test(filePath)) { reportError('Must be .html'); continue; }
|
||||
|
||||
panel.webview.postMessage({ type: 'status', message: `[${idx}/${paths.length}] Validating ${fileName}...` });
|
||||
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch('https://p.applov.in/validateHTMLContent', {
|
||||
method: 'POST', body: buildForm(filePath), headers,
|
||||
});
|
||||
} catch (err: any) { reportError('Network error (validate): ' + (err?.message || err)); continue; }
|
||||
const validateText = await res.text();
|
||||
if (!res.ok) { reportError(`Validate HTTP ${res.status}: ${validateText.slice(0, 300)}`); continue; }
|
||||
let validateJson: any;
|
||||
try { validateJson = JSON.parse(validateText); } catch { validateJson = null; }
|
||||
if (validateJson && validateJson.code && validateJson.code !== 200) {
|
||||
reportError(`Validation failed: ${validateText.slice(0, 300)}`); continue;
|
||||
}
|
||||
|
||||
panel.webview.postMessage({ type: 'status', message: `[${idx}/${paths.length}] Uploading ${fileName}...` });
|
||||
|
||||
try {
|
||||
res = await fetch('https://p.applov.in/getCachedAdURL', {
|
||||
method: 'POST', body: buildForm(filePath), headers,
|
||||
});
|
||||
} catch (err: any) { reportError('Network error (cache): ' + (err?.message || err)); continue; }
|
||||
const cacheText = await res.text();
|
||||
if (!res.ok) { reportError(`Cache HTTP ${res.status}: ${cacheText.slice(0, 300)}`); continue; }
|
||||
let cacheJson: any;
|
||||
try { cacheJson = JSON.parse(cacheText); }
|
||||
catch { reportError(`Non-JSON response: ${cacheText.slice(0, 300)}`); continue; }
|
||||
const hash = cacheJson.results;
|
||||
if (!hash) { reportError(`No hash in response: ${cacheText.slice(0, 300)}`); continue; }
|
||||
|
||||
panel.webview.postMessage({
|
||||
type: 'fileResult',
|
||||
name: fileName,
|
||||
hash,
|
||||
previewUrl: `https://p.applov.in/getPreviewHTML?preview=${hash}`,
|
||||
pageUrl: `https://p.applov.in/playablePreview?preview=${hash}&qr=1`,
|
||||
qrImg: `https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=${encodeURIComponent(hash)}`,
|
||||
});
|
||||
}
|
||||
|
||||
panel.webview.postMessage({ type: 'done' });
|
||||
}
|
||||
|
||||
function getHtml(): string {
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<style>
|
||||
body { font-family: var(--vscode-font-family); color: var(--vscode-foreground); padding: 16px; }
|
||||
h2 { margin-top: 0; }
|
||||
button {
|
||||
background: var(--vscode-button-background); color: var(--vscode-button-foreground);
|
||||
border: none; padding: 6px 12px; border-radius: 2px; cursor: pointer;
|
||||
}
|
||||
button:hover { background: var(--vscode-button-hoverBackground); }
|
||||
button.secondary {
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
}
|
||||
button.secondary:hover { background: var(--vscode-button-secondaryHoverBackground); }
|
||||
.file-cell { display: flex; align-items: center; gap: 8px; }
|
||||
.file-name { opacity: 0.85; font-size: 12px; word-break: break-all; }
|
||||
.actions { margin-top: 12px; display: flex; gap: 8px; }
|
||||
#status { margin-top: 12px; min-height: 20px; }
|
||||
.err { color: var(--vscode-errorForeground); white-space: pre-wrap; }
|
||||
.ok { color: var(--vscode-testing-iconPassed, var(--vscode-foreground)); }
|
||||
.result {
|
||||
padding: 10px;
|
||||
background: var(--vscode-textBlockQuote-background);
|
||||
border-left: 3px solid var(--vscode-textBlockQuote-border);
|
||||
word-break: break-all;
|
||||
}
|
||||
.result-actions { margin-top: 8px; display: flex; gap: 8px; }
|
||||
#results { margin-top: 12px; display: grid; grid-template-columns: repeat(auto-fill,minmax(280px,1fr)); gap: 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>AppLovin Playable Preview (QR)</h2>
|
||||
<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). Requires the <code>__Host-APPLOVINID</code> cookie set in <em>HPL Toolbox — AppLovin Upload</em> settings.</p>
|
||||
|
||||
<div class="file-cell" style="margin-bottom:8px;">
|
||||
<button id="pick" class="secondary">Choose files...</button>
|
||||
<span class="file-name" id="fileName">(no files)</span>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button id="upload">Upload to AppLovin</button>
|
||||
</div>
|
||||
<div id="status"></div>
|
||||
<div id="results"></div>
|
||||
|
||||
<script>
|
||||
const vscode = acquireVsCodeApi();
|
||||
const pickBtn = document.getElementById('pick');
|
||||
const uploadBtn = document.getElementById('upload');
|
||||
const fileNameEl = document.getElementById('fileName');
|
||||
const statusEl = document.getElementById('status');
|
||||
const resultsEl = document.getElementById('results');
|
||||
let selectedPaths = [];
|
||||
|
||||
pickBtn.addEventListener('click', () => vscode.postMessage({ type: 'pickFiles' }));
|
||||
|
||||
uploadBtn.addEventListener('click', () => {
|
||||
statusEl.textContent = '';
|
||||
resultsEl.innerHTML = '';
|
||||
if (!selectedPaths.length) {
|
||||
statusEl.innerHTML = '<span class="err">Pick at least one HTML file first.</span>';
|
||||
return;
|
||||
}
|
||||
uploadBtn.disabled = true;
|
||||
vscode.postMessage({ type: 'upload', paths: selectedPaths });
|
||||
});
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
}
|
||||
|
||||
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 ? '...' : '');
|
||||
} else if (m.type === 'start') {
|
||||
resultsEl.innerHTML = '';
|
||||
} else if (m.type === 'status') {
|
||||
statusEl.textContent = m.message;
|
||||
statusEl.className = '';
|
||||
} else if (m.type === 'error') {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'err';
|
||||
div.textContent = m.message;
|
||||
statusEl.innerHTML = '';
|
||||
statusEl.appendChild(div);
|
||||
uploadBtn.disabled = false;
|
||||
} else if (m.type === 'fileError') {
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'result';
|
||||
wrap.innerHTML = '<div><strong>' + escapeHtml(m.name) + '</strong></div>' +
|
||||
'<div class="err" style="margin-top:6px;">' + escapeHtml(m.message) + '</div>';
|
||||
resultsEl.appendChild(wrap);
|
||||
} else if (m.type === 'fileResult') {
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'result';
|
||||
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%;" />' +
|
||||
'</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);
|
||||
actions.appendChild(openPreview);
|
||||
wrap.appendChild(actions);
|
||||
resultsEl.appendChild(wrap);
|
||||
} else if (m.type === 'done') {
|
||||
statusEl.innerHTML = '<span class="ok">Done.</span>';
|
||||
uploadBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
Reference in New Issue
Block a user