From e50b4c2133175145c08c8e86ca46d1abfe26c616 Mon Sep 17 00:00:00 2001 From: hesukastro Date: Tue, 16 Jun 2026 10:20:49 +0800 Subject: [PATCH] Added localization in Device Simulator --- README.md | 6 + ..._01_real_nougc_noseason_en_full_na_al.html | 6381 +++++++++++++++++ package.json | 2 +- src/tools/deviceSimulator.ts | 194 +- standalone/device_simulator.go | 157 +- standalone/main.go | 1 + 6 files changed, 6713 insertions(+), 28 deletions(-) create mode 100644 aiAssets/arr_mip_grhpl_easylevelprogression_01_real_nougc_noseason_en_full_na_al.html 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 }