This commit is contained in:
2026-06-22 15:10:22 +08:00
parent 8e5d56ab1d
commit 0e30ed0b36
16 changed files with 911 additions and 1017 deletions

View File

@@ -311,6 +311,27 @@ const SharedCSS = `
.err { color: #f48771; white-space: pre-wrap; }
.ok { color: #89d185; }
.hint { font-size: 12px; opacity: 0.7; }
.settings-modal { position:fixed; inset:0; z-index:200; display:none; align-items:center; justify-content:center; padding:18px; background:rgba(0,0,0,.48); }
.settings-modal.active { display:flex; }
.settings-modal-dialog { width:min(840px,94vw); min-height:min(560px,80vh); max-height:calc(100vh - 36px); display:flex; flex-direction:column; border:1px solid var(--tool-border); border-radius:var(--tool-radius); background:#1e1e1e; box-shadow:0 16px 48px rgba(0,0,0,.45); overflow:hidden; }
.settings-modal-dialog > .panel-header { flex-shrink:0; }
.settings-modal-body { overflow-y:auto; padding:12px; flex:1 1 0; min-height:0; }
.settings-modal-body .tool-panel { margin-bottom:var(--tool-gap-lg); scroll-margin-top:8px; }
@keyframes settings-flash { 0%,100%{box-shadow:none} 25%,75%{box-shadow:0 0 0 2px #007fd4} }
.settings-highlight { animation:settings-flash 1.2s ease forwards; }
.settings-grid { display:flex; flex-direction:column; gap:12px; }
.settings-label { display:flex; flex-direction:column; gap:5px; color:#a7a7a7; font-size:11px; font-weight:700; letter-spacing:.4px; text-transform:uppercase; max-width:100%; }
.settings-label input[type=text] { width:100%; }
.settings-label.settings-checkbox { flex-direction:row; align-items:center; gap:8px; text-transform:none; letter-spacing:0; font-size:13px; font-weight:400; color:#ddd; cursor:pointer; }
.settings-hint { color:#a7a7a7; font-size:11px; font-weight:400; letter-spacing:0; text-transform:none; }
.settings-save-status { font-size:12px; color:#89d185; min-height:20px; }
.settings-save-status.err { color:#f48771; }
.advanced-details { margin-bottom:var(--tool-gap-lg); }
.advanced-summary { display:inline-flex; align-items:center; gap:6px; margin-bottom:10px; padding:5px 10px; color:#a7a7a7; font-size:11px; font-weight:700; letter-spacing:.4px; text-transform:uppercase; border:1px solid var(--tool-border); border-radius:var(--tool-radius); background:rgba(128,128,128,.08); cursor:pointer; user-select:none; list-style:none; }
.advanced-summary::-webkit-details-marker { display:none; }
.advanced-summary::before { content:"▶"; font-size:9px; transition:transform 120ms ease; }
details[open] > .advanced-summary::before { transform:rotate(90deg); }
.advanced-summary:hover { color:#ddd; border-color:#555; }
`
const SharedDropZoneScript = `
@@ -483,7 +504,7 @@ var navItems = []navItem{
{Path: "/mobile", Label: "Send To Mobile", Description: "Share HTML to a phone via LAN + QR"},
{Path: "/mraid", Label: "MRAID Checker", Description: "Check MRAID requirements and best practices"},
{Path: "/mintegral", Label: "Mintegral Checker", Description: "Validate Mintegral playable ZIPs against PlayTurbo checks"},
{Path: "/playworks", Label: "Playworks Converter", Description: "Convert Playworks HTML to per-network variants", Beta: true},
{Path: "/playworks", Label: "Playworks Converter", Description: "Convert Playworks HTML to per-network variants"},
{Path: "/device-simulator", Label: "Device Simulator", Description: "Preview HTML playables inside mobile device frames"},
}
@@ -555,6 +576,7 @@ func navInitials(label string) string {
func Page(activePath, title, body string) string {
cfg := LoadConfig()
betaToolsEnabled := cfg.BetaToolsEnabled
randomizeUA := cfg.Applovin.RandomizeUserAgent == nil || *cfg.Applovin.RandomizeUserAgent
var tabs strings.Builder
for _, n := range visibleNavItems(betaToolsEnabled) {
cls := ""
@@ -730,7 +752,156 @@ if (sessionStorage.getItem('hplToolboxUpdateChecked') !== 'true') {
sessionStorage.setItem('hplToolboxUpdateChecked', 'true');
checkForUpdates(true);
}
function openSettingsModal(sectionId) {
document.getElementById('settingsModal').classList.add('active');
if (sectionId) {
const el = document.getElementById('settings-section-' + sectionId);
if (el) {
const details = el.closest('details');
if (details) details.open = true;
setTimeout(function() {
el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
el.classList.add('settings-highlight');
setTimeout(function() { el.classList.remove('settings-highlight'); }, 1300);
}, 50);
}
}
}
function closeSettingsModal() {
document.getElementById('settingsModal').classList.remove('active');
}
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && document.getElementById('settingsModal').classList.contains('active')) closeSettingsModal();
});
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('settingsModalClose').addEventListener('click', closeSettingsModal);
document.getElementById('settingsModal').addEventListener('click', function(e) {
if (e.target === this) closeSettingsModal();
});
document.getElementById('settingsModalForm').addEventListener('submit', async (e) => {
e.preventDefault();
const form = e.currentTarget;
const status = document.getElementById('settingsModalStatus');
const getVal = (name) => (form.querySelector('[name="' + name + '"]') || {}).value || '';
const getChecked = (name) => !!(form.querySelector('[name="' + name + '"]') || {}).checked;
const payload = {
plec: { uploadUrl: getVal('plec.uploadUrl'), originUrl: getVal('plec.originUrl'), previewBase: getVal('plec.previewBase'), skipExistsCheck: getChecked('plec.skipExistsCheck') },
applovin: { cookie: getVal('applovin.cookie'), delayMs: parseInt(getVal('applovin.delayMs'), 10) || 0, randomizeUserAgent: getChecked('applovin.randomizeUserAgent') },
sendToMobile: { port: parseInt(getVal('sendToMobile.port'), 10) || 0 },
projectInitManifestUrl: getVal('projectInitManifestUrl'),
deviceSimulator: { devicesUrl: getVal('deviceSimulator.devicesUrl'), languagesUrl: getVal('deviceSimulator.languagesUrl') },
};
try {
const r = await fetch('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
const j = await r.json();
if (j.ok) {
status.textContent = 'Saved.';
status.className = 'settings-save-status';
setTimeout(closeSettingsModal, 900);
} else {
status.textContent = 'Error: ' + (j.error || 'unknown');
status.className = 'settings-save-status err';
}
} catch (err) {
status.textContent = 'Error: ' + (err.message || err);
status.className = 'settings-save-status err';
}
setTimeout(function() { status.textContent = ''; status.className = 'settings-save-status'; }, 4000);
});
});
</script>
<div id="settingsModal" class="settings-modal" role="dialog" aria-modal="true" aria-labelledby="settingsModalTitle">
<div class="settings-modal-dialog">
<div class="panel-header" style="display:flex;align-items:center;justify-content:space-between;gap:8px;">
<h3 id="settingsModalTitle" class="panel-title">Settings</h3>
<button id="settingsModalClose" type="button" class="icon-btn" style="margin-left:auto;" title="Close" aria-label="Close">&times;</button>
</div>
<div class="settings-modal-body">
<form id="settingsModalForm">
<section id="settings-section-device-simulator" class="tool-panel">
<div class="panel-header"><h3 class="panel-title">Device Simulator</h3></div>
<div class="panel-body">
<div class="settings-grid">
<label class="settings-label">Devices JSON URL
<input type="text" name="deviceSimulator.devicesUrl" value="` + htmlEsc(cfg.DeviceSimulator.DevicesURL) + `" placeholder="Google Drive share link or direct JSON URL" />
<span class="settings-hint">Remote devices.json — fetched on open, falls back to built-in list if empty or unreachable.</span>
</label>
<label class="settings-label">Languages JSON URL
<input type="text" name="deviceSimulator.languagesUrl" value="` + htmlEsc(cfg.DeviceSimulator.LanguagesURL) + `" placeholder="Google Drive share link or direct JSON URL" />
<span class="settings-hint">Remote languages.json — fetched on open, falls back to built-in list if empty or unreachable.</span>
</label>
</div>
</div>
</section>
<section id="settings-section-initialize-project" class="tool-panel">
<div class="panel-header"><h3 class="panel-title">Initialize Project</h3></div>
<div class="panel-body">
<div class="settings-grid">
<label class="settings-label">Manifest URL
<input type="text" name="projectInitManifestUrl" value="` + htmlEsc(cfg.ProjectInitManifestURL) + `" placeholder="Google Drive share link or direct JSON URL" />
</label>
</div>
</div>
</section>
<details class="advanced-details">
<summary class="advanced-summary">Advanced</summary>
<section class="tool-panel">
<div class="panel-header"><h3 class="panel-title">PLEC Upload</h3></div>
<div class="panel-body">
<div class="settings-grid">
<label class="settings-label">Upload URL
<input type="text" name="plec.uploadUrl" value="` + htmlEsc(cfg.Plec.UploadUrl) + `" />
</label>
<label class="settings-label">Origin URL
<input type="text" name="plec.originUrl" value="` + htmlEsc(cfg.Plec.OriginUrl) + `" />
</label>
<label class="settings-label">Preview Base URL
<input type="text" name="plec.previewBase" value="` + htmlEsc(cfg.Plec.PreviewBase) + `" />
</label>
<label class="settings-label settings-checkbox">
<input type="checkbox" name="plec.skipExistsCheck" ` + checkedAttr(cfg.Plec.SkipExistsCheck) + ` />
Skip duplicate filename check
</label>
</div>
</div>
</section>
<section class="tool-panel">
<div class="panel-header"><h3 class="panel-title">AppLovin Playable Preview</h3></div>
<div class="panel-body">
<div class="settings-grid">
<label class="settings-label">__Host-APPLOVINID Cookie
<input type="text" name="applovin.cookie" value="` + htmlEsc(cfg.Applovin.Cookie) + `" placeholder="Log in at p.applov.in and copy from DevTools" />
</label>
<label class="settings-label">Upload delay (ms)
<input type="text" name="applovin.delayMs" value="` + intStr(cfg.Applovin.DelayMs) + `" inputmode="numeric" />
</label>
<label class="settings-label settings-checkbox">
<input type="checkbox" name="applovin.randomizeUserAgent" ` + checkedAttr(randomizeUA) + ` />
Randomize User-Agent on every request
</label>
</div>
</div>
</section>
<section class="tool-panel">
<div class="panel-header"><h3 class="panel-title">Send To Mobile</h3></div>
<div class="panel-body">
<div class="settings-grid">
<label class="settings-label">LAN server port
<input type="text" name="sendToMobile.port" value="` + intStr(cfg.SendToMobile.Port) + `" inputmode="numeric" />
<span class="settings-hint">0 = OS-assigned random port</span>
</label>
</div>
</div>
</section>
</details>
<div class="action-row" style="margin-top:4px;">
<button type="submit">Save Settings</button>
<span id="settingsModalStatus" class="settings-save-status"></span>
</div>
</form>
</div>
</div>
</div>
</body></html>`
}