diff --git a/README.md b/README.md index 0b45bdf..410c225 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,8 @@ Bundled VS Code extension and standalone tools for playable ad workflows. - **Send To Mobile** - Share selected HTML files to a phone over LAN with a QR code. - **MRAID Checker** - Check HTML files against MRAID requirements and best practices. - **Mintegral Checker** - Validate Mintegral playable ZIPs against PlayTurbo requirements. -- **Playworks Converter** - Convert Playworks HTML into per-network variants. Beta. +- **Playworks Converter** - Convert Playworks HTML into per-network variants. +- **Device Simulator** - Preview HTML playables inside accurate mobile device frames with notch and Dynamic Island overlays. - **Initialize Project** - Download team template files (AGENTS.md, LOCALIZATION.md, .gitignore) from a shared Google Drive manifest. # Changelog @@ -131,3 +132,22 @@ Added - Destination folder is selectable via folder picker, defaulting to workspace root in the extension. - Standalone stores the manifest URL in config.json and exposes an inline URL editor via the settings (⚙) button. ``` + +**v1.0.0** +``` +Added + - Added remote devices.json and languages.json support for Device Simulator. + - Device and language lists are fetched from a configurable Google Drive URL on open and cached for offline use, with built-in defaults as fallback. + - Device Simulator remote JSON URLs are configurable in VS Code extension settings (hplToolbox.deviceSimulator.devicesUrl / languagesUrl). + - Settings modal in standalone — accessible from any page via the ⚙ button, with Device Simulator and Initialize Project sections highlighted when opened from those tools. + - Device list is now sorted alphabetically in both extension and standalone. + +Changed + - Device Simulator settings gear now opens the VS Code extension settings (extension) or the global settings modal (standalone). + - Device management (add/edit/delete/reorder) removed from Device Simulator in both extension and standalone; device list is now managed via remote JSON. + - Settings page in standalone replaced by a modal overlay available on every page. + - Initialize Project manifest URL is now configured via the settings modal instead of the inline settings panel. + +Fixed + - Fixed Device Simulator not appearing in the VS Code extension launcher due to a trailing comma in tools.json causing JSON.parse to fail and fall back to a hardcoded list that excluded it. +``` diff --git a/data/devices.json b/data/devices.json new file mode 100644 index 0000000..1df984d --- /dev/null +++ b/data/devices.json @@ -0,0 +1,243 @@ +{ + "version": "1.0.0", + "devices": [ + { + "id": "iphone-17-pro-max", + "name": "iPhone 17 Pro Max", + "sw": 440, + "sh": 956, + "scr": 44, + "notch": "island", + "nw": 126, + "nh": 37, + "nr": 20, + "sat": 62, + "sab": 34 + }, + { + "id": "iphone-17-pro", + "name": "iPhone 17 Pro", + "sw": 402, + "sh": 874, + "scr": 42, + "notch": "island", + "nw": 126, + "nh": 37, + "nr": 20, + "sat": 62, + "sab": 34 + }, + { + "id": "iphone-air", + "name": "iPhone Air", + "sw": 420, + "sh": 912, + "scr": 42, + "notch": "island", + "nw": 126, + "nh": 37, + "nr": 20, + "sat": 62, + "sab": 34 + }, + { + "id": "iphone-17", + "name": "iPhone 17", + "sw": 402, + "sh": 874, + "scr": 42, + "notch": "island", + "nw": 126, + "nh": 37, + "nr": 20, + "sat": 62, + "sab": 34 + }, + { + "id": "iphone-16-pro-max", + "name": "iPhone 16 Pro Max", + "sw": 440, + "sh": 956, + "scr": 44, + "notch": "island", + "nw": 126, + "nh": 37, + "nr": 20, + "sat": 62, + "sab": 34 + }, + { + "id": "iphone-16-pro", + "name": "iPhone 16 Pro", + "sw": 402, + "sh": 874, + "scr": 42, + "notch": "island", + "nw": 126, + "nh": 37, + "nr": 20, + "sat": 62, + "sab": 34 + }, + { + "id": "iphone-16-plus", + "name": "iPhone 16 Plus", + "sw": 430, + "sh": 932, + "scr": 42, + "notch": "island", + "nw": 120, + "nh": 36, + "nr": 18, + "sat": 59, + "sab": 34 + }, + { + "id": "iphone-16", + "name": "iPhone 16", + "sw": 390, + "sh": 844, + "scr": 40, + "notch": "island", + "nw": 120, + "nh": 36, + "nr": 18, + "sat": 59, + "sab": 34 + }, + { + "id": "iphone-15-pro-max", + "name": "iPhone 15 Pro Max", + "sw": 430, + "sh": 932, + "scr": 42, + "notch": "island", + "nw": 120, + "nh": 36, + "nr": 18, + "sat": 59, + "sab": 34 + }, + { + "id": "iphone-15-pro", + "name": "iPhone 15 Pro", + "sw": 393, + "sh": 852, + "scr": 40, + "notch": "island", + "nw": 120, + "nh": 36, + "nr": 18, + "sat": 59, + "sab": 34 + }, + { + "id": "iphone-15-plus", + "name": "iPhone 15 Plus", + "sw": 430, + "sh": 932, + "scr": 42, + "notch": "island", + "nw": 120, + "nh": 36, + "nr": 18, + "sat": 59, + "sab": 34 + }, + { + "id": "iphone-15", + "name": "iPhone 15", + "sw": 390, + "sh": 844, + "scr": 38, + "notch": "island", + "nw": 120, + "nh": 36, + "nr": 18, + "sat": 59, + "sab": 34 + }, + { + "id": "iphone-14-pro-max", + "name": "iPhone 14 Pro Max", + "sw": 430, + "sh": 932, + "scr": 42, + "notch": "island", + "nw": 120, + "nh": 36, + "nr": 18, + "sat": 59, + "sab": 34 + }, + { + "id": "iphone-14-pro", + "name": "iPhone 14 Pro", + "sw": 393, + "sh": 852, + "scr": 40, + "notch": "island", + "nw": 120, + "nh": 36, + "nr": 18, + "sat": 59, + "sab": 34 + }, + { + "id": "iphone-14-plus", + "name": "iPhone 14 Plus", + "sw": 428, + "sh": 926, + "scr": 40, + "notch": "notch", + "nw": 130, + "nh": 34, + "sat": 47, + "sab": 34 + }, + { + "id": "iphone-14", + "name": "iPhone 14", + "sw": 390, + "sh": 844, + "scr": 38, + "notch": "notch", + "nw": 130, + "nh": 34, + "sat": 47, + "sab": 34 + }, + { + "id": "iphone-se3", + "name": "iPhone SE (3rd gen)", + "sw": 375, + "sh": 667, + "scr": 4, + "notch": "none", + "sat": 20, + "sab": 0 + }, + { + "id": "pixel-8", + "name": "Google Pixel 8", + "sw": 411, + "sh": 914, + "scr": 22, + "notch": "punch", + "ps": 14, + "sat": 28, + "sab": 24 + }, + { + "id": "galaxy-s24", + "name": "Samsung Galaxy S24", + "sw": 360, + "sh": 780, + "scr": 20, + "notch": "punch", + "ps": 12, + "sat": 28, + "sab": 24 + } + ] +} diff --git a/data/languages.json b/data/languages.json new file mode 100644 index 0000000..3606b56 --- /dev/null +++ b/data/languages.json @@ -0,0 +1,15 @@ +{ + "version": "1.0.0", + "languages": [ + { "name": "English", "code": "en" }, + { "name": "Chinese (Simplified)", "code": "zh-hans" }, + { "name": "Chinese (Traditional)", "code": "zh-hant" }, + { "name": "French", "code": "fr" }, + { "name": "German", "code": "de" }, + { "name": "Japanese", "code": "ja" }, + { "name": "Korean", "code": "ko" }, + { "name": "Portuguese", "code": "pt" }, + { "name": "Russian", "code": "ru" }, + { "name": "Spanish", "code": "es" } + ] +} diff --git a/dist/hpl-toolbox-0.2.4.exe b/dist/hpl-toolbox-1.0.0.exe similarity index 69% rename from dist/hpl-toolbox-0.2.4.exe rename to dist/hpl-toolbox-1.0.0.exe index 9dd8a6c..da1da58 100644 Binary files a/dist/hpl-toolbox-0.2.4.exe and b/dist/hpl-toolbox-1.0.0.exe differ diff --git a/dist/hpl-toolbox-0.2.4.vsix b/dist/hpl-toolbox-1.0.0.vsix similarity index 99% rename from dist/hpl-toolbox-0.2.4.vsix rename to dist/hpl-toolbox-1.0.0.vsix index 9a2a57a..98928bc 100644 Binary files a/dist/hpl-toolbox-0.2.4.vsix and b/dist/hpl-toolbox-1.0.0.vsix differ diff --git a/package.json b/package.json index 8d2e8c1..2753bf2 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.4", + "version": "1.0.0", "publisher": "hesukastro", "license": "UNLICENSED", "repository": { @@ -75,7 +75,8 @@ { "type": "webview", "id": "hplToolbox.launcher", - "name": "" + "name": "", + "icon": "media/hpl.png" } ] }, @@ -145,6 +146,21 @@ "description": "URL to a JSON manifest file listing the files to download. The manifest must be an array of { \"filename\": string, \"url\": string } objects. Can be a raw GitHub/Gitea file URL or any direct JSON URL." } } + }, + { + "title": "HPL Toolbox — Device Simulator", + "properties": { + "hplToolbox.deviceSimulator.devicesUrl": { + "type": "string", + "default": "https://drive.google.com/file/d/1w04wviCmgNqtDCO1GLyYfwEeWpjBPGJ4/view?usp=drive_link", + "description": "URL to a remote devices.json file. When set, the Device Simulator fetches this on open and uses it as the default device list. Leave empty to use built-in defaults only. Supports Google Drive share links (file/d/...) or any direct JSON URL." + }, + "hplToolbox.deviceSimulator.languagesUrl": { + "type": "string", + "default": "https://drive.google.com/file/d/1I3ZiI8CLU2JxYZEtWCz6Vr0r8Uj2732f/view?usp=drive_link", + "description": "URL to a remote languages.json file. When set, the Device Simulator fetches this on open and populates the language dropdown from it. Leave empty to use built-in defaults only." + } + } } ] }, diff --git a/src/launcherView.ts b/src/launcherView.ts index d0318c2..165bcf1 100644 --- a/src/launcherView.ts +++ b/src/launcherView.ts @@ -49,7 +49,6 @@ const FALLBACK_TOOLS: ToolDefinition[] = [ command: 'hplToolbox.openPlayworksConverter', title: 'Playworks Converter', description: 'Convert Playworks HTML to per-network variants', - beta: true, }, ]; diff --git a/src/tools/deviceSimulator.ts b/src/tools/deviceSimulator.ts index bfd01e1..bdca44f 100644 --- a/src/tools/deviceSimulator.ts +++ b/src/tools/deviceSimulator.ts @@ -2,8 +2,26 @@ import * as vscode from 'vscode'; import * as fs from 'fs'; import * as path from 'path'; import * as http from 'http'; +import * as https from 'https'; import { getToolWebviewStyles } from './shared'; +const GS_DEVICES_KEY = 'hplDeviceSimulator.remoteDevices.v1'; +const GS_LANGUAGES_KEY = 'hplDeviceSimulator.remoteLanguages.v1'; + +function getSimulatorConfig() { + const cfg = vscode.workspace.getConfiguration('hplToolbox.deviceSimulator'); + return { + devicesUrl: (cfg.get('devicesUrl') || '').trim(), + languagesUrl: (cfg.get('languagesUrl') || '').trim(), + }; +} + +function resolveGDriveUrl(url: string): string { + const fileId = url.match(/\/file\/d\/([a-zA-Z0-9_-]+)/)?.[1] + ?? url.match(/[?&]id=([a-zA-Z0-9_-]+)/)?.[1]; + return fileId ? `https://drive.google.com/uc?export=download&id=${fileId}` : url; +} + const store: { panel: vscode.WebviewPanel | null; previewServer: http.Server | null; @@ -23,33 +41,49 @@ interface Device { } const DEVICES: Device[] = [ - // ── iPhone 17 ────────────────────────────────────────────────────────────── - { id: 'iphone-17-pro-max', name: 'iPhone 17 Pro Max', sw: 440, sh: 956, scr: 44, notch: 'island', nw: 126, nh: 37, nr: 20, sat: 62, sab: 34 }, - { id: 'iphone-17-pro', name: 'iPhone 17 Pro', sw: 402, sh: 874, scr: 42, notch: 'island', nw: 126, nh: 37, nr: 20, sat: 62, sab: 34 }, - { id: 'iphone-air', name: 'iPhone Air', sw: 420, sh: 912, scr: 42, notch: 'island', nw: 126, nh: 37, nr: 20, sat: 62, sab: 34 }, - { id: 'iphone-17', name: 'iPhone 17', sw: 402, sh: 874, scr: 42, notch: 'island', nw: 126, nh: 37, nr: 20, sat: 62, sab: 34 }, - // ── iPhone 16 ────────────────────────────────────────────────────────────── - { id: 'iphone-16-pro-max', name: 'iPhone 16 Pro Max', sw: 440, sh: 956, scr: 44, notch: 'island', nw: 126, nh: 37, nr: 20, sat: 62, sab: 34 }, - { id: 'iphone-16-pro', name: 'iPhone 16 Pro', sw: 402, sh: 874, scr: 42, notch: 'island', nw: 126, nh: 37, nr: 20, sat: 62, sab: 34 }, - { id: 'iphone-16-plus', name: 'iPhone 16 Plus', sw: 430, sh: 932, scr: 42, notch: 'island', nw: 120, nh: 36, nr: 18, sat: 59, sab: 34 }, - { id: 'iphone-16', name: 'iPhone 16', sw: 390, sh: 844, scr: 40, notch: 'island', nw: 120, nh: 36, nr: 18, sat: 59, sab: 34 }, - // ── iPhone 15 ────────────────────────────────────────────────────────────── - { id: 'iphone-15-pro-max', name: 'iPhone 15 Pro Max', sw: 430, sh: 932, scr: 42, notch: 'island', nw: 120, nh: 36, nr: 18, sat: 59, sab: 34 }, - { id: 'iphone-15-pro', name: 'iPhone 15 Pro', sw: 393, sh: 852, scr: 40, notch: 'island', nw: 120, nh: 36, nr: 18, sat: 59, sab: 34 }, - { id: 'iphone-15-plus', name: 'iPhone 15 Plus', sw: 430, sh: 932, scr: 42, notch: 'island', nw: 120, nh: 36, nr: 18, sat: 59, sab: 34 }, - { id: 'iphone-15', name: 'iPhone 15', sw: 390, sh: 844, scr: 38, notch: 'island', nw: 120, nh: 36, nr: 18, sat: 59, sab: 34 }, - // ── iPhone 14 ────────────────────────────────────────────────────────────── - { id: 'iphone-14-pro-max', name: 'iPhone 14 Pro Max', sw: 430, sh: 932, scr: 42, notch: 'island', nw: 120, nh: 36, nr: 18, sat: 59, sab: 34 }, - { id: 'iphone-14-pro', name: 'iPhone 14 Pro', sw: 393, sh: 852, scr: 40, notch: 'island', nw: 120, nh: 36, nr: 18, sat: 59, sab: 34 }, - { id: 'iphone-14-plus', name: 'iPhone 14 Plus', sw: 428, sh: 926, scr: 40, notch: 'notch', nw: 130, nh: 34, sat: 47, sab: 34 }, - { id: 'iphone-14', name: 'iPhone 14', sw: 390, sh: 844, scr: 38, notch: 'notch', nw: 130, nh: 34, sat: 47, sab: 34 }, - // ── Other ────────────────────────────────────────────────────────────────── - { id: 'iphone-se3', name: 'iPhone SE (3rd gen)', sw: 375, sh: 667, scr: 4, notch: 'none', sat: 20, sab: 0 }, - { id: 'pixel-8', name: 'Google Pixel 8', sw: 411, sh: 914, scr: 22, notch: 'punch', ps: 14, sat: 28, sab: 24 }, - { id: 'galaxy-s24', name: 'Samsung Galaxy S24', sw: 360, sh: 780, scr: 20, notch: 'punch', ps: 12, sat: 28, sab: 24 }, + { id: 'pixel-8', name: 'Google Pixel 8', sw: 411, sh: 914, scr: 22, notch: 'punch', ps: 14, sat: 28, sab: 24 }, + { id: 'iphone-14', name: 'iPhone 14', sw: 390, sh: 844, scr: 38, notch: 'notch', nw: 130, nh: 34, sat: 47, sab: 34 }, + { id: 'iphone-14-plus', name: 'iPhone 14 Plus', sw: 428, sh: 926, scr: 40, notch: 'notch', nw: 130, nh: 34, sat: 47, sab: 34 }, + { id: 'iphone-14-pro', name: 'iPhone 14 Pro', sw: 393, sh: 852, scr: 40, notch: 'island', nw: 120, nh: 36, nr: 18, sat: 59, sab: 34 }, + { id: 'iphone-14-pro-max', name: 'iPhone 14 Pro Max', sw: 430, sh: 932, scr: 42, notch: 'island', nw: 120, nh: 36, nr: 18, sat: 59, sab: 34 }, + { id: 'iphone-15', name: 'iPhone 15', sw: 390, sh: 844, scr: 38, notch: 'island', nw: 120, nh: 36, nr: 18, sat: 59, sab: 34 }, + { id: 'iphone-15-plus', name: 'iPhone 15 Plus', sw: 430, sh: 932, scr: 42, notch: 'island', nw: 120, nh: 36, nr: 18, sat: 59, sab: 34 }, + { id: 'iphone-15-pro', name: 'iPhone 15 Pro', sw: 393, sh: 852, scr: 40, notch: 'island', nw: 120, nh: 36, nr: 18, sat: 59, sab: 34 }, + { id: 'iphone-15-pro-max', name: 'iPhone 15 Pro Max', sw: 430, sh: 932, scr: 42, notch: 'island', nw: 120, nh: 36, nr: 18, sat: 59, sab: 34 }, + { id: 'iphone-16', name: 'iPhone 16', sw: 390, sh: 844, scr: 40, notch: 'island', nw: 120, nh: 36, nr: 18, sat: 59, sab: 34 }, + { id: 'iphone-16-plus', name: 'iPhone 16 Plus', sw: 430, sh: 932, scr: 42, notch: 'island', nw: 120, nh: 36, nr: 18, sat: 59, sab: 34 }, + { id: 'iphone-16-pro', name: 'iPhone 16 Pro', sw: 402, sh: 874, scr: 42, notch: 'island', nw: 126, nh: 37, nr: 20, sat: 62, sab: 34 }, + { id: 'iphone-16-pro-max', name: 'iPhone 16 Pro Max', sw: 440, sh: 956, scr: 44, notch: 'island', nw: 126, nh: 37, nr: 20, sat: 62, sab: 34 }, + { id: 'iphone-17', name: 'iPhone 17', sw: 402, sh: 874, scr: 42, notch: 'island', nw: 126, nh: 37, nr: 20, sat: 62, sab: 34 }, + { id: 'iphone-17-pro', name: 'iPhone 17 Pro', sw: 402, sh: 874, scr: 42, notch: 'island', nw: 126, nh: 37, nr: 20, sat: 62, sab: 34 }, + { id: 'iphone-17-pro-max', name: 'iPhone 17 Pro Max', sw: 440, sh: 956, scr: 44, notch: 'island', nw: 126, nh: 37, nr: 20, sat: 62, sab: 34 }, + { id: 'iphone-air', name: 'iPhone Air', sw: 420, sh: 912, scr: 42, notch: 'island', nw: 126, nh: 37, nr: 20, sat: 62, sab: 34 }, + { id: 'iphone-se3', name: 'iPhone SE (3rd gen)', sw: 375, sh: 667, scr: 4, notch: 'none', sat: 20, sab: 0 }, + { id: 'galaxy-s24', name: 'Samsung Galaxy S24', sw: 360, sh: 780, scr: 20, notch: 'punch', ps: 12, sat: 28, sab: 24 }, ]; -export function openDeviceSimulator(_context: vscode.ExtensionContext) { +function fetchJson(url: string, redirectsLeft = 3): Promise { + return new Promise((resolve, reject) => { + const req = https.get(url, { headers: { 'User-Agent': 'HPLToolbox-Extension' } }, (res) => { + if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { + if (redirectsLeft > 0) { + fetchJson(res.headers.location, redirectsLeft - 1).then(resolve).catch(reject); + } else { + reject(new Error('Too many redirects')); + } + return; + } + if (res.statusCode !== 200) { reject(new Error(`HTTP ${res.statusCode}`)); return; } + let data = ''; + res.on('data', (chunk) => { data += chunk; }); + res.on('end', () => { try { resolve(JSON.parse(data)); } catch (e) { reject(e); } }); + }); + req.on('error', reject); + req.setTimeout(8000, () => { req.destroy(); reject(new Error('timeout')); }); + }); +} + +export function openDeviceSimulator(context: vscode.ExtensionContext) { if (store.panel) { store.panel.reveal(); return; @@ -75,6 +109,29 @@ export function openDeviceSimulator(_context: vscode.ExtensionContext) { }); panel.webview.html = getHtml(DEVICES); + // Send cached remote data immediately so dropdowns update without waiting for network + const cachedDevices = context.globalState.get(GS_DEVICES_KEY); + const cachedLanguages = context.globalState.get(GS_LANGUAGES_KEY); + if (cachedDevices) panel.webview.postMessage({ type: 'updateRemoteDevices', payload: cachedDevices }); + if (cachedLanguages) panel.webview.postMessage({ type: 'updateRemoteLanguages', payload: cachedLanguages }); + + // Background fetch — updates cache and pushes fresh data if panel is still open + const simCfg = getSimulatorConfig(); + if (simCfg.devicesUrl) { + fetchJson(resolveGDriveUrl(simCfg.devicesUrl)).then((data) => { + const payload = JSON.stringify(data); + context.globalState.update(GS_DEVICES_KEY, payload); + panel.webview.postMessage({ type: 'updateRemoteDevices', payload }); + }).catch(() => {}); + } + if (simCfg.languagesUrl) { + fetchJson(resolveGDriveUrl(simCfg.languagesUrl)).then((data) => { + const payload = JSON.stringify(data); + context.globalState.update(GS_LANGUAGES_KEY, payload); + panel.webview.postMessage({ type: 'updateRemoteLanguages', payload }); + }).catch(() => {}); + } + let lastFilePath = ''; panel.webview.onDidReceiveMessage(async (msg) => { @@ -92,15 +149,8 @@ export function openDeviceSimulator(_context: vscode.ExtensionContext) { } } else if (msg.type === 'reload') { if (lastFilePath) sendFile(panel, lastFilePath, false); - } else if (msg.type === 'exportDevices') { - const target = await vscode.window.showSaveDialog({ - defaultUri: vscode.Uri.file('hpl-device-simulator-devices.json'), - filters: { 'JSON': ['json'] }, - saveLabel: 'Export Devices', - }); - if (target) { - await fs.promises.writeFile(target.fsPath, String(msg.content || ''), 'utf8'); - } + } else if (msg.type === 'openSettings') { + vscode.commands.executeCommand('workbench.action.openSettings', 'hplToolbox.deviceSimulator'); } }); } @@ -204,27 +254,18 @@ function getHtml(devices: Device[]): string { - -
@@ -437,9 +326,9 @@ html, body { min-height: 100%; }

Preview HTML playables inside accurate mobile device frames with notch and Dynamic Island overlays.

-
+

Input

- +
Playable HTML
@@ -450,62 +339,7 @@ html, body { min-height: 100%; }
- -
- -

Playable

@@ -525,15 +359,13 @@ html, body { min-height: 100%; }
- -
+
` w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = w.Write([]byte(Page("/device-simulator", "Device Simulator", body))) @@ -1002,6 +637,120 @@ func DeviceSimulatorPreview(w http.ResponseWriter, r *http.Request) { _, _ = w.Write(data) } +var reGDriveFileIDSim = regexp.MustCompile(`/file/d/([a-zA-Z0-9_-]+)`) +var reGDriveIDParamSim = regexp.MustCompile(`[?&]id=([a-zA-Z0-9_-]+)`) + +func resolveSimGDriveURL(rawURL string) string { + if m := reGDriveFileIDSim.FindStringSubmatch(rawURL); m != nil { + return "https://drive.google.com/uc?export=download&id=" + m[1] + } + if m := reGDriveIDParamSim.FindStringSubmatch(rawURL); m != nil { + return "https://drive.google.com/uc?export=download&id=" + m[1] + } + return rawURL +} + +type simRemoteCache struct { + mu sync.Mutex + devices string + languages string + devURL string + langURL string +} + +var simCache simRemoteCache + +func fetchRemoteJSON(rawURL string) (string, error) { + url := resolveSimGDriveURL(rawURL) + client := &http.Client{Timeout: 10 * time.Second} + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return "", err + } + req.Header.Set("User-Agent", "Mozilla/5.0") + resp, err := client.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + if resp.StatusCode == http.StatusFound || resp.StatusCode == http.StatusMovedPermanently { + loc := resp.Header.Get("Location") + if loc != "" { + return fetchRemoteJSON(loc) + } + } + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("HTTP %d", resp.StatusCode) + } + body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) + if err != nil { + return "", err + } + var v any + if err := json.Unmarshal(body, &v); err != nil { + return "", fmt.Errorf("invalid JSON: %w", err) + } + return string(body), nil +} + +func DeviceSimulatorRemoteDevicesEndpoint(w http.ResponseWriter, r *http.Request) { + cfg := LoadConfig() + url := strings.TrimSpace(cfg.DeviceSimulator.DevicesURL) + if url == "" { + writeJSON(w, map[string]any{"error": "devicesUrl not configured"}) + return + } + simCache.mu.Lock() + if simCache.devURL == url && simCache.devices != "" { + cached := simCache.devices + simCache.mu.Unlock() + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(cached)) + return + } + simCache.mu.Unlock() + data, err := fetchRemoteJSON(url) + if err != nil { + writeJSON(w, map[string]any{"error": err.Error()}) + return + } + simCache.mu.Lock() + simCache.devURL = url + simCache.devices = data + simCache.mu.Unlock() + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(data)) +} + +func DeviceSimulatorRemoteLanguagesEndpoint(w http.ResponseWriter, r *http.Request) { + cfg := LoadConfig() + url := strings.TrimSpace(cfg.DeviceSimulator.LanguagesURL) + if url == "" { + writeJSON(w, map[string]any{"error": "languagesUrl not configured"}) + return + } + simCache.mu.Lock() + if simCache.langURL == url && simCache.languages != "" { + cached := simCache.languages + simCache.mu.Unlock() + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(cached)) + return + } + simCache.mu.Unlock() + data, err := fetchRemoteJSON(url) + if err != nil { + writeJSON(w, map[string]any{"error": err.Error()}) + return + } + simCache.mu.Lock() + simCache.langURL = url + simCache.languages = data + simCache.mu.Unlock() + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(data)) +} + func injectDeviceSimulatorMuteBridge(html string) string { bridge := `` lower := strings.ToLower(html) diff --git a/standalone/home.go b/standalone/home.go index 4fd0cb5..614753c 100644 --- a/standalone/home.go +++ b/standalone/home.go @@ -20,9 +20,12 @@ func HomePage(w http.ResponseWriter, r *http.Request) { } body := ` -
-

HPL Toolbox

-

Standalone build. Pick a tool to open.

+
+
+

HPL Toolbox

+

Standalone build. Pick a tool to open.

+
+
` + items.String() + ` diff --git a/standalone/layout.go b/standalone/layout.go index cf7fc26..f01cdbc 100644 --- a/standalone/layout.go +++ b/standalone/layout.go @@ -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); + }); +}); + ` } diff --git a/standalone/main.go b/standalone/main.go index 10b08c4..eb710d3 100644 --- a/standalone/main.go +++ b/standalone/main.go @@ -116,6 +116,7 @@ func buildMux() *http.ServeMux { mux.HandleFunc("GET /mobile", MobilePage) mux.HandleFunc("GET /playworks", PlayworksPage) mux.HandleFunc("GET /device-simulator", DeviceSimulatorPage) + mux.HandleFunc("GET /settings", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/", http.StatusFound) }) mux.HandleFunc("GET /changelog", ChangelogPage) mux.HandleFunc("GET /assets/qrcode.min.js", MobileQrScript) @@ -178,6 +179,11 @@ func buildMux() *http.ServeMux { mux.HandleFunc("POST /api/device-simulator/pick", DeviceSimulatorPick) mux.HandleFunc("POST /api/device-simulator/load", DeviceSimulatorLoad) mux.HandleFunc("GET /api/device-simulator/preview/", DeviceSimulatorPreview) + mux.HandleFunc("GET /api/device-simulator/remoteDevices", DeviceSimulatorRemoteDevicesEndpoint) + mux.HandleFunc("GET /api/device-simulator/remoteLanguages", DeviceSimulatorRemoteLanguagesEndpoint) + + // Settings + mux.HandleFunc("POST /api/settings", SettingsSaveEndpoint) return mux } diff --git a/standalone/platform.go b/standalone/platform.go index bad6bd4..db37c20 100644 --- a/standalone/platform.go +++ b/standalone/platform.go @@ -28,13 +28,19 @@ type SendToMobileConfig struct { Port int `json:"port"` } +type DeviceSimulatorConfig struct { + DevicesURL string `json:"devicesUrl"` + LanguagesURL string `json:"languagesUrl"` +} + type AppConfig struct { - Plec PlecConfig `json:"plec"` - Applovin ApplovinConfig `json:"applovin"` - SendToMobile SendToMobileConfig `json:"sendToMobile"` - BetaToolsEnabled bool `json:"betaToolsEnabled"` - LastPickDir string `json:"lastPickDir,omitempty"` - ProjectInitManifestURL string `json:"projectInitManifestUrl,omitempty"` + Plec PlecConfig `json:"plec"` + Applovin ApplovinConfig `json:"applovin"` + SendToMobile SendToMobileConfig `json:"sendToMobile"` + DeviceSimulator DeviceSimulatorConfig `json:"deviceSimulator"` + BetaToolsEnabled bool `json:"betaToolsEnabled"` + LastPickDir string `json:"lastPickDir,omitempty"` + ProjectInitManifestURL string `json:"projectInitManifestUrl,omitempty"` } var configMu sync.Mutex @@ -66,6 +72,10 @@ func defaultConfig() AppConfig { }, Applovin: ApplovinConfig{Cookie: "", DelayMs: 5000}, SendToMobile: SendToMobileConfig{Port: 0}, + DeviceSimulator: DeviceSimulatorConfig{ + DevicesURL: "https://drive.google.com/file/d/1w04wviCmgNqtDCO1GLyYfwEeWpjBPGJ4/view?usp=drive_link", + LanguagesURL: "https://drive.google.com/file/d/1I3ZiI8CLU2JxYZEtWCz6Vr0r8Uj2732f/view?usp=drive_link", + }, } } diff --git a/standalone/project_init.go b/standalone/project_init.go index ac2096c..744f17a 100644 --- a/standalone/project_init.go +++ b/standalone/project_init.go @@ -94,7 +94,6 @@ func getManifestURL() string { } func ProjectInitPage(w http.ResponseWriter, r *http.Request) { - manifestURL := getManifestURL() body := `

Initialize Project

@@ -107,17 +106,10 @@ func ProjectInitPage(w http.ResponseWriter, r *http.Request) {
- +
-
@@ -139,29 +131,11 @@ func ProjectInitPage(w http.ResponseWriter, r *http.Request) {