diff --git a/README.md b/README.md
index 1367c83..9dbf754 100644
--- a/README.md
+++ b/README.md
@@ -111,3 +111,9 @@ Fixed
Added
- Added automatic update checking to the VS Code extension and standalone app.
```
+
+**v0.2.3**
+```
+Added
+ - Added Localization dropdown in Device Simulator.
+```
diff --git a/aiAssets/arr_mip_grhpl_easylevelprogression_01_real_nougc_noseason_en_full_na_al.html b/aiAssets/arr_mip_grhpl_easylevelprogression_01_real_nougc_noseason_en_full_na_al.html
new file mode 100644
index 0000000..52d623d
--- /dev/null
+++ b/aiAssets/arr_mip_grhpl_easylevelprogression_01_real_nougc_noseason_en_full_na_al.html
@@ -0,0 +1,6381 @@
+
+
+
+
+
+ Arrow Puzzle
+
+
+
+
+
+
+
diff --git a/package.json b/package.json
index 1e91c05..d732331 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.3",
"publisher": "hesukastro",
"license": "UNLICENSED",
"repository": {
diff --git a/src/tools/deviceSimulator.ts b/src/tools/deviceSimulator.ts
index e60316f..bfd01e1 100644
--- a/src/tools/deviceSimulator.ts
+++ b/src/tools/deviceSimulator.ts
@@ -1,9 +1,16 @@
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
+import * as http from 'http';
import { getToolWebviewStyles } from './shared';
-const store: { panel: vscode.WebviewPanel | null } = { panel: null };
+const store: {
+ panel: vscode.WebviewPanel | null;
+ previewServer: http.Server | null;
+ previewPort: number;
+ previewContent: string;
+ previewDir: string;
+} = { panel: null, previewServer: null, previewPort: 0, previewContent: '', previewDir: '' };
interface Device {
id: string;
@@ -56,7 +63,16 @@ export function openDeviceSimulator(_context: vscode.ExtensionContext) {
);
store.panel = panel;
- panel.onDidDispose(() => { store.panel = null; });
+ panel.onDidDispose(() => {
+ store.panel = null;
+ store.previewContent = '';
+ store.previewDir = '';
+ if (store.previewServer) {
+ store.previewServer.close();
+ store.previewServer = null;
+ store.previewPort = 0;
+ }
+ });
panel.webview.html = getHtml(DEVICES);
let lastFilePath = '';
@@ -72,10 +88,10 @@ export function openDeviceSimulator(_context: vscode.ExtensionContext) {
});
if (picked?.[0]) {
lastFilePath = picked[0].fsPath;
- sendFile(panel, lastFilePath);
+ sendFile(panel, lastFilePath, true);
}
} else if (msg.type === 'reload') {
- if (lastFilePath) sendFile(panel, lastFilePath);
+ 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'),
@@ -89,15 +105,96 @@ export function openDeviceSimulator(_context: vscode.ExtensionContext) {
});
}
-function sendFile(panel: vscode.WebviewPanel, filePath: string) {
+function sendFile(panel: vscode.WebviewPanel, filePath: string, resetMute: boolean) {
try {
const content = fs.readFileSync(filePath, 'utf8');
- panel.webview.postMessage({ type: 'fileLoaded', content, name: path.basename(filePath) });
+ store.previewContent = content;
+ store.previewDir = path.dirname(filePath);
+ ensurePreviewServer(() => {
+ const sourceUrl = store.previewPort ? `http://127.0.0.1:${store.previewPort}/preview/index.html` : '';
+ panel.webview.postMessage({ type: 'fileLoaded', content, name: path.basename(filePath), sourceUrl, resetMute });
+ });
} catch {
vscode.window.showErrorMessage('Device Simulator: could not read ' + path.basename(filePath));
}
}
+function ensurePreviewServer(callback: () => void) {
+ if (store.previewServer && store.previewPort) {
+ callback();
+ return;
+ }
+ const server = http.createServer((req, res) => {
+ if (!req.url || !req.url.startsWith('/preview')) {
+ res.writeHead(404);
+ res.end('Not found');
+ return;
+ }
+ const requestUrl = new URL(req.url, 'http://127.0.0.1');
+ const assetPath = decodeURIComponent(requestUrl.pathname.replace(/^\/preview\/?/, ''));
+ if (!assetPath || assetPath === 'index.html') {
+ res.writeHead(200, {
+ 'Content-Type': 'text/html; charset=utf-8',
+ 'Cache-Control': 'no-store',
+ });
+ res.end(injectServerMuteBridge(store.previewContent));
+ return;
+ }
+ const resolved = path.resolve(store.previewDir, assetPath);
+ if (!store.previewDir || !resolved.startsWith(path.resolve(store.previewDir) + path.sep)) {
+ res.writeHead(403);
+ res.end('Forbidden');
+ return;
+ }
+ fs.readFile(resolved, (err, data) => {
+ if (err) {
+ res.writeHead(404);
+ res.end('Not found');
+ return;
+ }
+ res.writeHead(200, { 'Cache-Control': 'no-store' });
+ res.end(data);
+ });
+ });
+ server.listen(0, '127.0.0.1', () => {
+ const address = server.address();
+ store.previewServer = server;
+ store.previewPort = typeof address === 'object' && address ? address.port : 0;
+ callback();
+ });
+ server.on('error', () => {
+ store.previewServer = null;
+ store.previewPort = 0;
+ callback();
+ });
+}
+
+function injectServerMuteBridge(html: string): string {
+ const bridge =
+ '' +
+ '(function(){' +
+ 'if(window.__hplMuteBridgeInstalled)return;window.__hplMuteBridgeInstalled=true;' +
+ 'var muted=false,media=[],contexts=[],gains=[],destinations=[];' +
+ 'function rememberMedia(el){if(!el||media.indexOf(el)>=0)return;media.push(el);applyMedia(el);}' +
+ 'function applyMedia(el){try{el.muted=muted;if(muted){if(el.__hplVolume===undefined)el.__hplVolume=el.volume;el.volume=0;}else{if(el.__hplVolume!==undefined)el.volume=el.__hplVolume;}}catch(e){}}' +
+ 'function apply(){for(var i=0;i=0&&gains[idx])args[0]=gains[idx];return origConnect.apply(this,args);};patched.__hplPatched=true;window.AudioNode.prototype.connect=patched;}}catch(e){}' +
+ 'try{patchAudioContext("AudioContext");patchAudioContext("webkitAudioContext");}catch(e){}' +
+ 'try{var play=window.HTMLMediaElement&&window.HTMLMediaElement.prototype&&window.HTMLMediaElement.prototype.play;if(play&&!play.__hplPatched){var p=function(){rememberMedia(this);return play.apply(this,arguments);};p.__hplPatched=true;window.HTMLMediaElement.prototype.play=p;}}catch(e){}' +
+ 'function scan(root){try{(root||document).querySelectorAll("audio,video").forEach(rememberMedia);}catch(e){}}' +
+ 'if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",function(){scan(document);});else scan(document);' +
+ 'try{new MutationObserver(function(ms){ms.forEach(function(m){Array.prototype.forEach.call(m.addedNodes,function(n){if(n.nodeType!==1)return;if(n.matches&&n.matches("audio,video"))rememberMedia(n);scan(n);});});}).observe(document.documentElement,{childList:true,subtree:true});}catch(e){}' +
+ '})();' +
+ '';
+ if (/]*>/i.test(html)) {
+ return html.replace(/]*>/i, (match) => match + bridge);
+ }
+ return bridge + html;
+}
+
function getHtml(devices: Device[]): string {
const devicesJson = JSON.stringify(devices);
return `
@@ -187,8 +284,13 @@ html, body { min-height: 100%; }
}
.title-orient {
min-height: 28px;
- padding: 2px 10px;
- white-space: nowrap;
+}
+.orient-icon {
+ display: block;
+ transition: transform 120ms ease;
+}
+.title-orient.landscape .orient-icon {
+ transform: rotate(90deg);
}
.icon-btn {
width: 28px;
@@ -413,7 +515,8 @@ html, body { min-height: 100%; }
-
+
+
@@ -430,12 +533,27 @@ html, body { min-height: 100%; }
const vscode = acquireVsCodeApi();
const DEFAULT_DEVICES = ${devicesJson};
const DEVICES_STORAGE_KEY = 'hplDeviceSimulator.devices.v1';
+const 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' }
+];
let devices = loadDevices();
let device = devices[0] || DEFAULT_DEVICES[0];
+let language = 'en';
let landscape = false;
let currentContent = null;
+let currentSourceUrl = '';
let currentBlobUrl = null;
+let loadVersion = 0;
let cutoutColor = '#000000';
let muted = false;
const mediaVolumes = new WeakMap();
@@ -444,12 +562,18 @@ let muteDocument = null;
/* ── device selector ─────────────────────────────────────────────────── */
const sel = document.getElementById('device-select');
+const languageSelect = document.getElementById('language-select');
populateDeviceSelect(device.id);
+populateLanguageSelect();
sel.addEventListener('change', function() {
device = devices[parseInt(sel.value, 10)] || devices[0] || DEFAULT_DEVICES[0];
renderScreen();
if (currentContent) loadContent(currentContent);
});
+languageSelect.addEventListener('change', function() {
+ language = languageSelect.value || LANGUAGES[0].code;
+ if (currentContent) loadContent(currentContent);
+});
document.getElementById('add-device-btn').addEventListener('click', addDevice);
document.getElementById('remove-device-btn').addEventListener('click', removeCurrentDevice);
document.getElementById('import-devices-btn').addEventListener('click', function() {
@@ -554,6 +678,17 @@ function populateDeviceSelect(selectedId) {
renderDeviceList(getCheckedDeviceIds());
}
+function populateLanguageSelect() {
+ languageSelect.innerHTML = '';
+ LANGUAGES.forEach(function(lang) {
+ const o = document.createElement('option');
+ o.value = lang.code;
+ o.textContent = lang.name;
+ languageSelect.appendChild(o);
+ });
+ languageSelect.value = language;
+}
+
function renderDeviceList(checkedIds) {
const list = document.getElementById('device-list');
if (!list) return;
@@ -767,11 +902,18 @@ function importDevices(event) {
/* ── orientation ────────────────────────────────────────────────────── */
document.getElementById('orient-btn').addEventListener('click', function() {
landscape = !landscape;
- this.textContent = landscape ? 'Portrait' : 'Landscape';
+ renderOrientationButton();
renderScreen();
if (currentContent) loadContent(currentContent);
});
+function renderOrientationButton() {
+ const btn = document.getElementById('orient-btn');
+ btn.classList.toggle('landscape', landscape);
+ btn.title = landscape ? 'Switch to Portrait' : 'Switch to Landscape';
+ btn.setAttribute('aria-label', landscape ? 'Switch to Portrait' : 'Switch to Landscape');
+}
+
function triggerPick(btn) {
const status = document.getElementById('setup-status');
if (status) {
@@ -859,12 +1001,17 @@ function loadContent(html) {
const frame = document.getElementById('preview-frame');
if (!frame) return;
- // Revoke previous blob URL to free memory
if (currentBlobUrl) {
URL.revokeObjectURL(currentBlobUrl);
currentBlobUrl = null;
}
+ if (currentSourceUrl) {
+ frame.src = withPreviewParams(currentSourceUrl);
+ return;
+ }
+
+ // Revoke previous blob URL to free memory
// Blob URL iframe: the blob document has its own security context — VS Code
// does not inject its nonce-based CSP into it, so inline scripts run freely.
const blob = new Blob([injectMuteBridge(html)], { type: 'text/html; charset=utf-8' });
@@ -872,8 +1019,22 @@ function loadContent(html) {
frame.src = currentBlobUrl;
}
+function withPreviewParams(url) {
+ const separator = url.indexOf('?') >= 0 ? '&' : '?';
+ loadVersion += 1;
+ return url + separator + 'lang=' + encodeURIComponent(language) + '&v=' + loadVersion;
+}
+
function injectMuteBridge(html) {
- const bridge =
+ const languageBridge =
+ '' +
+ '(function(){try{' +
+ 'var url=new URL(window.location.href);' +
+ 'url.searchParams.set("lang","' + language + '");' +
+ 'history.replaceState(null,"",url.href);' +
+ '}catch(e){}})();' +
+ '';
+ const bridge = languageBridge +
'' +
'(function(){' +
'if(window.__hplMuteBridgeInstalled)return;window.__hplMuteBridgeInstalled=true;' +
@@ -909,10 +1070,10 @@ function renderMuteButton() {
function applyMute() {
const frame = document.getElementById('preview-frame');
if (!frame || !frame.contentWindow) return;
+ frame.contentWindow.postMessage({ type: 'hplDeviceSimulatorMute', muted: muted }, '*');
try {
frame.contentWindow.__hplMuted = muted;
if (typeof frame.contentWindow.__hplSetMuted === 'function') frame.contentWindow.__hplSetMuted(muted);
- frame.contentWindow.postMessage({ type: 'hplDeviceSimulatorMute', muted: muted }, '*');
const doc = frame.contentWindow.document;
doc.querySelectorAll('audio,video').forEach(syncMediaMute);
if (muteDocument !== doc) {
@@ -962,8 +1123,11 @@ window.addEventListener('message', function(event) {
if (!msg || !msg.type) return;
if (msg.type === 'fileLoaded') {
currentContent = msg.content;
- muted = false;
- renderMuteButton();
+ currentSourceUrl = msg.sourceUrl || '';
+ if (msg.resetMute) {
+ muted = false;
+ renderMuteButton();
+ }
document.getElementById('file-label').textContent = msg.name;
document.getElementById('setup-file-name').textContent = msg.name;
var setupStatus = document.getElementById('setup-status');
diff --git a/standalone/device_simulator.go b/standalone/device_simulator.go
index ce24227..ef99949 100644
--- a/standalone/device_simulator.go
+++ b/standalone/device_simulator.go
@@ -5,6 +5,7 @@ import (
"net/http"
"os"
"path/filepath"
+ "strings"
)
type deviceSimulatorDevice struct {
@@ -132,7 +133,8 @@ func DeviceSimulatorPage(w http.ResponseWriter, r *http.Request) {
-
+
+
@@ -166,7 +168,9 @@ func DeviceSimulatorPage(w http.ResponseWriter, r *http.Request) {
#dimLabel { white-space: nowrap; flex-shrink: 0; }
.title-actions { display:flex; align-items:center; gap:8px; flex-shrink:0; }
.title-select { width:180px; min-width:120px; }
- .title-orient { min-height:28px; padding:2px 10px; white-space:nowrap; }
+ .title-orient { min-height:28px; }
+ .orient-icon { display:block; transition:transform 120ms ease; }
+ .title-orient.landscape .orient-icon { transform:rotate(90deg); }
.icon-btn {
width:28px;
height:28px;
@@ -314,11 +318,26 @@ func DeviceSimulatorPage(w http.ResponseWriter, r *http.Request) {
`
+ lower := strings.ToLower(html)
+ headIndex := strings.Index(lower, "= 0 {
+ closeIndex := strings.Index(lower[headIndex:], ">")
+ if closeIndex >= 0 {
+ insertAt := headIndex + closeIndex + 1
+ return html[:insertAt] + bridge + html[insertAt:]
+ }
+ }
+ return bridge + html
+}
diff --git a/standalone/main.go b/standalone/main.go
index 75edf1e..6aa1192 100644
--- a/standalone/main.go
+++ b/standalone/main.go
@@ -170,6 +170,7 @@ func buildMux() *http.ServeMux {
// Device Simulator
mux.HandleFunc("POST /api/device-simulator/pick", DeviceSimulatorPick)
mux.HandleFunc("POST /api/device-simulator/load", DeviceSimulatorLoad)
+ mux.HandleFunc("GET /api/device-simulator/preview/", DeviceSimulatorPreview)
return mux
}