standalone

This commit is contained in:
2026-05-28 01:25:53 +08:00
parent ca9cc62b8c
commit 16847a78ca
12 changed files with 859 additions and 443 deletions

View File

@@ -35,31 +35,50 @@ func pickApplovinUserAgent(randomize bool) string {
func ApplovinPage(w http.ResponseWriter, r *http.Request) {
body := `
<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>
<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>
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
<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>
<section class="tool-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 id="fileName" class="file-name" 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="status" style="margin-top:12px;min-height:20px;"></div>
<div id="results" style="margin-top:12px;display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px;"></div>
<div id="results" class="results-panel"></div>
<style>
.result { padding:10px; background:#252526; border-left:3px solid #007fd4; word-break:break-all; }
.result-actions { margin-top:8px; display:flex; gap:8px; flex-wrap:wrap; }
.qr-cell { text-align:center; }
.qr-cell img { width:104px; height:104px; max-width:100%; padding:6px; border-radius:4px; background:#fff; cursor:zoom-in; }
.result-actions { display:flex; gap:6px; flex-wrap:wrap; }
.qr-results:not(.has-errors) .error-col { display:none; }
.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:4px; background:#fff; }
</style>
<div id="qrModal" class="qr-modal"><img alt="Expanded QR" /></div>
<script>
const PAGE_SIZE = 5;
let selectedPage = 0;
const pickBtn = document.getElementById('pick');
const pickFolderBtn = document.getElementById('pickFolder');
const clearBtn = document.getElementById('clear');
const uploadBtn = document.getElementById('upload');
const fileNameEl = document.getElementById('fileName');
@@ -68,6 +87,7 @@ const statusEl = document.getElementById('status');
const resultsEl = document.getElementById('results');
const saveAllBtn = document.getElementById('saveAll');
const regenAllBtn = document.getElementById('regenAll');
const qrModal = document.getElementById('qrModal');
let selectedPaths = [];
let resultItems = [];
@@ -76,15 +96,38 @@ function basename(p) {
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)';
fileNameEl.textContent = '(no files selected)';
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 = '';
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', async () => {
@@ -114,15 +157,27 @@ regenAllBtn.addEventListener('click', () => {
clearBtn.addEventListener('click', () => {
selectedPaths = [];
selectedPage = 0;
renderSelection();
});
pickFolderBtn.addEventListener('click', async () => {
const r = await fetch('/api/applovin/pickFolder', { method: 'POST' });
const j = await r.json();
if (j.files && j.files.length) {
const added = j.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();
}
});
pickBtn.addEventListener('click', async () => {
const r = await fetch('/api/applovin/pick', { method: 'POST' });
const j = await r.json();
if (j.files && j.files.length) {
const added = j.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();
}
});
@@ -136,8 +191,8 @@ uploadBtn.addEventListener('click', async () => {
}
uploadBtn.disabled = true;
resultItems = [];
saveAllBtn.style.display = 'none';
regenAllBtn.style.display = 'none';
saveAllBtn.disabled = true;
regenAllBtn.disabled = true;
try {
const res = await fetch('/api/applovin/upload', {
method: 'POST',
@@ -174,24 +229,40 @@ function escapeHtml(s){
return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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>';
return resultsEl.querySelector('tbody');
}
qrModal.addEventListener('click', () => qrModal.classList.remove('open'));
function handle(m) {
if (m.type === 'status') {
statusEl.textContent = m.message;
} 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);
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 wrap = document.createElement('div');
wrap.className = 'result';
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 + '" 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 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 openBtn = document.createElement('button');
@@ -218,13 +289,12 @@ function handle(m) {
}
};
actions.appendChild(openBtn); actions.appendChild(saveQr);
wrap.appendChild(actions);
resultsEl.appendChild(wrap);
actionCell.appendChild(actions);
row.append(file, qr, actionCell, error);
tbody.appendChild(row);
resultItems.push({ name: m.name, url: m.qrImg });
if (resultItems.length >= 2) {
saveAllBtn.style.display = '';
regenAllBtn.style.display = '';
}
saveAllBtn.disabled = resultItems.length === 0;
regenAllBtn.disabled = resultItems.length === 0;
} else if (m.type === 'done') {
statusEl.innerHTML = '<span class="ok">Done.</span>';
}
@@ -232,7 +302,7 @@ function handle(m) {
</script>
`
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = w.Write([]byte(Page("/applovin", "AppLovin Upload", body)))
_, _ = w.Write([]byte(Page("/applovin", "AppLovin Playable Preview", body)))
}
func ApplovinPick(w http.ResponseWriter, r *http.Request) {
@@ -253,6 +323,23 @@ func ApplovinPick(w http.ResponseWriter, r *http.Request) {
writeJSON(w, map[string]any{"files": files})
}
func ApplovinPickFolder(w http.ResponseWriter, r *http.Request) {
cfg := LoadConfig()
dir := PickFolder("Select folder", cfg.LastPickDir)
if dir == "" {
writeJSON(w, map[string]any{"files": []any{}})
return
}
cfg.LastPickDir = dir
_ = SaveConfig(cfg)
paths := collectHTMLFiles(dir)
files := make([]map[string]string, 0, len(paths))
for _, p := range paths {
files = append(files, map[string]string{"path": p, "name": filepath.Base(p)})
}
writeJSON(w, map[string]any{"files": files})
}
func ApplovinUpload(w http.ResponseWriter, r *http.Request) {
var req struct {
Paths []string `json:"paths"`