This commit is contained in:
2026-06-08 22:31:02 +08:00
parent ad6403f3e4
commit 6c09beea43
7 changed files with 210 additions and 14 deletions

View File

@@ -16,7 +16,7 @@ func HomePage(w http.ResponseWriter, r *http.Request) {
if n.Beta {
badge = ` <span class="beta-pill">Beta</span>`
}
items.WriteString(`<button type="button" class="home-tool" data-path="` + n.Path + `"><span class="home-tool-title">` + n.Label + badge + `</span><span class="home-tool-desc">` + n.Description + `</span></button>`)
items.WriteString(`<button type="button" class="home-tool" draggable="true" data-path="` + n.Path + `"><span class="home-tool-title">` + n.Label + badge + `</span><span class="home-tool-desc">` + n.Description + `</span></button>`)
}
body := `
@@ -43,6 +43,7 @@ func HomePage(w http.ResponseWriter, r *http.Request) {
font:inherit;
}
.home-tool:hover { background:#33373a; border-color:#007fd4; }
.home-tool.dragging { opacity:0.55; }
.home-tool-title { display:flex; align-items:center; justify-content:space-between; gap:8px; font-size:12px; font-weight:600; }
.home-tool-desc { display:block; margin-top:4px; color:#a7a7a7; font-size:11px; line-height:1.35; }
</style>

View File

@@ -104,6 +104,7 @@ const SharedCSS = `
}
.sidebar .nav-link:hover { background: #2a2d2e; }
.sidebar .nav-link.active { background: #094771; color: #fff; }
.sidebar .nav-link.dragging { opacity: 0.55; }
.nav-icon {
width: 28px;
height: 26px;
@@ -478,7 +479,7 @@ func navInitials(label string) string {
case "Mintegral Checker":
return "MiC"
case "Device Simulator":
return "SIM"
return "DS"
}
words := strings.Fields(label)
if len(words) == 0 {
@@ -512,7 +513,7 @@ func Page(activePath, title, body string) string {
if n.Beta {
label += `<span class="beta-pill">Beta</span>`
}
tabs.WriteString(`<button type="button" class="nav-link` + cls + `" data-path="` + n.Path + `" title="` + n.Label + `"><span class="nav-icon">` + navInitials(n.Label) + `</span><span class="nav-text">` + label + `</span></button>`)
tabs.WriteString(`<button type="button" class="nav-link` + cls + `" data-path="` + n.Path + `" draggable="` + boolAttr(n.Path != "/") + `" title="` + n.Label + `"><span class="nav-icon">` + navInitials(n.Label) + `</span><span class="nav-text">` + label + `</span></button>`)
}
betaButtonLabel := "Show beta"
@@ -552,10 +553,72 @@ document.getElementById('sidebarToggle').addEventListener('click', () => {
});
document.querySelectorAll('[data-path]').forEach((element) => {
element.addEventListener('click', () => {
if (element.dataset.dragging === 'true') {
element.dataset.dragging = 'false';
return;
}
const path = element.dataset.path;
if (path) window.location.href = path;
});
});
const TOOL_ORDER_KEY = 'hplToolbox.toolOrder.v1';
function getToolOrder() {
try {
const parsed = JSON.parse(localStorage.getItem(TOOL_ORDER_KEY) || '[]');
return Array.isArray(parsed) ? parsed.filter(path => typeof path === 'string') : [];
} catch {
return [];
}
}
function setToolOrder(paths) {
localStorage.setItem(TOOL_ORDER_KEY, JSON.stringify(paths));
}
function applyStoredToolOrder(container, selector) {
if (!container) return;
const order = getToolOrder();
if (!order.length) return;
const elements = Array.from(container.querySelectorAll(selector));
const byPath = new Map(elements.map(el => [el.dataset.path, el]));
const home = byPath.get('/');
const ordered = order.map(path => byPath.get(path)).filter(Boolean);
const orderedSet = new Set(ordered);
const remaining = elements.filter(el => el !== home && !orderedSet.has(el));
if (home) container.appendChild(home);
ordered.concat(remaining).forEach(el => container.appendChild(el));
}
function setupToolReorder(container, selector) {
if (!container) return;
applyStoredToolOrder(container, selector);
let dragged = null;
container.querySelectorAll(selector).forEach(element => {
if (element.dataset.path === '/') return;
element.addEventListener('dragstart', event => {
dragged = element;
element.classList.add('dragging');
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', element.dataset.path || '');
});
element.addEventListener('dragend', () => {
element.classList.remove('dragging');
element.dataset.dragging = 'true';
dragged = null;
const paths = Array.from(container.querySelectorAll(selector))
.map(el => el.dataset.path)
.filter(path => path && path !== '/');
setToolOrder(paths);
setTimeout(() => { element.dataset.dragging = 'false'; }, 0);
});
element.addEventListener('dragover', event => {
event.preventDefault();
if (!dragged || dragged === element || element.dataset.path === '/') return;
const rect = element.getBoundingClientRect();
const after = event.clientY > rect.top + rect.height / 2;
container.insertBefore(dragged, after ? element.nextSibling : element);
});
});
}
setupToolReorder(document.querySelector('.sidebar-nav'), '.nav-link');
setupToolReorder(document.querySelector('.home-tools'), '.home-tool');
document.getElementById('betaToolsToggle').addEventListener('click', async (event) => {
const enabled = event.currentTarget.dataset.enabled === 'true';
await fetch('/api/betaTools', {

View File

@@ -457,7 +457,7 @@ func shareHandler(s *activeShare) http.Handler {
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Cache-Control", "no-store")
_, _ = w.Write(s.files[0].buf)
_, _ = w.Write([]byte(mobilePreviewPage(s.files[0])))
case "/file":
fallthrough
case "/file/0":
@@ -481,7 +481,7 @@ func shareHandler(s *activeShare) http.Handler {
if parts[0] == "view" {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Cache-Control", "no-store")
_, _ = w.Write(file.buf)
_, _ = w.Write([]byte(mobilePreviewPage(file)))
return
}
safeName := safeFilename(file.filename)
@@ -499,6 +499,35 @@ func shareHandler(s *activeShare) http.Handler {
})
}
func mobilePreviewPage(file sharedFile) string {
playableJSON, _ := json.Marshal(string(file.buf))
playableScriptValue := strings.ReplaceAll(string(playableJSON), "</script", "<\\/script")
playableScriptValue = strings.ReplaceAll(playableScriptValue, "</SCRIPT", "<\\/SCRIPT")
title := html.EscapeString(file.filename)
return `<!DOCTYPE html>
<html><head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>` + title + `</title>
<style>
html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; background: #000; }
iframe { position: fixed; inset: 0; width: 100%; height: 100%; border: 0; background: #000; }
.boot { position: fixed; inset: 0; display: grid; place-items: center; color: #777; font: 12px system-ui, sans-serif; }
</style>
</head><body>
<div id="boot" class="boot">Loading preview...</div>
<iframe id="playable" sandbox="allow-scripts allow-same-origin allow-pointer-lock allow-forms allow-popups allow-popups-to-escape-sandbox allow-top-navigation-by-user-activation"></iframe>
<script>
const playableHtml = ` + playableScriptValue + `;
setTimeout(() => {
const frame = document.getElementById('playable');
document.getElementById('boot').style.display = 'none';
frame.srcdoc = playableHtml;
}, 750);
</script>
</body></html>`
}
var unsafeFilenameRx = regexp.MustCompile(`[^A-Za-z0-9._-]`)
func safeFilename(s string) string { return unsafeFilenameRx.ReplaceAllString(s, "_") }