diff --git a/README.md b/README.md index b2cd8f2..6b47ef0 100644 --- a/README.md +++ b/README.md @@ -85,3 +85,23 @@ Changed - Standalone navigation now uses a collapsible sidebar with compact tool initials. - Mintegral Checker UI now embeds the playable preview, checklist, runtime events, mute, and reload controls in one view. ``` + +**v0.2.1** +``` +Added + - Added Device Simulator to the VS Code extension and standalone app. + - Added editable Device Simulator device lists with add, delete, restore defaults, import, and export. + - Added drag-and-drop reordering for Device Simulator devices. + - Added iPhone 17, iPhone Air, iPhone 17 Pro, and iPhone 17 Pro Max defaults. + - Added drag-and-drop tool reordering with persisted order in the VS Code launcher and standalone Home/sidebar. + +Changed + - Device Simulator settings now use a modal list manager with multi-select, add/delete, and move up/down controls. + - Standalone Device Simulator sidebar abbreviation is now DS. + - Send To Mobile browser preview now opens playables in a delayed full-screen preview wrapper while Download still serves the original HTML. + +Fixed + - Fixed Device Simulator mute for HTML media and WebAudio playables. + - Fixed Device Simulator Add Device and Remove Device interactions inside embedded webviews. + - Fixed Send To Mobile browser preview spending the View button's initial tap as a premature playable click-through while preserving real user-initiated redirects. +``` diff --git a/package.json b/package.json index 95c6127..2ea5c6b 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "hpl-toolbox", "displayName": "HPL Toolbox", "description": "Bundled tools: PLEC Upload, AppLovin Playable Preview, Base64 Scanner, MRAID Checker. Local HTML Host.", - "version": "0.2.2", + "version": "0.2.1", "publisher": "hesukastro", "license": "UNLICENSED", "repository": { diff --git a/src/launcherView.ts b/src/launcherView.ts index fd64ce3..82df3fb 100644 --- a/src/launcherView.ts +++ b/src/launcherView.ts @@ -3,6 +3,7 @@ import * as fs from 'fs'; import * as path from 'path'; const BETA_TOOLS_ENABLED_KEY = 'hplToolbox.betaTools.enabled'; +const TOOL_ORDER_KEY = 'hplToolbox.toolOrder.v1'; interface ToolDefinition { id?: string; @@ -54,7 +55,8 @@ export class LauncherViewProvider implements vscode.WebviewViewProvider { const version = this.context.extension.packageJSON.version as string; const tools = loadToolDefinitions(this.context.extensionPath); const betaToolsEnabled = this.context.globalState.get(BETA_TOOLS_ENABLED_KEY, false); - view.webview.html = getHtml(version, betaToolsEnabled, tools); + const toolOrder = this.context.globalState.get(TOOL_ORDER_KEY, []); + view.webview.html = getHtml(version, betaToolsEnabled, tools, toolOrder); view.webview.onDidReceiveMessage(async (msg) => { if (msg?.type === 'open' && typeof msg.command === 'string') { vscode.commands.executeCommand(msg.command); @@ -62,7 +64,9 @@ export class LauncherViewProvider implements vscode.WebviewViewProvider { vscode.commands.executeCommand('hplToolbox.openChangelog'); } else if (msg?.type === 'setBetaToolsEnabled' && typeof msg.enabled === 'boolean') { await this.context.globalState.update(BETA_TOOLS_ENABLED_KEY, msg.enabled); - view.webview.html = getHtml(version, msg.enabled, tools); + view.webview.html = getHtml(version, msg.enabled, tools, this.context.globalState.get(TOOL_ORDER_KEY, [])); + } else if (msg?.type === 'setToolOrder' && Array.isArray(msg.order)) { + await this.context.globalState.update(TOOL_ORDER_KEY, msg.order.filter((id: unknown) => typeof id === 'string')); } }); } @@ -88,8 +92,8 @@ function loadToolDefinitions(extensionPath: string): ToolDefinition[] { } } -function getHtml(version: string, betaToolsEnabled: boolean, tools: ToolDefinition[]): string { - const visibleTools = sortToolsForDisplay(tools.filter(tool => betaToolsEnabled || !tool.beta)); +function getHtml(version: string, betaToolsEnabled: boolean, tools: ToolDefinition[], toolOrder: string[]): string { + const visibleTools = sortToolsForDisplay(tools.filter(tool => betaToolsEnabled || !tool.beta), toolOrder); const toolButtons = visibleTools.map(renderToolButton).join('\n'); return ` @@ -127,6 +131,9 @@ function getHtml(version: string, betaToolsEnabled: boolean, tools: ToolDefiniti cursor: pointer; font: inherit; } + .tool-btn.dragging { + opacity: 0.55; + } .tool-btn:hover { background: var(--vscode-list-hoverBackground, var(--vscode-button-secondaryHoverBackground)); border-color: var(--vscode-focusBorder, var(--vscode-panel-border)); @@ -217,9 +224,44 @@ ${toolButtons} const vscode = acquireVsCodeApi(); document.querySelectorAll('.tool-btn').forEach((btn) => { btn.addEventListener('click', () => { + if (btn.dataset.dragging === 'true') { + btn.dataset.dragging = 'false'; + return; + } vscode.postMessage({ type: 'open', command: btn.dataset.cmd }); }); }); + const toolsEl = document.querySelector('.tools'); + let dragged = null; + toolsEl.querySelectorAll('.tool-btn').forEach((btn) => { + btn.draggable = true; + btn.addEventListener('dragstart', (event) => { + dragged = btn; + btn.classList.add('dragging'); + event.dataTransfer.effectAllowed = 'move'; + event.dataTransfer.setData('text/plain', btn.dataset.id || ''); + }); + btn.addEventListener('dragend', () => { + btn.classList.remove('dragging'); + btn.dataset.dragging = 'true'; + dragged = null; + persistToolOrder(); + setTimeout(() => { btn.dataset.dragging = 'false'; }, 0); + }); + btn.addEventListener('dragover', (event) => { + event.preventDefault(); + if (!dragged || dragged === btn) return; + const rect = btn.getBoundingClientRect(); + const after = event.clientY > rect.top + rect.height / 2; + toolsEl.insertBefore(dragged, after ? btn.nextSibling : btn); + }); + }); + function persistToolOrder() { + vscode.postMessage({ + type: 'setToolOrder', + order: Array.from(toolsEl.querySelectorAll('.tool-btn')).map(btn => btn.dataset.id).filter(Boolean), + }); + } document.getElementById('betaToggle').addEventListener('click', (event) => { const enabled = event.currentTarget.dataset.enabled === 'true'; vscode.postMessage({ type: 'setBetaToolsEnabled', enabled: !enabled }); @@ -232,17 +274,29 @@ ${toolButtons} `; } -function sortToolsForDisplay(tools: ToolDefinition[]): ToolDefinition[] { - return [...tools].sort((a, b) => Number(Boolean(a.beta)) - Number(Boolean(b.beta))); +function sortToolsForDisplay(tools: ToolDefinition[], toolOrder: string[]): ToolDefinition[] { + const order = new Map(toolOrder.map((id, index) => [id, index])); + return [...tools].sort((a, b) => { + const aId = toolId(a); + const bId = toolId(b); + const ai = order.has(aId) ? order.get(aId)! : Number.MAX_SAFE_INTEGER; + const bi = order.has(bId) ? order.get(bId)! : Number.MAX_SAFE_INTEGER; + if (ai !== bi) return ai - bi; + return Number(Boolean(a.beta)) - Number(Boolean(b.beta)); + }); } function renderToolButton(tool: ToolDefinition): string { - return ` `; } +function toolId(tool: ToolDefinition): string { + return tool.id || tool.command; +} + function escapeHtml(value: string): string { return value.replace(/[&<>"']/g, ch => ({ '&': '&', diff --git a/src/tools/sendToMobile.ts b/src/tools/sendToMobile.ts index a50b0a2..d2df987 100644 --- a/src/tools/sendToMobile.ts +++ b/src/tools/sendToMobile.ts @@ -234,7 +234,7 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse, shar 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store', }); - res.end(file.buf); + res.end(mobilePreviewPage(file)); return; } @@ -296,6 +296,35 @@ function chooserPage(files: SharedFile[]): string { `; } +function mobilePreviewPage(file: SharedFile): string { + const playableJson = JSON.stringify(file.buf.toString('utf8')).replace(/<\/script/gi, '<\\/script'); + const title = file.filename.replace(/[&<>"]/g, (c) => + ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c] as string) + ); + return ` + + + +${title} + + +
Loading preview...
+ + +`; +} + interface LanIp { address: string; iface: string; } function getLanIps(): LanIp[] { diff --git a/standalone/home.go b/standalone/home.go index eeb3922..4fd0cb5 100644 --- a/standalone/home.go +++ b/standalone/home.go @@ -16,7 +16,7 @@ func HomePage(w http.ResponseWriter, r *http.Request) { if n.Beta { badge = ` Beta` } - items.WriteString(``) + items.WriteString(``) } 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; } diff --git a/standalone/layout.go b/standalone/layout.go index 09eef91..00eabec 100644 --- a/standalone/layout.go +++ b/standalone/layout.go @@ -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 += `Beta` } - tabs.WriteString(``) + tabs.WriteString(``) } 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', { diff --git a/standalone/mobile.go b/standalone/mobile.go index 6107f3f..ed73e00 100644 --- a/standalone/mobile.go +++ b/standalone/mobile.go @@ -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), " + + + +` + title + ` + + +
Loading preview...
+ + +` +} + var unsafeFilenameRx = regexp.MustCompile(`[^A-Za-z0-9._-]`) func safeFilename(s string) string { return unsafeFilenameRx.ReplaceAllString(s, "_") }