574 lines
21 KiB
TypeScript
574 lines
21 KiB
TypeScript
import * as vscode from 'vscode';
|
||
import * as fs from 'fs';
|
||
import * as path from 'path';
|
||
import { File } from 'node:buffer';
|
||
import { getToolWebviewStyles, 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 Playable Preview');
|
||
if (!isNew) return;
|
||
|
||
panel.webview.html = getHtml();
|
||
panel.webview.onDidReceiveMessage(async (msg) => {
|
||
try {
|
||
if (handleClipboardAndOpen(msg)) return;
|
||
|
||
if (msg.type === 'pickFolder') {
|
||
const picked = await vscode.window.showOpenDialog({
|
||
canSelectFolders: true,
|
||
canSelectFiles: false,
|
||
canSelectMany: false,
|
||
openLabel: 'Select Folder',
|
||
defaultUri: getPickerDefaultUri(),
|
||
});
|
||
if (picked && picked[0]) {
|
||
const files = await collectHtmlFiles(picked[0].fsPath);
|
||
panel.webview.postMessage({
|
||
type: 'filesSelected',
|
||
files: files.map(filePath => ({ path: filePath, name: path.basename(filePath) })),
|
||
});
|
||
}
|
||
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<void> {
|
||
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<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];
|
||
if (!paths.length) {
|
||
panel.webview.postMessage({ type: 'error', message: 'No files selected.' });
|
||
return;
|
||
}
|
||
|
||
const buildHeaders = (): Record<string, string> => {
|
||
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': 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: `Waiting ${idx}/${paths.length} ${fileName}` });
|
||
await sleep(delayMs);
|
||
}
|
||
|
||
panel.webview.postMessage({ type: 'status', message: `Validating ${idx}/${paths.length} ${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: `Uploading ${idx}/${paths.length} ${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);
|
||
}
|
||
|
||
async function collectHtmlFiles(root: string): Promise<string[]> {
|
||
const out: string[] = [];
|
||
const stack: string[] = [root];
|
||
while (stack.length) {
|
||
const dir = stack.pop()!;
|
||
let entries: fs.Dirent[];
|
||
try {
|
||
entries = fs.readdirSync(dir, { withFileTypes: true });
|
||
} catch {
|
||
continue;
|
||
}
|
||
for (const e of entries) {
|
||
const full = path.join(dir, e.name);
|
||
if (e.isDirectory()) {
|
||
if (e.name === 'node_modules' || e.name === '.git') continue;
|
||
stack.push(full);
|
||
} else if (e.isFile() && /\.html?$/i.test(e.name)) {
|
||
out.push(full);
|
||
}
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
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>
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<style>
|
||
${getToolWebviewStyles()}
|
||
.selected-files { margin-top: 10px; }
|
||
.qr-cell { text-align: center; }
|
||
.qr-cell img {
|
||
width: 104px;
|
||
height: 104px;
|
||
max-width: 100%;
|
||
padding: 6px;
|
||
border-radius: var(--tool-radius);
|
||
background: #fff;
|
||
}
|
||
.result-actions { display: flex; gap: 6px; flex-wrap: wrap; }
|
||
.qr-cell img { cursor: zoom-in; }
|
||
.qr-modal {
|
||
position: fixed;
|
||
inset: 0;
|
||
display: none;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 24px;
|
||
background: rgba(0, 0, 0, 0.72);
|
||
z-index: 10;
|
||
}
|
||
.qr-modal.open { display: flex; }
|
||
.qr-modal img {
|
||
width: min(70vw, 520px);
|
||
height: min(70vw, 520px);
|
||
padding: 18px;
|
||
border-radius: var(--tool-radius);
|
||
background: #fff;
|
||
}
|
||
.qr-results:not(.has-errors) .error-col { display: none; }
|
||
.remove-selected {
|
||
min-width: 28px;
|
||
width: 28px;
|
||
padding: 3px 0;
|
||
color: var(--vscode-errorForeground);
|
||
font-size: 16px;
|
||
line-height: 1;
|
||
}
|
||
.pager { margin-top: 8px; display: flex; align-items: center; gap: 8px; justify-content: flex-end; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<main class="tool-page">
|
||
<header class="tool-header">
|
||
<h2 class="tool-title">AppLovin Playable Preview</h2>
|
||
<p class="tool-description">Upload HTML files to p.applov.in and get QR codes to scan with the AppLovin Playable Preview app.</p>
|
||
</header>
|
||
|
||
<section class="tool-panel input-panel">
|
||
<div class="panel-header">
|
||
<h3 class="panel-title">Inputs</h3>
|
||
</div>
|
||
<div class="panel-body">
|
||
<div class="control-group">
|
||
<div class="file-row">
|
||
<button id="pickFolder" class="secondary">Select Folder</button>
|
||
<button id="pick" class="secondary">Select File(s)</button>
|
||
<button id="clear" class="secondary">Clear</button>
|
||
</div>
|
||
<div class="file-name" id="fileName" style="margin-top:8px;">(no files selected)</div>
|
||
<div id="fileList" class="selected-files"></div>
|
||
</div>
|
||
<div class="action-row">
|
||
<button id="upload">Upload</button>
|
||
<button id="saveAll" class="secondary" disabled>Save All</button>
|
||
<button id="regenAll" class="secondary" disabled>Regenerate</button>
|
||
</div>
|
||
<div id="status" class="status-panel"></div>
|
||
</div>
|
||
</section>
|
||
<div id="outputPanel" class="is-hidden">
|
||
<div id="results" class="results-panel"></div>
|
||
</div>
|
||
<div id="qrModal" class="qr-modal" title="Click to close"><img alt="Expanded QR" /></div>
|
||
</main>
|
||
|
||
<script>
|
||
const vscode = acquireVsCodeApi();
|
||
const pickFolderBtn = document.getElementById('pickFolder');
|
||
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 outputPanel = document.getElementById('outputPanel');
|
||
const qrModal = document.getElementById('qrModal');
|
||
const saveAllBtn = document.getElementById('saveAll');
|
||
const regenAllBtn = document.getElementById('regenAll');
|
||
const PAGE_SIZE = 5;
|
||
let selectedPaths = [];
|
||
let selectedPage = 0;
|
||
let resultItems = [];
|
||
|
||
function basename(p) {
|
||
const i = Math.max(p.lastIndexOf('/'), p.lastIndexOf('\\\\'));
|
||
return i >= 0 ? p.slice(i + 1) : p;
|
||
}
|
||
function renderSelection() {
|
||
const oldPager = document.getElementById('selectedPager');
|
||
if (oldPager) oldPager.remove();
|
||
if (!selectedPaths.length) {
|
||
fileNameEl.textContent = '(no files selected)';
|
||
fileListEl.innerHTML = '';
|
||
return;
|
||
}
|
||
fileNameEl.textContent = selectedPaths.length + ' file' + (selectedPaths.length === 1 ? '' : 's');
|
||
const maxPage = Math.max(0, Math.ceil(selectedPaths.length / PAGE_SIZE) - 1);
|
||
selectedPage = Math.min(selectedPage, maxPage);
|
||
const start = selectedPage * PAGE_SIZE;
|
||
const visiblePaths = selectedPaths.slice(start, start + PAGE_SIZE);
|
||
fileListEl.innerHTML =
|
||
'<div class="results-panel" style="margin-top:0;">' +
|
||
'<table class="data-table">' +
|
||
'<thead><tr><th>Filename</th><th style="width:44px;"></th></tr></thead>' +
|
||
'<tbody>' +
|
||
visiblePaths.map((p, offset) => {
|
||
const i = start + offset;
|
||
return (
|
||
'<tr><td class="mono wrap">' + escapeHtml(basename(p)) + '</td>' +
|
||
'<td><button class="remove-selected danger" data-index="' + i + '" title="Remove">×</button></td></tr>'
|
||
);
|
||
}).join('') +
|
||
'</tbody>' +
|
||
'</table>' +
|
||
'</div>';
|
||
fileListEl.querySelectorAll('.remove-selected').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
selectedPaths.splice(Number(btn.dataset.index), 1);
|
||
renderSelection();
|
||
});
|
||
});
|
||
if (selectedPaths.length > PAGE_SIZE) {
|
||
const pager = document.createElement('div');
|
||
pager.id = 'selectedPager';
|
||
pager.className = 'pager';
|
||
pager.innerHTML =
|
||
'<button class="secondary" id="prevPage"' + (selectedPage === 0 ? ' disabled' : '') + '>Previous</button>' +
|
||
'<span class="file-name">Page ' + (selectedPage + 1) + ' of ' + (maxPage + 1) + '</span>' +
|
||
'<button class="secondary" id="nextPage"' + (selectedPage === maxPage ? ' disabled' : '') + '>Next</button>';
|
||
fileListEl.appendChild(pager);
|
||
pager.querySelector('#prevPage').addEventListener('click', () => { selectedPage--; renderSelection(); });
|
||
pager.querySelector('#nextPage').addEventListener('click', () => { selectedPage++; renderSelection(); });
|
||
}
|
||
}
|
||
|
||
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();
|
||
});
|
||
});
|
||
qrModal.addEventListener('click', () => {
|
||
qrModal.classList.remove('open');
|
||
});
|
||
|
||
pickFolderBtn.addEventListener('click', () => vscode.postMessage({ type: 'pickFolder' }));
|
||
pickBtn.addEventListener('click', () => vscode.postMessage({ type: 'pickFiles' }));
|
||
clearBtn.addEventListener('click', () => {
|
||
selectedPaths = [];
|
||
selectedPage = 0;
|
||
renderSelection();
|
||
});
|
||
|
||
uploadBtn.addEventListener('click', () => {
|
||
statusEl.textContent = '';
|
||
statusEl.classList.remove('is-busy');
|
||
resultsEl.innerHTML = '';
|
||
outputPanel.classList.add('is-hidden');
|
||
if (!selectedPaths.length) {
|
||
statusEl.innerHTML = '<span class="err">Pick at least one HTML file first.</span>';
|
||
return;
|
||
}
|
||
uploadBtn.disabled = true;
|
||
statusEl.textContent = 'Uploading...';
|
||
statusEl.classList.add('is-busy');
|
||
resultItems = [];
|
||
saveAllBtn.disabled = true;
|
||
regenAllBtn.disabled = true;
|
||
vscode.postMessage({ type: 'upload', paths: selectedPaths });
|
||
});
|
||
|
||
function escapeHtml(s) {
|
||
return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||
}
|
||
|
||
function ensureResultsTable() {
|
||
let tbody = resultsEl.querySelector('tbody');
|
||
if (tbody) return tbody;
|
||
resultsEl.innerHTML =
|
||
'<table class="data-table qr-results">' +
|
||
'<thead><tr><th>File</th><th style="width:140px;">QR</th><th style="width:170px;">Actions</th><th class="error-col">Error</th></tr></thead>' +
|
||
'<tbody></tbody>' +
|
||
'</table>';
|
||
outputPanel.classList.remove('is-hidden');
|
||
return resultsEl.querySelector('tbody');
|
||
}
|
||
|
||
window.addEventListener('message', (e) => {
|
||
const m = e.data;
|
||
if (m.type === 'filesSelected') {
|
||
const added = m.files.map(f => f.path).filter(p => !selectedPaths.includes(p));
|
||
selectedPaths = selectedPaths.concat(added);
|
||
selectedPage = Math.max(0, Math.ceil(selectedPaths.length / PAGE_SIZE) - 1);
|
||
renderSelection();
|
||
} else if (m.type === 'start') {
|
||
resultsEl.innerHTML = '';
|
||
outputPanel.classList.add('is-hidden');
|
||
resultItems = [];
|
||
saveAllBtn.disabled = true;
|
||
regenAllBtn.disabled = true;
|
||
} else if (m.type === 'status') {
|
||
statusEl.textContent = m.message;
|
||
statusEl.className = 'status-panel is-busy';
|
||
} else if (m.type === 'error') {
|
||
statusEl.classList.remove('is-busy');
|
||
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 tbody = ensureResultsTable();
|
||
resultsEl.querySelector('table').classList.add('has-errors');
|
||
const row = document.createElement('tr');
|
||
row.innerHTML =
|
||
'<td class="mono wrap">' + escapeHtml(m.name) + '</td>' +
|
||
'<td class="muted">-</td>' +
|
||
'<td class="muted">-</td>' +
|
||
'<td class="err wrap error-col">' + escapeHtml(m.message) + '</td>';
|
||
tbody.appendChild(row);
|
||
} else if (m.type === 'fileResult') {
|
||
const tbody = ensureResultsTable();
|
||
const row = document.createElement('tr');
|
||
const file = document.createElement('td');
|
||
file.className = 'mono wrap';
|
||
file.textContent = m.name;
|
||
const qr = document.createElement('td');
|
||
qr.className = 'qr-cell';
|
||
qr.innerHTML = '<img src="' + m.qrImg + '" data-qr="' + m.qrImg + '" alt="QR" />';
|
||
qr.querySelector('img').addEventListener('click', (event) => {
|
||
qrModal.querySelector('img').src = event.currentTarget.src;
|
||
qrModal.classList.add('open');
|
||
});
|
||
const actionCell = document.createElement('td');
|
||
const error = document.createElement('td');
|
||
error.className = 'error-col';
|
||
error.innerHTML = '<span class="muted">-</span>';
|
||
const actions = document.createElement('div');
|
||
actions.className = 'result-actions';
|
||
const openPreview = document.createElement('button');
|
||
openPreview.textContent = 'Open';
|
||
openPreview.className = 'secondary';
|
||
openPreview.onclick = () => vscode.postMessage({ type: 'open', text: m.previewUrl });
|
||
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);
|
||
actionCell.appendChild(actions);
|
||
row.append(file, qr, actionCell, error);
|
||
tbody.appendChild(row);
|
||
resultItems.push({ name: m.name, url: m.qrImg });
|
||
saveAllBtn.disabled = resultItems.length === 0;
|
||
regenAllBtn.disabled = resultItems.length === 0;
|
||
} else if (m.type === 'done') {
|
||
statusEl.classList.remove('is-busy');
|
||
statusEl.innerHTML = '<span class="ok">Done.</span>';
|
||
uploadBtn.disabled = false;
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>`;
|
||
}
|