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', defaultUri: getPickerDefaultUri(), }); 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; } 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) }); } }); } 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 { return new Promise(resolve => setTimeout(resolve, ms)); } async function handleUpload(panel: vscode.WebviewPanel, filePaths: string[]) { const cfg = vscode.workspace.getConfiguration('hplToolbox.applovin'); const cookie = (cfg.get('cookie') || '').trim(); const delayMs = Math.max(0, cfg.get('delayMs') ?? 1500); const randomizeUA = cfg.get('randomizeUserAgent') ?? true; const paths = Array.isArray(filePaths) ? filePaths : [filePaths]; if (!paths.length) { panel.webview.postMessage({ type: 'error', message: 'No files selected.' }); return; } const buildHeaders = (): Record => { const headers: Record = { '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': pickUserAgent(randomizeUA), }; if (cookie) headers['Cookie'] = `__Host-APPLOVINID=${cookie}`; return headers; }; 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; } 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}...` }); let res: Response; try { res = await fetch('https://p.applov.in/validateHTMLContent', { method: 'POST', body: buildForm(filePath), headers: buildHeaders(), }); } 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: buildHeaders(), }); } 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 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 { 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 `

AppLovin Playable Preview (QR)

Upload HTML files to p.applov.in and get QR codes to scan with the AppLovin Playable Preview app (iOS/Android).

(no files)
`; }