Compare commits
4 Commits
d384c1be3a
...
75a506a505
| Author | SHA1 | Date | |
|---|---|---|---|
| 75a506a505 | |||
| 698223234d | |||
| 9e82656eeb | |||
| c7ad1e14ca |
BIN
dist/hpl-toolbox-0.1.5.exe
vendored
BIN
dist/hpl-toolbox-0.1.5.exe
vendored
Binary file not shown.
BIN
dist/hpl-toolbox-0.1.5.vsix
vendored
BIN
dist/hpl-toolbox-0.1.5.vsix
vendored
Binary file not shown.
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "hpl-toolbox",
|
"name": "hpl-toolbox",
|
||||||
"displayName": "HPL Toolbox",
|
"displayName": "HPL Toolbox",
|
||||||
"description": "Bundled tools: PLEC Upload, AppLovin Demo Upload, Base64 Asset Scanner, Daily Update. Local HTML Host.",
|
"description": "Bundled tools: PLEC Upload, AppLovin Demo Upload, Base64 Asset Scanner, MRAID Checker. Local HTML Host.",
|
||||||
"version": "0.1.5",
|
"version": "0.1.5",
|
||||||
"publisher": "hesukastro",
|
"publisher": "hesukastro",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
@@ -31,8 +31,8 @@
|
|||||||
"title": "HPL Toolbox: Open Base64 Scanner"
|
"title": "HPL Toolbox: Open Base64 Scanner"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "hplToolbox.openDailyUpdate",
|
"command": "hplToolbox.openMraidChecker",
|
||||||
"title": "HPL Toolbox: Open Daily Update"
|
"title": "HPL Toolbox: Open MRAID Checker"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "hplToolbox.openSendToMobile",
|
"command": "hplToolbox.openSendToMobile",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { LauncherViewProvider } from './launcherView';
|
|||||||
import { openPlecUpload } from './tools/plecUpload';
|
import { openPlecUpload } from './tools/plecUpload';
|
||||||
import { openApplovinUpload } from './tools/applovinUpload';
|
import { openApplovinUpload } from './tools/applovinUpload';
|
||||||
import { openBase64Scanner } from './tools/base64Scanner';
|
import { openBase64Scanner } from './tools/base64Scanner';
|
||||||
import { openDailyUpdate } from './tools/dailyUpdate';
|
import { openMraidChecker } from './tools/mraidChecker';
|
||||||
import { openSendToMobile } from './tools/sendToMobile';
|
import { openSendToMobile } from './tools/sendToMobile';
|
||||||
import { openPlayworksConverter } from './tools/playworksConverter';
|
import { openPlayworksConverter } from './tools/playworksConverter';
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ export function activate(context: vscode.ExtensionContext) {
|
|||||||
vscode.commands.registerCommand('hplToolbox.openPlecUpload', () => openPlecUpload(context)),
|
vscode.commands.registerCommand('hplToolbox.openPlecUpload', () => openPlecUpload(context)),
|
||||||
vscode.commands.registerCommand('hplToolbox.openApplovinUpload', () => openApplovinUpload(context)),
|
vscode.commands.registerCommand('hplToolbox.openApplovinUpload', () => openApplovinUpload(context)),
|
||||||
vscode.commands.registerCommand('hplToolbox.openBase64Scanner', () => openBase64Scanner(context)),
|
vscode.commands.registerCommand('hplToolbox.openBase64Scanner', () => openBase64Scanner(context)),
|
||||||
vscode.commands.registerCommand('hplToolbox.openDailyUpdate', () => openDailyUpdate(context)),
|
vscode.commands.registerCommand('hplToolbox.openMraidChecker', () => openMraidChecker(context)),
|
||||||
vscode.commands.registerCommand('hplToolbox.openSendToMobile', () => openSendToMobile(context)),
|
vscode.commands.registerCommand('hplToolbox.openSendToMobile', () => openSendToMobile(context)),
|
||||||
vscode.commands.registerCommand('hplToolbox.openPlayworksConverter', () => openPlayworksConverter(context)),
|
vscode.commands.registerCommand('hplToolbox.openPlayworksConverter', () => openPlayworksConverter(context)),
|
||||||
vscode.window.registerWebviewViewProvider('hplToolbox.launcher', new LauncherViewProvider(context))
|
vscode.window.registerWebviewViewProvider('hplToolbox.launcher', new LauncherViewProvider(context))
|
||||||
|
|||||||
@@ -1,28 +1,87 @@
|
|||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
|
|
||||||
|
const BETA_TOOLS_ENABLED_KEY = 'hplToolbox.betaTools.enabled';
|
||||||
|
|
||||||
|
interface ToolDefinition {
|
||||||
|
command: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
beta?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TOOLS: ToolDefinition[] = [
|
||||||
|
{
|
||||||
|
command: 'hplToolbox.openPlecUpload',
|
||||||
|
title: 'PLEC Upload',
|
||||||
|
description: 'Upload HTML to internal PLEC server',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: 'hplToolbox.openApplovinUpload',
|
||||||
|
title: 'AppLovin Demo Upload',
|
||||||
|
description: 'Upload to p.applov.in (QR preview)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: 'hplToolbox.openBase64Scanner',
|
||||||
|
title: 'Base64 Scanner',
|
||||||
|
description: 'Find non-base64 assets in HTML',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: 'hplToolbox.openMraidChecker',
|
||||||
|
title: 'MRAID Checker',
|
||||||
|
description: 'Check MRAID requirements and best practices',
|
||||||
|
beta: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: 'hplToolbox.openSendToMobile',
|
||||||
|
title: 'Send To Mobile',
|
||||||
|
description: 'Share HTML to a phone via LAN + QR',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: 'hplToolbox.openPlayworksConverter',
|
||||||
|
title: 'Playworks Converter',
|
||||||
|
description: 'Convert Playworks HTML to per-network variants',
|
||||||
|
beta: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export class LauncherViewProvider implements vscode.WebviewViewProvider {
|
export class LauncherViewProvider implements vscode.WebviewViewProvider {
|
||||||
constructor(private readonly context: vscode.ExtensionContext) {}
|
constructor(private readonly context: vscode.ExtensionContext) { }
|
||||||
|
|
||||||
resolveWebviewView(view: vscode.WebviewView) {
|
resolveWebviewView(view: vscode.WebviewView) {
|
||||||
view.webview.options = { enableScripts: true };
|
view.webview.options = { enableScripts: true };
|
||||||
const version = this.context.extension.packageJSON.version as string;
|
const version = this.context.extension.packageJSON.version as string;
|
||||||
view.webview.html = getHtml(version);
|
const betaToolsEnabled = this.context.globalState.get<boolean>(BETA_TOOLS_ENABLED_KEY, false);
|
||||||
view.webview.onDidReceiveMessage((msg) => {
|
view.webview.html = getHtml(version, betaToolsEnabled);
|
||||||
|
view.webview.onDidReceiveMessage(async (msg) => {
|
||||||
if (msg?.type === 'open' && typeof msg.command === 'string') {
|
if (msg?.type === 'open' && typeof msg.command === 'string') {
|
||||||
vscode.commands.executeCommand(msg.command);
|
vscode.commands.executeCommand(msg.command);
|
||||||
|
} 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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHtml(version: string): string {
|
function getHtml(version: string, betaToolsEnabled: boolean): string {
|
||||||
|
const visibleTools = sortToolsForDisplay(TOOLS.filter(tool => betaToolsEnabled || !tool.beta));
|
||||||
|
const hiddenBetaCount = TOOLS.filter(tool => tool.beta).length - visibleTools.filter(tool => tool.beta).length;
|
||||||
|
const toolButtons = visibleTools.map(renderToolButton).join('\n');
|
||||||
return `<!DOCTYPE html>
|
return `<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<style>
|
<style>
|
||||||
body { font-family: var(--vscode-font-family); color: var(--vscode-foreground); padding: 12px 8px; }
|
body {
|
||||||
|
min-height: calc(100vh - 24px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-family: var(--vscode-font-family);
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
padding: 12px 8px;
|
||||||
|
}
|
||||||
h3 { margin: 0 0 12px 0; font-size: 11px; text-transform: uppercase; letter-spacing: 0.6px; opacity: 0.7; }
|
h3 { margin: 0 0 12px 0; font-size: 11px; text-transform: uppercase; letter-spacing: 0.6px; opacity: 0.7; }
|
||||||
|
.tools { flex: 1; }
|
||||||
.tool-btn {
|
.tool-btn {
|
||||||
display: block; width: 100%; text-align: left;
|
display: block; width: 100%; text-align: left;
|
||||||
padding: 8px 10px; margin-bottom: 6px;
|
padding: 8px 10px; margin-bottom: 6px;
|
||||||
@@ -34,35 +93,48 @@ function getHtml(version: string): string {
|
|||||||
.tool-btn:hover {
|
.tool-btn:hover {
|
||||||
background: var(--vscode-button-secondaryHoverBackground);
|
background: var(--vscode-button-secondaryHoverBackground);
|
||||||
}
|
}
|
||||||
|
.tool-title { display: flex; align-items: center; gap: 6px; }
|
||||||
.tool-desc { display: block; font-size: 11px; opacity: 0.7; margin-top: 2px; }
|
.tool-desc { display: block; font-size: 11px; opacity: 0.7; margin-top: 2px; }
|
||||||
|
.beta-badge {
|
||||||
|
padding: 1px 5px;
|
||||||
|
border: 1px solid var(--vscode-panel-border);
|
||||||
|
border-radius: 3px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
font-size: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.beta-toggle {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px solid var(--vscode-panel-border);
|
||||||
|
}
|
||||||
|
.beta-toggle button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 7px 10px;
|
||||||
|
background: var(--vscode-button-secondaryBackground);
|
||||||
|
color: var(--vscode-button-secondaryForeground);
|
||||||
|
border: 1px solid var(--vscode-panel-border);
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.beta-toggle button:hover {
|
||||||
|
background: var(--vscode-button-secondaryHoverBackground);
|
||||||
|
}
|
||||||
|
.beta-note { display: block; margin-top: 3px; color: var(--vscode-descriptionForeground); font-size: 11px; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h3>HPL Toolbox ${version}</h3>
|
<h3>HPL Toolbox ${version}</h3>
|
||||||
<button class="tool-btn" data-cmd="hplToolbox.openPlecUpload">
|
<div class="tools">
|
||||||
PLEC Upload
|
${toolButtons}
|
||||||
<span class="tool-desc">Upload HTML to internal PLEC server</span>
|
</div>
|
||||||
</button>
|
<div class="beta-toggle">
|
||||||
<button class="tool-btn" data-cmd="hplToolbox.openApplovinUpload">
|
<button id="betaToggle" data-enabled="${betaToolsEnabled ? 'true' : 'false'}">
|
||||||
AppLovin Demo Upload
|
${betaToolsEnabled ? 'Disable Beta Tools' : 'Enable Beta Tools'}
|
||||||
<span class="tool-desc">Upload to p.applov.in (QR preview)</span>
|
<span class="beta-note">${betaToolsEnabled ? 'Beta tools are visible in this launcher.' : `${hiddenBetaCount} beta tool(s) hidden until enabled.`}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tool-btn" data-cmd="hplToolbox.openBase64Scanner">
|
</div>
|
||||||
Base64 Scanner
|
|
||||||
<span class="tool-desc">Find non-base64 assets in HTML</span>
|
|
||||||
</button>
|
|
||||||
<button class="tool-btn" data-cmd="hplToolbox.openDailyUpdate">
|
|
||||||
Daily Update
|
|
||||||
<span class="tool-desc">Compose & copy a daily status</span>
|
|
||||||
</button>
|
|
||||||
<button class="tool-btn" data-cmd="hplToolbox.openSendToMobile">
|
|
||||||
Send To Mobile
|
|
||||||
<span class="tool-desc">Share HTML to a phone via LAN + QR</span>
|
|
||||||
</button>
|
|
||||||
<button class="tool-btn" data-cmd="hplToolbox.openPlayworksConverter">
|
|
||||||
Playworks Converter
|
|
||||||
<span class="tool-desc">Convert Playworks HTML to per-network variants</span>
|
|
||||||
</button>
|
|
||||||
<script>
|
<script>
|
||||||
const vscode = acquireVsCodeApi();
|
const vscode = acquireVsCodeApi();
|
||||||
document.querySelectorAll('.tool-btn').forEach((btn) => {
|
document.querySelectorAll('.tool-btn').forEach((btn) => {
|
||||||
@@ -70,7 +142,32 @@ function getHtml(version: string): string {
|
|||||||
vscode.postMessage({ type: 'open', command: btn.dataset.cmd });
|
vscode.postMessage({ type: 'open', command: btn.dataset.cmd });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
document.getElementById('betaToggle').addEventListener('click', (event) => {
|
||||||
|
const enabled = event.currentTarget.dataset.enabled === 'true';
|
||||||
|
vscode.postMessage({ type: 'setBetaToolsEnabled', enabled: !enabled });
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>`;
|
</html>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sortToolsForDisplay(tools: ToolDefinition[]): ToolDefinition[] {
|
||||||
|
return [...tools].sort((a, b) => Number(Boolean(a.beta)) - Number(Boolean(b.beta)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderToolButton(tool: ToolDefinition): string {
|
||||||
|
return ` <button class="tool-btn" data-cmd="${escapeHtml(tool.command)}">
|
||||||
|
<span class="tool-title">${escapeHtml(tool.title)}${tool.beta ? ' <span class="beta-badge">Beta</span>' : ''}</span>
|
||||||
|
<span class="tool-desc">${escapeHtml(tool.description)}</span>
|
||||||
|
</button>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(value: string): string {
|
||||||
|
return value.replace(/[&<>"']/g, ch => ({
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": ''',
|
||||||
|
}[ch] ?? ch));
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,122 +0,0 @@
|
|||||||
import * as vscode from 'vscode';
|
|
||||||
import { singletonPanel } from './shared';
|
|
||||||
|
|
||||||
const PROJECT_KEY = 'hplToolbox.dailyUpdate.lastProject';
|
|
||||||
const store: { panel: vscode.WebviewPanel | null } = { panel: null };
|
|
||||||
|
|
||||||
export function openDailyUpdate(context: vscode.ExtensionContext) {
|
|
||||||
const { panel, isNew } = singletonPanel(store, 'hplToolbox.dailyUpdate', 'Daily Update');
|
|
||||||
if (!isNew) return;
|
|
||||||
|
|
||||||
const lastProject = context.globalState.get<string>(PROJECT_KEY, '');
|
|
||||||
panel.webview.html = getHtml(lastProject);
|
|
||||||
|
|
||||||
panel.webview.onDidReceiveMessage(async (msg) => {
|
|
||||||
if (msg.type === 'submit') {
|
|
||||||
const text = formatMessage(msg.payload);
|
|
||||||
await vscode.env.clipboard.writeText(text);
|
|
||||||
await context.globalState.update(PROJECT_KEY, msg.payload.project);
|
|
||||||
vscode.window.showInformationMessage('Daily update copied to clipboard.');
|
|
||||||
panel.dispose();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatMessage(p: { date: string; status: string; project: string; remarks: string }): string {
|
|
||||||
const bullets = p.remarks
|
|
||||||
.split(/\r?\n/)
|
|
||||||
.map((l) => l.replace(/^\s*[-*]\s*/, '').trim())
|
|
||||||
.filter((l) => l.length > 0)
|
|
||||||
.map((l) => `- ${l}`)
|
|
||||||
.join('\n');
|
|
||||||
|
|
||||||
const lines = [
|
|
||||||
`Date: ${formatDate(p.date)}`,
|
|
||||||
`Status: ${p.status}`,
|
|
||||||
`Project: ${p.project}`,
|
|
||||||
`Remarks:`,
|
|
||||||
bullets,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (p.status === 'For OT') {
|
|
||||||
lines.push('@Joyce Lanot , @Reland Pigte, @Anthony Castor, @Angela Grace');
|
|
||||||
}
|
|
||||||
|
|
||||||
return lines.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDate(iso: string): string {
|
|
||||||
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
|
|
||||||
if (!m) return iso;
|
|
||||||
return `${m[2]}/${m[3]}/${m[1]}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeAttr(s: string): string {
|
|
||||||
return s.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
||||||
}
|
|
||||||
|
|
||||||
function getHtml(lastProject: string): string {
|
|
||||||
const today = new Date().toISOString().slice(0, 10);
|
|
||||||
return `<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline';" />
|
|
||||||
<style>
|
|
||||||
body { font-family: var(--vscode-font-family); color: var(--vscode-foreground); padding: 16px; max-width: 520px; }
|
|
||||||
label { display: block; margin-top: 12px; font-size: 12px; opacity: 0.85; }
|
|
||||||
input, select, textarea {
|
|
||||||
width: 100%; box-sizing: border-box; margin-top: 4px; padding: 6px 8px;
|
|
||||||
background: var(--vscode-input-background); color: var(--vscode-input-foreground);
|
|
||||||
border: 1px solid var(--vscode-input-border, transparent); font-family: inherit; font-size: 13px;
|
|
||||||
}
|
|
||||||
textarea { min-height: 96px; resize: vertical; }
|
|
||||||
button {
|
|
||||||
margin-top: 16px; padding: 8px 16px; cursor: pointer;
|
|
||||||
background: var(--vscode-button-background); color: var(--vscode-button-foreground);
|
|
||||||
border: none; font-size: 13px;
|
|
||||||
}
|
|
||||||
button:hover { background: var(--vscode-button-hoverBackground); }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h2 style="margin:0 0 4px 0;">Daily Update</h2>
|
|
||||||
<div style="font-size:12px;opacity:0.7;">Submit copies the formatted text to your clipboard.</div>
|
|
||||||
|
|
||||||
<label for="date">Date</label>
|
|
||||||
<input type="date" id="date" value="${today}" />
|
|
||||||
|
|
||||||
<label for="status">Status</label>
|
|
||||||
<select id="status">
|
|
||||||
<option>Started</option>
|
|
||||||
<option>Progress</option>
|
|
||||||
<option>Finished</option>
|
|
||||||
<option>Client Feedback</option>
|
|
||||||
<option>For OT</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<label for="project">Project</label>
|
|
||||||
<input type="text" id="project" placeholder="Project name" value="${escapeAttr(lastProject)}" />
|
|
||||||
|
|
||||||
<label for="remarks">Remarks</label>
|
|
||||||
<textarea id="remarks" placeholder="What did you do?"></textarea>
|
|
||||||
|
|
||||||
<button id="submit">Submit</button>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const vscode = acquireVsCodeApi();
|
|
||||||
document.getElementById('submit').addEventListener('click', () => {
|
|
||||||
vscode.postMessage({
|
|
||||||
type: 'submit',
|
|
||||||
payload: {
|
|
||||||
date: document.getElementById('date').value,
|
|
||||||
status: document.getElementById('status').value,
|
|
||||||
project: document.getElementById('project').value.trim(),
|
|
||||||
remarks: document.getElementById('remarks').value.trim(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>`;
|
|
||||||
}
|
|
||||||
496
src/tools/mraidChecker.ts
Normal file
496
src/tools/mraidChecker.ts
Normal file
@@ -0,0 +1,496 @@
|
|||||||
|
import * as vscode from 'vscode';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { singletonPanel } from './shared';
|
||||||
|
|
||||||
|
const store: { panel: vscode.WebviewPanel | null } = { panel: null };
|
||||||
|
|
||||||
|
type Severity = 'requirement' | 'best-practice';
|
||||||
|
|
||||||
|
interface MraidIssue {
|
||||||
|
id: string;
|
||||||
|
severity: Severity;
|
||||||
|
title: string;
|
||||||
|
detail: string;
|
||||||
|
line?: number;
|
||||||
|
reference: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MraidResult {
|
||||||
|
file: string;
|
||||||
|
ok: boolean;
|
||||||
|
issues: MraidIssue[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MraidRule {
|
||||||
|
id: string;
|
||||||
|
severity: Severity;
|
||||||
|
title: string;
|
||||||
|
detail: string;
|
||||||
|
reference: string;
|
||||||
|
test: (ctx: ScanContext) => MraidIssue | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScanContext {
|
||||||
|
html: string;
|
||||||
|
scriptText: string;
|
||||||
|
lowerHtml: string;
|
||||||
|
lowerScript: string;
|
||||||
|
mraidCallLines: Map<string, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openMraidChecker(_context: vscode.ExtensionContext) {
|
||||||
|
const { panel, isNew } = singletonPanel(store, 'hplToolbox.mraidChecker', 'MRAID Checker');
|
||||||
|
if (!isNew) return;
|
||||||
|
|
||||||
|
panel.webview.html = getHtml();
|
||||||
|
panel.webview.onDidReceiveMessage(async (msg) => {
|
||||||
|
if (msg.type === 'ready') {
|
||||||
|
const ws = vscode.workspace.workspaceFolders?.[0];
|
||||||
|
if (ws) {
|
||||||
|
const distPath = path.join(ws.uri.fsPath, 'dist');
|
||||||
|
if (fs.existsSync(distPath) && fs.statSync(distPath).isDirectory()) {
|
||||||
|
panel.webview.postMessage({ type: 'folderPicked', path: distPath });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (msg.type === 'pickFolder') {
|
||||||
|
const picked = await vscode.window.showOpenDialog({
|
||||||
|
canSelectFolders: true,
|
||||||
|
canSelectFiles: false,
|
||||||
|
canSelectMany: false,
|
||||||
|
openLabel: 'Select Folder',
|
||||||
|
});
|
||||||
|
if (picked && picked[0]) {
|
||||||
|
panel.webview.postMessage({ type: 'folderPicked', path: picked[0].fsPath });
|
||||||
|
}
|
||||||
|
} else if (msg.type === 'pickFiles') {
|
||||||
|
const picked = await vscode.window.showOpenDialog({
|
||||||
|
canSelectFolders: false,
|
||||||
|
canSelectFiles: true,
|
||||||
|
canSelectMany: true,
|
||||||
|
openLabel: 'Select Files',
|
||||||
|
filters: { 'HTML': ['html', 'htm'] },
|
||||||
|
});
|
||||||
|
if (picked && picked.length) {
|
||||||
|
panel.webview.postMessage({ type: 'filesPicked', paths: picked.map(u => u.fsPath) });
|
||||||
|
}
|
||||||
|
} else if (msg.type === 'scan') {
|
||||||
|
const folder: string | undefined = msg.folder;
|
||||||
|
const files: string[] | undefined = msg.files;
|
||||||
|
let targets: string[] = [];
|
||||||
|
let baseDir = '';
|
||||||
|
let skipped = 0;
|
||||||
|
if (files && files.length) {
|
||||||
|
const existing = files.filter(f => fs.existsSync(f));
|
||||||
|
targets = existing.filter(isSupportedMraidBuild);
|
||||||
|
skipped = existing.length - targets.length;
|
||||||
|
baseDir = targets.length ? commonBaseDir(targets) : '';
|
||||||
|
} else if (folder && fs.existsSync(folder)) {
|
||||||
|
const allHtmlFiles = await collectHtmlFiles(folder);
|
||||||
|
targets = allHtmlFiles.filter(isSupportedMraidBuild);
|
||||||
|
skipped = allHtmlFiles.length - targets.length;
|
||||||
|
baseDir = folder;
|
||||||
|
} else {
|
||||||
|
panel.webview.postMessage({ type: 'results', results: [], error: 'Pick a folder or files first' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targets.length === 0) {
|
||||||
|
panel.webview.postMessage({
|
||||||
|
type: 'results',
|
||||||
|
results: [],
|
||||||
|
skipped,
|
||||||
|
error: 'No AppLovin, ironSource, or Unity HTML builds were found.',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const results: MraidResult[] = [];
|
||||||
|
for (const file of targets) {
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(file, 'utf8');
|
||||||
|
const issues = checkMraid(content);
|
||||||
|
const display = baseDir ? path.relative(baseDir, file) || path.basename(file) : file;
|
||||||
|
results.push({ file: display, ok: issues.length === 0, issues });
|
||||||
|
} catch {
|
||||||
|
// Skip unreadable files, matching the scanner pattern used elsewhere.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panel.webview.postMessage({ type: 'results', results, skipped });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function commonBaseDir(files: string[]): string {
|
||||||
|
if (files.length === 0) return '';
|
||||||
|
if (files.length === 1) return path.dirname(files[0]);
|
||||||
|
const split = files.map(f => path.dirname(f).split(path.sep));
|
||||||
|
const first = split[0];
|
||||||
|
let i = 0;
|
||||||
|
while (i < first.length && split.every(parts => parts[i] === first[i])) i++;
|
||||||
|
return first.slice(0, i).join(path.sep);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function collectHtmlFiles(root: string): Promise<string[]> {
|
||||||
|
const out: string[] = [];
|
||||||
|
const stack: string[] = [root];
|
||||||
|
while (stack.length) {
|
||||||
|
const dir = stack.pop()!;
|
||||||
|
let entries: fs.Dirent[];
|
||||||
|
try {
|
||||||
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const e of entries) {
|
||||||
|
const full = path.join(dir, e.name);
|
||||||
|
if (e.isDirectory()) {
|
||||||
|
if (e.name === 'node_modules' || e.name === '.git') continue;
|
||||||
|
stack.push(full);
|
||||||
|
} else if (e.isFile() && /\.html?$/i.test(e.name)) {
|
||||||
|
out.push(full);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkMraid(html: string): MraidIssue[] {
|
||||||
|
const scriptText = extractScriptText(html);
|
||||||
|
const ctx: ScanContext = {
|
||||||
|
html,
|
||||||
|
scriptText,
|
||||||
|
lowerHtml: html.toLowerCase(),
|
||||||
|
lowerScript: scriptText.toLowerCase(),
|
||||||
|
mraidCallLines: collectMraidCallLines(html),
|
||||||
|
};
|
||||||
|
|
||||||
|
return MRAID_RULES
|
||||||
|
.map(rule => rule.test(ctx))
|
||||||
|
.filter((issue): issue is MraidIssue => issue !== null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractScriptText(html: string): string {
|
||||||
|
const scripts: string[] = [];
|
||||||
|
const scriptRegex = /<script\b[^>]*>([\s\S]*?)<\/script>/gi;
|
||||||
|
let m: RegExpExecArray | null;
|
||||||
|
while ((m = scriptRegex.exec(html)) !== null) {
|
||||||
|
scripts.push(m[1]);
|
||||||
|
}
|
||||||
|
return scripts.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectMraidCallLines(html: string): Map<string, number> {
|
||||||
|
const calls = new Map<string, number>();
|
||||||
|
const lines = html.split(/\r?\n/);
|
||||||
|
const callRegex = /\bmraid\s*\.\s*([a-zA-Z_$][\w$]*)\s*\(/g;
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
let m: RegExpExecArray | null;
|
||||||
|
while ((m = callRegex.exec(lines[i])) !== null) {
|
||||||
|
if (!calls.has(m[1])) calls.set(m[1], i + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return calls;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSupportedMraidBuild(filePath: string): boolean {
|
||||||
|
const normalized = filePath.toLowerCase().replace(/\\/g, '/');
|
||||||
|
return /(^|[\/._ -])(al|applovin|app-lovin|is|iron.?source|un|unity|unityads|unity-ads)([\/._ -]|$)/i.test(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasMraidReference(ctx: ScanContext): boolean {
|
||||||
|
return /\bmraid\b/i.test(ctx.html);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasMraidCall(ctx: ScanContext, method: string): boolean {
|
||||||
|
return ctx.mraidCallLines.has(method);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mraidLine(ctx: ScanContext, method: string): number | undefined {
|
||||||
|
return ctx.mraidCallLines.get(method);
|
||||||
|
}
|
||||||
|
|
||||||
|
function issue(rule: MraidRule, line?: number): MraidIssue {
|
||||||
|
return {
|
||||||
|
id: rule.id,
|
||||||
|
severity: rule.severity,
|
||||||
|
title: rule.title,
|
||||||
|
detail: rule.detail,
|
||||||
|
line,
|
||||||
|
reference: rule.reference,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasReadyGate(ctx: ScanContext): boolean {
|
||||||
|
return (
|
||||||
|
/mraid\s*\.\s*addEventListener\s*\(\s*['"]ready['"]/i.test(ctx.scriptText) ||
|
||||||
|
/mraid\s*\.\s*getState\s*\(\s*\)\s*={0,2}\s*['"]loading['"]/i.test(ctx.scriptText) ||
|
||||||
|
/['"]loading['"]\s*={0,2}\s*mraid\s*\.\s*getState\s*\(\s*\)/i.test(ctx.scriptText)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasEventListener(ctx: ScanContext, eventName: string): boolean {
|
||||||
|
const pattern = new RegExp(`mraid\\s*\\.\\s*addEventListener\\s*\\(\\s*['"]${eventName}['"]`, 'i');
|
||||||
|
return pattern.test(ctx.scriptText);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasSupportCheck(ctx: ScanContext, feature: string): boolean {
|
||||||
|
const pattern = new RegExp(`mraid\\s*\\.\\s*supports\\s*\\(\\s*['"]${feature}['"]`, 'i');
|
||||||
|
return pattern.test(ctx.scriptText);
|
||||||
|
}
|
||||||
|
|
||||||
|
function usesDirectBrowserNavigation(ctx: ScanContext): boolean {
|
||||||
|
return (
|
||||||
|
/\bwindow\s*\.\s*open\s*\(/i.test(ctx.scriptText) ||
|
||||||
|
/\blocation\s*\.\s*(href|assign|replace)\b/i.test(ctx.scriptText)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const MRAID_REFERENCE = 'MRAID 3.0 specification and MRAID 3.0 Best Practices Guide in mraidDocuments/';
|
||||||
|
|
||||||
|
const MRAID_RULES: MraidRule[] = [
|
||||||
|
{
|
||||||
|
id: 'mraid-reference',
|
||||||
|
severity: 'requirement',
|
||||||
|
title: 'MRAID API reference not found',
|
||||||
|
detail: 'Playable HTML should use the injected mraid object when it depends on an MRAID container.',
|
||||||
|
reference: MRAID_REFERENCE,
|
||||||
|
test: (ctx) => hasMraidReference(ctx) ? null : issue(MRAID_RULES_BY_ID['mraid-reference']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ready-gate',
|
||||||
|
severity: 'requirement',
|
||||||
|
title: 'MRAID ready state is not guarded',
|
||||||
|
detail: 'Gate ad startup behind mraid.ready, or call startup immediately only when mraid.getState() is not loading.',
|
||||||
|
reference: MRAID_REFERENCE,
|
||||||
|
test: (ctx) => !hasMraidReference(ctx) || hasReadyGate(ctx) ? null : issue(MRAID_RULES_BY_ID['ready-gate']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'error-listener',
|
||||||
|
severity: 'best-practice',
|
||||||
|
title: 'No MRAID error listener found',
|
||||||
|
detail: 'Listen for mraid error events so unsupported or failed MRAID calls can be logged or handled gracefully.',
|
||||||
|
reference: MRAID_REFERENCE,
|
||||||
|
test: (ctx) => !hasMraidReference(ctx) || hasEventListener(ctx, 'error') ? null : issue(MRAID_RULES_BY_ID['error-listener']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'viewability',
|
||||||
|
severity: 'best-practice',
|
||||||
|
title: 'Viewability handling not found',
|
||||||
|
detail: 'Use mraid.isViewable() and/or viewableChange before starting animation, audio, video, or timers.',
|
||||||
|
reference: MRAID_REFERENCE,
|
||||||
|
test: (ctx) => {
|
||||||
|
const hasVisibilitySignal = hasMraidCall(ctx, 'isViewable') || hasEventListener(ctx, 'viewableChange') || hasEventListener(ctx, 'exposureChange');
|
||||||
|
return !hasMraidReference(ctx) || hasVisibilitySignal ? null : issue(MRAID_RULES_BY_ID['viewability']);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'open-api',
|
||||||
|
severity: 'requirement',
|
||||||
|
title: 'Browser navigation used instead of mraid.open',
|
||||||
|
detail: 'Use mraid.open(url) for click-through navigation inside an MRAID ad container.',
|
||||||
|
reference: MRAID_REFERENCE,
|
||||||
|
test: (ctx) => usesDirectBrowserNavigation(ctx) && !hasMraidCall(ctx, 'open') ? issue(MRAID_RULES_BY_ID['open-api']) : null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'close-api',
|
||||||
|
severity: 'best-practice',
|
||||||
|
title: 'No mraid.close call found',
|
||||||
|
detail: 'If the creative renders its own close control, wire that control to mraid.close().',
|
||||||
|
reference: MRAID_REFERENCE,
|
||||||
|
test: (ctx) => {
|
||||||
|
const appearsToHaveCloseUi = /\b(close|dismiss|skip)\b/i.test(ctx.html);
|
||||||
|
return appearsToHaveCloseUi && !hasMraidCall(ctx, 'close') ? issue(MRAID_RULES_BY_ID['close-api']) : null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'expand-support',
|
||||||
|
severity: 'requirement',
|
||||||
|
title: 'mraid.expand used without supports check',
|
||||||
|
detail: 'Check mraid.supports("expand") before using expand behavior.',
|
||||||
|
reference: MRAID_REFERENCE,
|
||||||
|
test: (ctx) => hasMraidCall(ctx, 'expand') && !hasSupportCheck(ctx, 'expand') ? issue(MRAID_RULES_BY_ID['expand-support'], mraidLine(ctx, 'expand')) : null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'resize-support',
|
||||||
|
severity: 'requirement',
|
||||||
|
title: 'mraid.resize used without supports check',
|
||||||
|
detail: 'Check mraid.supports("resize") before using resize behavior.',
|
||||||
|
reference: MRAID_REFERENCE,
|
||||||
|
test: (ctx) => hasMraidCall(ctx, 'resize') && !hasSupportCheck(ctx, 'resize') ? issue(MRAID_RULES_BY_ID['resize-support'], mraidLine(ctx, 'resize')) : null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'orientation-support',
|
||||||
|
severity: 'best-practice',
|
||||||
|
title: 'Orientation properties set without supports check',
|
||||||
|
detail: 'Check mraid.supports("orientation") before requiring orientation behavior.',
|
||||||
|
reference: MRAID_REFERENCE,
|
||||||
|
test: (ctx) => hasMraidCall(ctx, 'setOrientationProperties') && !hasSupportCheck(ctx, 'orientation') ? issue(MRAID_RULES_BY_ID['orientation-support'], mraidLine(ctx, 'setOrientationProperties')) : null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'size-change',
|
||||||
|
severity: 'best-practice',
|
||||||
|
title: 'No sizeChange listener found',
|
||||||
|
detail: 'Listen for sizeChange when layout depends on container size, resize, expand, or orientation behavior.',
|
||||||
|
reference: MRAID_REFERENCE,
|
||||||
|
test: (ctx) => {
|
||||||
|
const dependsOnSize = hasMraidCall(ctx, 'getMaxSize') || hasMraidCall(ctx, 'getScreenSize') || hasMraidCall(ctx, 'resize') || hasMraidCall(ctx, 'expand') || hasMraidCall(ctx, 'setOrientationProperties');
|
||||||
|
return dependsOnSize && !hasEventListener(ctx, 'sizeChange') ? issue(MRAID_RULES_BY_ID['size-change']) : null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'state-change',
|
||||||
|
severity: 'best-practice',
|
||||||
|
title: 'No stateChange listener found',
|
||||||
|
detail: 'Listen for stateChange when using expand, resize, close, or other state-changing container behavior.',
|
||||||
|
reference: MRAID_REFERENCE,
|
||||||
|
test: (ctx) => {
|
||||||
|
const changesState = hasMraidCall(ctx, 'expand') || hasMraidCall(ctx, 'resize') || hasMraidCall(ctx, 'close');
|
||||||
|
return changesState && !hasEventListener(ctx, 'stateChange') ? issue(MRAID_RULES_BY_ID['state-change']) : null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const MRAID_RULES_BY_ID = MRAID_RULES.reduce<Record<string, MraidRule>>((acc, rule) => {
|
||||||
|
acc[rule.id] = rule;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
function getHtml(): string {
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<style>
|
||||||
|
body { font-family: var(--vscode-font-family); padding: 12px; color: var(--vscode-foreground); }
|
||||||
|
.row { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; }
|
||||||
|
input[type=text] { flex: 1; padding: 4px 6px; background: var(--vscode-input-background); color: var(--vscode-input-foreground); border: 1px solid var(--vscode-input-border); }
|
||||||
|
button { padding: 4px 12px; background: var(--vscode-button-background); color: var(--vscode-button-foreground); border: none; cursor: pointer; }
|
||||||
|
button:hover { background: var(--vscode-button-hoverBackground); }
|
||||||
|
.summary { margin-top: 12px; opacity: 0.85; }
|
||||||
|
.docs { color: var(--vscode-descriptionForeground); margin-bottom: 12px; }
|
||||||
|
.row-result { padding: 8px 0; border-bottom: 1px solid var(--vscode-panel-border); }
|
||||||
|
.file-head { display: flex; gap: 8px; align-items: baseline; font-family: var(--vscode-editor-font-family); font-size: 12px; }
|
||||||
|
.mark { width: 18px; flex-shrink: 0; font-weight: bold; }
|
||||||
|
.ok { color: #3fb950; }
|
||||||
|
.bad { color: #f85149; }
|
||||||
|
.file-name { word-break: break-all; }
|
||||||
|
.counts { color: var(--vscode-descriptionForeground); margin-left: auto; white-space: nowrap; }
|
||||||
|
.issues { margin: 6px 0 0 26px; padding: 0; }
|
||||||
|
.issue { margin: 6px 0; list-style: none; }
|
||||||
|
.badge { display: inline-block; min-width: 82px; margin-right: 6px; padding: 1px 5px; border-radius: 3px; font-size: 11px; text-align: center; }
|
||||||
|
.requirement { color: #ffb4ad; background: rgba(248,81,73,0.18); }
|
||||||
|
.best-practice { color: #ffd580; background: rgba(210,153,34,0.18); }
|
||||||
|
.issue-title { font-weight: 600; }
|
||||||
|
.issue-detail { color: var(--vscode-descriptionForeground); margin-top: 2px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>MRAID Checker</h2>
|
||||||
|
<div class="docs">Static checks for AppLovin, ironSource, and Unity HTML builds based on MRAID 3.0 requirements and best practices from mraidDocuments/.</div>
|
||||||
|
<div class="row">
|
||||||
|
<input id="folder" type="text" placeholder="Folder to scan recursively..." />
|
||||||
|
<button id="pick">Browse Folder...</button>
|
||||||
|
<button id="pickFiles">Pick Files...</button>
|
||||||
|
<button id="scan">Scan</button>
|
||||||
|
</div>
|
||||||
|
<div id="selection" class="summary"></div>
|
||||||
|
<div id="status"></div>
|
||||||
|
<div id="results"></div>
|
||||||
|
<script>
|
||||||
|
const vscode = acquireVsCodeApi();
|
||||||
|
vscode.postMessage({ type: 'ready' });
|
||||||
|
const folderEl = document.getElementById('folder');
|
||||||
|
const statusEl = document.getElementById('status');
|
||||||
|
const resultsEl = document.getElementById('results');
|
||||||
|
const selectionEl = document.getElementById('selection');
|
||||||
|
let pickedFiles = [];
|
||||||
|
|
||||||
|
function clearFiles() {
|
||||||
|
pickedFiles = [];
|
||||||
|
selectionEl.textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeText(value) {
|
||||||
|
return String(value).replace(/[&<>"']/g, (ch) => ({
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": '''
|
||||||
|
}[ch]));
|
||||||
|
}
|
||||||
|
|
||||||
|
folderEl.addEventListener('input', clearFiles);
|
||||||
|
|
||||||
|
document.getElementById('pick').addEventListener('click', () => {
|
||||||
|
vscode.postMessage({ type: 'pickFolder' });
|
||||||
|
});
|
||||||
|
document.getElementById('pickFiles').addEventListener('click', () => {
|
||||||
|
vscode.postMessage({ type: 'pickFiles' });
|
||||||
|
});
|
||||||
|
document.getElementById('scan').addEventListener('click', () => {
|
||||||
|
statusEl.textContent = 'Scanning...';
|
||||||
|
resultsEl.innerHTML = '';
|
||||||
|
if (pickedFiles.length) {
|
||||||
|
vscode.postMessage({ type: 'scan', files: pickedFiles });
|
||||||
|
} else {
|
||||||
|
const folder = folderEl.value.trim();
|
||||||
|
if (!folder) { statusEl.textContent = 'Pick a folder or files first.'; return; }
|
||||||
|
vscode.postMessage({ type: 'scan', folder });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('message', (event) => {
|
||||||
|
const msg = event.data;
|
||||||
|
if (msg.type === 'folderPicked') {
|
||||||
|
folderEl.value = msg.path;
|
||||||
|
clearFiles();
|
||||||
|
} else if (msg.type === 'filesPicked') {
|
||||||
|
pickedFiles = msg.paths;
|
||||||
|
folderEl.value = '';
|
||||||
|
selectionEl.textContent = pickedFiles.length + ' file(s) selected';
|
||||||
|
} else if (msg.type === 'results') {
|
||||||
|
if (msg.error) { statusEl.textContent = 'Error: ' + msg.error; return; }
|
||||||
|
const total = msg.results.length;
|
||||||
|
const withIssues = msg.results.filter(r => !r.ok).length;
|
||||||
|
const requirements = msg.results.reduce((n, r) => n + r.issues.filter(i => i.severity === 'requirement').length, 0);
|
||||||
|
const bestPractices = msg.results.reduce((n, r) => n + r.issues.filter(i => i.severity === 'best-practice').length, 0);
|
||||||
|
const skipped = Number(msg.skipped || 0);
|
||||||
|
const skippedText = skipped ? ' Skipped ' + skipped + ' non-target HTML file(s).' : '';
|
||||||
|
statusEl.innerHTML = '<div class="summary">Scanned ' + total + ' AppLovin/ironSource/Unity HTML file(s). ' + withIssues + ' file(s) need review. ' + requirements + ' requirement issue(s), ' + bestPractices + ' best-practice warning(s).' + skippedText + '</div>';
|
||||||
|
resultsEl.innerHTML = '';
|
||||||
|
for (const r of msg.results) {
|
||||||
|
const requirementsForFile = r.issues.filter(i => i.severity === 'requirement').length;
|
||||||
|
const bestPracticesForFile = r.issues.filter(i => i.severity === 'best-practice').length;
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'row-result';
|
||||||
|
row.innerHTML =
|
||||||
|
'<div class="file-head">' +
|
||||||
|
'<span class="mark ' + (r.ok ? 'ok' : 'bad') + '">' + (r.ok ? 'OK' : 'X') + '</span>' +
|
||||||
|
'<span class="file-name">' + escapeText(r.file) + '</span>' +
|
||||||
|
'<span class="counts">' + requirementsForFile + ' req / ' + bestPracticesForFile + ' bp</span>' +
|
||||||
|
'</div>';
|
||||||
|
if (!r.ok) {
|
||||||
|
const list = document.createElement('ul');
|
||||||
|
list.className = 'issues';
|
||||||
|
for (const i of r.issues) {
|
||||||
|
const item = document.createElement('li');
|
||||||
|
item.className = 'issue';
|
||||||
|
const line = i.line ? ' line ' + i.line + ':' : '';
|
||||||
|
item.innerHTML =
|
||||||
|
'<span class="badge ' + i.severity + '">' + escapeText(i.severity) + '</span>' +
|
||||||
|
'<span class="issue-title">' + escapeText(line + ' ' + i.title) + '</span>' +
|
||||||
|
'<div class="issue-detail">' + escapeText(i.detail) + '</div>';
|
||||||
|
list.appendChild(item);
|
||||||
|
}
|
||||||
|
row.appendChild(list);
|
||||||
|
}
|
||||||
|
resultsEl.appendChild(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"html"
|
|
||||||
"net/http"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func DailyPage(w http.ResponseWriter, r *http.Request) {
|
|
||||||
cfg := LoadConfig()
|
|
||||||
today := time.Now().Format("2006-01-02")
|
|
||||||
lastProject := html.EscapeString(cfg.DailyUpdate.LastProject)
|
|
||||||
|
|
||||||
body := `
|
|
||||||
<h2>Daily Update</h2>
|
|
||||||
<div class="hint" style="margin-bottom:12px;">Submit copies the formatted text to your clipboard.</div>
|
|
||||||
|
|
||||||
<div style="max-width:520px;">
|
|
||||||
<label style="display:block;margin-top:8px;font-size:12px;opacity:0.85;">Date</label>
|
|
||||||
<input type="date" id="date" value="` + today + `" style="width:100%;" />
|
|
||||||
|
|
||||||
<label style="display:block;margin-top:12px;font-size:12px;opacity:0.85;">Status</label>
|
|
||||||
<select id="status" style="width:100%;">
|
|
||||||
<option>Started</option><option>Progress</option><option>Finished</option>
|
|
||||||
<option>Client Feedback</option><option>For OT</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<label style="display:block;margin-top:12px;font-size:12px;opacity:0.85;">Project</label>
|
|
||||||
<input type="text" id="project" placeholder="Project name" value="` + lastProject + `" style="width:100%;" />
|
|
||||||
|
|
||||||
<label style="display:block;margin-top:12px;font-size:12px;opacity:0.85;">Remarks</label>
|
|
||||||
<textarea id="remarks" placeholder="What did you do?" style="width:100%;min-height:96px;"></textarea>
|
|
||||||
|
|
||||||
<button id="submit" style="margin-top:16px;">Submit</button>
|
|
||||||
<div id="status-msg" style="margin-top:12px;min-height:18px;"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
document.getElementById('submit').addEventListener('click', async () => {
|
|
||||||
const payload = {
|
|
||||||
date: document.getElementById('date').value,
|
|
||||||
status: document.getElementById('status').value,
|
|
||||||
project: document.getElementById('project').value.trim(),
|
|
||||||
remarks: document.getElementById('remarks').value.trim(),
|
|
||||||
};
|
|
||||||
const statusEl = document.getElementById('status-msg');
|
|
||||||
statusEl.textContent = 'Copying...';
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/daily/submit', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
});
|
|
||||||
const j = await res.json();
|
|
||||||
if (j.ok) {
|
|
||||||
statusEl.innerHTML = '<span class="ok">Copied to clipboard.</span>';
|
|
||||||
} else {
|
|
||||||
statusEl.innerHTML = '<span class="err">' + (j.error || 'Failed') + '</span>';
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
statusEl.innerHTML = '<span class="err">' + (e.message || e) + '</span>';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
`
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
||||||
_, _ = w.Write([]byte(Page("/daily", "Daily Update", body)))
|
|
||||||
}
|
|
||||||
|
|
||||||
type dailySubmitReq struct {
|
|
||||||
Date string `json:"date"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Project string `json:"project"`
|
|
||||||
Remarks string `json:"remarks"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func DailySubmit(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var req dailySubmitReq
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
||||||
writeJSON(w, map[string]any{"ok": false, "error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
text := formatDailyMessage(req)
|
|
||||||
if err := CopyToClipboard(text); err != nil {
|
|
||||||
writeJSON(w, map[string]any{"ok": false, "error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cfg := LoadConfig()
|
|
||||||
cfg.DailyUpdate.LastProject = req.Project
|
|
||||||
_ = SaveConfig(cfg)
|
|
||||||
writeJSON(w, map[string]any{"ok": true, "text": text})
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatDailyMessage(p dailySubmitReq) string {
|
|
||||||
bullets := []string{}
|
|
||||||
for _, line := range strings.Split(p.Remarks, "\n") {
|
|
||||||
t := strings.TrimRight(line, "\r")
|
|
||||||
t = regexp.MustCompile(`^\s*[-*]\s*`).ReplaceAllString(t, "")
|
|
||||||
t = strings.TrimSpace(t)
|
|
||||||
if t != "" {
|
|
||||||
bullets = append(bullets, "- "+t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lines := []string{
|
|
||||||
"Date: " + formatDailyDate(p.Date),
|
|
||||||
"Status: " + p.Status,
|
|
||||||
"Project: " + p.Project,
|
|
||||||
"Remarks:",
|
|
||||||
strings.Join(bullets, "\n"),
|
|
||||||
}
|
|
||||||
if p.Status == "For OT" {
|
|
||||||
lines = append(lines, "@Joyce Lanot , @Reland Pigte, @Anthony Castor, @Angela Grace")
|
|
||||||
}
|
|
||||||
return strings.Join(lines, "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatDailyDate(iso string) string {
|
|
||||||
t, err := time.Parse("2006-01-02", iso)
|
|
||||||
if err != nil {
|
|
||||||
return iso
|
|
||||||
}
|
|
||||||
return t.Format("01/02/2006")
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeJSON(w http.ResponseWriter, v any) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
_ = json.NewEncoder(w).Encode(v)
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,29 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "net/http"
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
func HomePage(w http.ResponseWriter, r *http.Request) {
|
func HomePage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
cfg := LoadConfig()
|
||||||
|
var items strings.Builder
|
||||||
|
for _, n := range visibleNavItems(cfg.BetaToolsEnabled) {
|
||||||
|
if n.Path == "/" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
label := n.Label
|
||||||
|
if n.Beta {
|
||||||
|
label += ` <span style="font-size:10px;opacity:0.65;text-transform:uppercase;">Beta</span>`
|
||||||
|
}
|
||||||
|
items.WriteString(`<li><a href="` + n.Path + `" style="color:#9cdcfe;">` + label + `</a> — ` + n.Description + `</li>`)
|
||||||
|
}
|
||||||
|
|
||||||
body := `
|
body := `
|
||||||
<h2>HPL Toolbox</h2>
|
<h2>HPL Toolbox</h2>
|
||||||
<p class="hint">Standalone build. Pick a tool from the top bar.</p>
|
<p class="hint">Standalone build. Pick a tool from the top bar.</p>
|
||||||
<ul style="line-height:1.9;">
|
<ul style="line-height:1.9;">
|
||||||
<li><a href="/plec" style="color:#9cdcfe;">PLEC Upload</a> — Upload HTML to the internal PLEC server</li>
|
` + items.String() + `
|
||||||
<li><a href="/applovin" style="color:#9cdcfe;">AppLovin Demo Upload</a> — Upload to p.applov.in (QR preview)</li>
|
|
||||||
<li><a href="/base64" style="color:#9cdcfe;">Base64 Scanner</a> — Find non-base64 assets in HTML</li>
|
|
||||||
<li><a href="/daily" style="color:#9cdcfe;">Daily Update</a> — Compose & copy a daily status</li>
|
|
||||||
<li><a href="/mobile" style="color:#9cdcfe;">Send To Mobile</a> — Share HTML to a phone via LAN + QR</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
`
|
`
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
|||||||
11
standalone/json.go
Normal file
11
standalone/json.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func writeJSON(w http.ResponseWriter, v any) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(v)
|
||||||
|
}
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "strings"
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
const SharedCSS = `
|
const SharedCSS = `
|
||||||
:root { color-scheme: dark; }
|
:root { color-scheme: dark; }
|
||||||
@@ -17,8 +22,13 @@ const SharedCSS = `
|
|||||||
.topbar a { color: #9cdcfe; text-decoration: none; padding: 4px 10px; border-radius: 3px; }
|
.topbar a { color: #9cdcfe; text-decoration: none; padding: 4px 10px; border-radius: 3px; }
|
||||||
.topbar a:hover { background: #2a2d2e; }
|
.topbar a:hover { background: #2a2d2e; }
|
||||||
.topbar a.active { background: #094771; color: #fff; }
|
.topbar a.active { background: #094771; color: #fff; }
|
||||||
|
.beta-pill { margin-left: 4px; padding: 1px 4px; border: 1px solid #555; border-radius: 3px; font-size: 10px; opacity: 0.7; text-transform: uppercase; }
|
||||||
.topbar-version { margin-left: auto; font-size: 11px; opacity: 0.45; letter-spacing: 0.4px; }
|
.topbar-version { margin-left: auto; font-size: 11px; opacity: 0.45; letter-spacing: 0.4px; }
|
||||||
.content { padding: 16px; max-width: 980px; margin: 0 auto; }
|
.content { padding: 16px; max-width: 980px; margin: 0 auto; }
|
||||||
|
.beta-tools-footer { max-width: 980px; margin: 24px auto 16px auto; padding: 12px 16px 0; border-top: 1px solid #333; }
|
||||||
|
.beta-tools-footer button { width: 100%; text-align: left; background: #3a3d41; color: #ddd; }
|
||||||
|
.beta-tools-footer button:hover { background: #45494e; }
|
||||||
|
.beta-note { display: block; margin-top: 3px; opacity: 0.65; font-size: 11px; }
|
||||||
h2 { margin-top: 0; font-weight: 500; }
|
h2 { margin-top: 0; font-weight: 500; }
|
||||||
input[type=text], input[type=date], select, textarea {
|
input[type=text], input[type=date], select, textarea {
|
||||||
box-sizing: border-box; padding: 6px 8px;
|
box-sizing: border-box; padding: 6px 8px;
|
||||||
@@ -42,26 +52,79 @@ const SharedCSS = `
|
|||||||
.hint { font-size: 12px; opacity: 0.7; }
|
.hint { font-size: 12px; opacity: 0.7; }
|
||||||
`
|
`
|
||||||
|
|
||||||
type navItem struct{ Path, Label string }
|
type navItem struct {
|
||||||
|
Path string
|
||||||
|
Label string
|
||||||
|
Description string
|
||||||
|
Beta bool
|
||||||
|
}
|
||||||
|
|
||||||
var navItems = []navItem{
|
var navItems = []navItem{
|
||||||
{"/", "Home"},
|
{Path: "/", Label: "Home"},
|
||||||
{"/plec", "PLEC Upload"},
|
{Path: "/plec", Label: "PLEC Upload", Description: "Upload HTML to the internal PLEC server"},
|
||||||
{"/applovin", "AppLovin Upload"},
|
{Path: "/applovin", Label: "AppLovin Upload", Description: "Upload to p.applov.in (QR preview)"},
|
||||||
{"/base64", "Base64 Scanner"},
|
{Path: "/base64", Label: "Base64 Scanner", Description: "Find non-base64 assets in HTML"},
|
||||||
{"/daily", "Daily Update"},
|
{Path: "/mraid", Label: "MRAID Checker", Description: "Check MRAID requirements and best practices", Beta: true},
|
||||||
{"/mobile", "Send To Mobile"},
|
{Path: "/mobile", Label: "Send To Mobile", Description: "Share HTML to a phone via LAN + QR"},
|
||||||
|
{Path: "/playworks", Label: "Playworks Converter", Description: "Convert Playworks HTML to per-network variants", Beta: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
func visibleNavItems(betaToolsEnabled bool) []navItem {
|
||||||
|
out := make([]navItem, 0, len(navItems))
|
||||||
|
for _, n := range navItems {
|
||||||
|
if n.Beta && !betaToolsEnabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, n)
|
||||||
|
}
|
||||||
|
return sortNavItemsForDisplay(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortNavItemsForDisplay(items []navItem) []navItem {
|
||||||
|
out := append([]navItem(nil), items...)
|
||||||
|
for i := 0; i < len(out); i++ {
|
||||||
|
for j := i + 1; j < len(out); j++ {
|
||||||
|
if out[i].Beta && !out[j].Beta {
|
||||||
|
out[i], out[j] = out[j], out[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func betaToolCount() int {
|
||||||
|
count := 0
|
||||||
|
for _, n := range navItems {
|
||||||
|
if n.Beta {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
func Page(activePath, title, body string) string {
|
func Page(activePath, title, body string) string {
|
||||||
|
cfg := LoadConfig()
|
||||||
|
betaToolsEnabled := cfg.BetaToolsEnabled
|
||||||
var tabs strings.Builder
|
var tabs strings.Builder
|
||||||
for _, n := range navItems {
|
for _, n := range visibleNavItems(betaToolsEnabled) {
|
||||||
cls := ""
|
cls := ""
|
||||||
if n.Path == activePath {
|
if n.Path == activePath {
|
||||||
cls = " class=\"active\""
|
cls = " class=\"active\""
|
||||||
}
|
}
|
||||||
tabs.WriteString(`<a href="` + n.Path + `"` + cls + `>` + n.Label + `</a>`)
|
label := n.Label
|
||||||
|
if n.Beta {
|
||||||
|
label += `<span class="beta-pill">Beta</span>`
|
||||||
|
}
|
||||||
|
tabs.WriteString(`<a href="` + n.Path + `"` + cls + `>` + label + `</a>`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
betaButtonLabel := "Enable Beta Tools"
|
||||||
|
betaButtonNote := "Hidden beta tools: " + strconv.Itoa(betaToolCount())
|
||||||
|
if betaToolsEnabled {
|
||||||
|
betaButtonLabel = "Disable Beta Tools"
|
||||||
|
betaButtonNote = "Beta tools are visible in this standalone launcher."
|
||||||
|
}
|
||||||
|
|
||||||
return `<!DOCTYPE html>
|
return `<!DOCTYPE html>
|
||||||
<html><head>
|
<html><head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
@@ -70,5 +133,43 @@ func Page(activePath, title, body string) string {
|
|||||||
</head><body>
|
</head><body>
|
||||||
<div class="topbar">` + tabs.String() + `<span class="topbar-version">v` + AppVersion + `</span></div>
|
<div class="topbar">` + tabs.String() + `<span class="topbar-version">v` + AppVersion + `</span></div>
|
||||||
<div class="content">` + body + `</div>
|
<div class="content">` + body + `</div>
|
||||||
|
<div class="beta-tools-footer">
|
||||||
|
<button id="betaToolsToggle" data-enabled="` + boolAttr(betaToolsEnabled) + `">` + betaButtonLabel + `<span class="beta-note">` + betaButtonNote + `</span></button>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
document.getElementById('betaToolsToggle').addEventListener('click', async (event) => {
|
||||||
|
const enabled = event.currentTarget.dataset.enabled === 'true';
|
||||||
|
await fetch('/api/betaTools', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ enabled: !enabled }),
|
||||||
|
});
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body></html>`
|
</body></html>`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func boolAttr(v bool) string {
|
||||||
|
if v {
|
||||||
|
return "true"
|
||||||
|
}
|
||||||
|
return "false"
|
||||||
|
}
|
||||||
|
|
||||||
|
func BetaToolsEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
writeJSON(w, map[string]any{"ok": false, "error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cfg := LoadConfig()
|
||||||
|
cfg.BetaToolsEnabled = req.Enabled
|
||||||
|
if err := SaveConfig(cfg); err != nil {
|
||||||
|
writeJSON(w, map[string]any{"ok": false, "error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, map[string]any{"ok": true})
|
||||||
|
}
|
||||||
|
|||||||
@@ -110,17 +110,16 @@ func buildMux() *http.ServeMux {
|
|||||||
mux.HandleFunc("GET /plec", PlecPage)
|
mux.HandleFunc("GET /plec", PlecPage)
|
||||||
mux.HandleFunc("GET /applovin", ApplovinPage)
|
mux.HandleFunc("GET /applovin", ApplovinPage)
|
||||||
mux.HandleFunc("GET /base64", Base64Page)
|
mux.HandleFunc("GET /base64", Base64Page)
|
||||||
mux.HandleFunc("GET /daily", DailyPage)
|
mux.HandleFunc("GET /mraid", MraidPage)
|
||||||
mux.HandleFunc("GET /mobile", MobilePage)
|
mux.HandleFunc("GET /mobile", MobilePage)
|
||||||
|
mux.HandleFunc("GET /playworks", PlayworksPage)
|
||||||
mux.HandleFunc("GET /assets/qrcode.min.js", MobileQrScript)
|
mux.HandleFunc("GET /assets/qrcode.min.js", MobileQrScript)
|
||||||
|
|
||||||
// Shared API
|
// Shared API
|
||||||
mux.HandleFunc("POST /api/clipboard", ClipboardEndpoint)
|
mux.HandleFunc("POST /api/clipboard", ClipboardEndpoint)
|
||||||
mux.HandleFunc("POST /api/open", OpenEndpoint)
|
mux.HandleFunc("POST /api/open", OpenEndpoint)
|
||||||
mux.HandleFunc("POST /api/focus", FocusEndpoint)
|
mux.HandleFunc("POST /api/focus", FocusEndpoint)
|
||||||
|
mux.HandleFunc("POST /api/betaTools", BetaToolsEndpoint)
|
||||||
// Daily Update
|
|
||||||
mux.HandleFunc("POST /api/daily/submit", DailySubmit)
|
|
||||||
|
|
||||||
// PLEC
|
// PLEC
|
||||||
mux.HandleFunc("POST /api/plec/pick", PlecPick)
|
mux.HandleFunc("POST /api/plec/pick", PlecPick)
|
||||||
@@ -137,11 +136,21 @@ func buildMux() *http.ServeMux {
|
|||||||
mux.HandleFunc("POST /api/base64/pickFiles", Base64PickFiles)
|
mux.HandleFunc("POST /api/base64/pickFiles", Base64PickFiles)
|
||||||
mux.HandleFunc("POST /api/base64/scan", Base64Scan)
|
mux.HandleFunc("POST /api/base64/scan", Base64Scan)
|
||||||
|
|
||||||
|
// MRAID Checker
|
||||||
|
mux.HandleFunc("POST /api/mraid/pickFolder", MraidPickFolder)
|
||||||
|
mux.HandleFunc("POST /api/mraid/pickFiles", MraidPickFiles)
|
||||||
|
mux.HandleFunc("POST /api/mraid/scan", MraidScan)
|
||||||
|
|
||||||
// Send To Mobile
|
// Send To Mobile
|
||||||
mux.HandleFunc("POST /api/mobile/pick", MobilePick)
|
mux.HandleFunc("POST /api/mobile/pick", MobilePick)
|
||||||
mux.HandleFunc("POST /api/mobile/start", MobileStart)
|
mux.HandleFunc("POST /api/mobile/start", MobileStart)
|
||||||
mux.HandleFunc("POST /api/mobile/stop", MobileStop)
|
mux.HandleFunc("POST /api/mobile/stop", MobileStop)
|
||||||
|
|
||||||
|
// Playworks Converter
|
||||||
|
mux.HandleFunc("POST /api/playworks/pickSource", PlayworksPickSource)
|
||||||
|
mux.HandleFunc("POST /api/playworks/pickOutput", PlayworksPickOutput)
|
||||||
|
mux.HandleFunc("POST /api/playworks/convert", PlayworksConvert)
|
||||||
|
|
||||||
return mux
|
return mux
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,10 +165,10 @@ func FocusEndpoint(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
user32 = syscall.NewLazyDLL("user32.dll")
|
user32 = syscall.NewLazyDLL("user32.dll")
|
||||||
procShowWindow = user32.NewProc("ShowWindow")
|
procShowWindow = user32.NewProc("ShowWindow")
|
||||||
procSetForeground = user32.NewProc("SetForegroundWindow")
|
procSetForeground = user32.NewProc("SetForegroundWindow")
|
||||||
procIsIconic = user32.NewProc("IsIconic")
|
procIsIconic = user32.NewProc("IsIconic")
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
376
standalone/mraid.go
Normal file
376
standalone/mraid.go
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mraidIssue struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Severity string `json:"severity"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Detail string `json:"detail"`
|
||||||
|
Line int `json:"line,omitempty"`
|
||||||
|
Reference string `json:"reference"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type mraidResult struct {
|
||||||
|
File string `json:"file"`
|
||||||
|
OK bool `json:"ok"`
|
||||||
|
Issues []mraidIssue `json:"issues"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type mraidRule struct {
|
||||||
|
ID string
|
||||||
|
Severity string
|
||||||
|
Title string
|
||||||
|
Detail string
|
||||||
|
Reference string
|
||||||
|
Test func(mraidContext, mraidRule) *mraidIssue
|
||||||
|
}
|
||||||
|
|
||||||
|
type mraidContext struct {
|
||||||
|
HTML string
|
||||||
|
ScriptText string
|
||||||
|
MraidCallLines map[string]int
|
||||||
|
}
|
||||||
|
|
||||||
|
func MraidPage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
body := `
|
||||||
|
<h2>MRAID Checker</h2>
|
||||||
|
<p class="hint">Static checks for AppLovin, ironSource, and Unity HTML builds based on MRAID 3.0 requirements and best practices from mraidDocuments/.</p>
|
||||||
|
<div style="display:flex;gap:8px;align-items:center;margin-bottom:8px;">
|
||||||
|
<input id="folder" type="text" placeholder="Folder to scan recursively..." style="flex:1;" />
|
||||||
|
<button id="pickFolder">Browse Folder...</button>
|
||||||
|
<button id="pickFiles">Pick Files...</button>
|
||||||
|
<button id="scan">Scan</button>
|
||||||
|
</div>
|
||||||
|
<div id="selection" class="hint"></div>
|
||||||
|
<div id="status" style="margin-top:8px;"></div>
|
||||||
|
<div id="results" style="margin-top:8px;"></div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.row-result { padding:8px 0; border-bottom:1px solid #333; }
|
||||||
|
.file-head { display:flex; gap:8px; align-items:baseline; font-family:Consolas, monospace; font-size:12px; }
|
||||||
|
.mark { width:20px; flex-shrink:0; font-weight:bold; }
|
||||||
|
.mark.ok { color:#3fb950; } .mark.bad { color:#f85149; }
|
||||||
|
.file-name { word-break:break-all; }
|
||||||
|
.counts { opacity:0.7; margin-left:auto; white-space:nowrap; }
|
||||||
|
.issues { margin:6px 0 0 28px; padding:0; }
|
||||||
|
.issue { list-style:none; margin:6px 0; }
|
||||||
|
.badge { display:inline-block; min-width:82px; margin-right:6px; padding:1px 5px; border-radius:3px; font-size:11px; text-align:center; }
|
||||||
|
.requirement { color:#ffb4ad; background:rgba(248,81,73,0.18); }
|
||||||
|
.best-practice { color:#ffd580; background:rgba(210,153,34,0.18); }
|
||||||
|
.issue-title { font-weight:600; }
|
||||||
|
.issue-detail { opacity:0.72; margin-top:2px; }
|
||||||
|
.summary { opacity:0.85; margin-bottom:6px; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const folderEl = document.getElementById('folder');
|
||||||
|
const statusEl = document.getElementById('status');
|
||||||
|
const resultsEl = document.getElementById('results');
|
||||||
|
const selectionEl = document.getElementById('selection');
|
||||||
|
let pickedFiles = [];
|
||||||
|
function clearFiles(){ pickedFiles = []; selectionEl.textContent = ''; }
|
||||||
|
function escapeText(value){ return String(value).replace(/[&<>"']/g, ch => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[ch])); }
|
||||||
|
folderEl.addEventListener('input', clearFiles);
|
||||||
|
|
||||||
|
document.getElementById('pickFolder').addEventListener('click', async () => {
|
||||||
|
const r = await fetch('/api/mraid/pickFolder', { method: 'POST' });
|
||||||
|
const j = await r.json();
|
||||||
|
if (j.path) { folderEl.value = j.path; clearFiles(); }
|
||||||
|
});
|
||||||
|
document.getElementById('pickFiles').addEventListener('click', async () => {
|
||||||
|
const r = await fetch('/api/mraid/pickFiles', { method: 'POST' });
|
||||||
|
const j = await r.json();
|
||||||
|
if (j.paths && j.paths.length) {
|
||||||
|
pickedFiles = j.paths;
|
||||||
|
folderEl.value = '';
|
||||||
|
selectionEl.textContent = pickedFiles.length + ' file(s) selected';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.getElementById('scan').addEventListener('click', async () => {
|
||||||
|
statusEl.textContent = 'Scanning...';
|
||||||
|
resultsEl.innerHTML = '';
|
||||||
|
let payload;
|
||||||
|
if (pickedFiles.length) payload = { files: pickedFiles };
|
||||||
|
else {
|
||||||
|
const folder = folderEl.value.trim();
|
||||||
|
if (!folder) { statusEl.textContent = 'Pick a folder or files first.'; return; }
|
||||||
|
payload = { folder };
|
||||||
|
}
|
||||||
|
const r = await fetch('/api/mraid/scan', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload) });
|
||||||
|
const j = await r.json();
|
||||||
|
if (j.error) { statusEl.textContent = 'Error: ' + j.error; return; }
|
||||||
|
const total = j.results.length;
|
||||||
|
const withIssues = j.results.filter(r => !r.ok).length;
|
||||||
|
const requirements = j.results.reduce((n, r) => n + r.issues.filter(i => i.severity === 'requirement').length, 0);
|
||||||
|
const bestPractices = j.results.reduce((n, r) => n + r.issues.filter(i => i.severity === 'best-practice').length, 0);
|
||||||
|
const skipped = Number(j.skipped || 0);
|
||||||
|
const skippedText = skipped ? ' Skipped ' + skipped + ' non-target HTML file(s).' : '';
|
||||||
|
statusEl.innerHTML = '<div class="summary">Scanned ' + total + ' AppLovin/ironSource/Unity HTML file(s). ' + withIssues + ' file(s) need review. ' + requirements + ' requirement issue(s), ' + bestPractices + ' best-practice warning(s).' + skippedText + '</div>';
|
||||||
|
resultsEl.innerHTML = '';
|
||||||
|
for (const rr of j.results) {
|
||||||
|
const reqCount = rr.issues.filter(i => i.severity === 'requirement').length;
|
||||||
|
const bpCount = rr.issues.filter(i => i.severity === 'best-practice').length;
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'row-result';
|
||||||
|
row.innerHTML = '<div class="file-head"><span class="mark ' + (rr.ok ? 'ok' : 'bad') + '">' + (rr.ok ? 'OK' : 'X') + '</span><span class="file-name">' + escapeText(rr.file) + '</span><span class="counts">' + reqCount + ' req / ' + bpCount + ' bp</span></div>';
|
||||||
|
if (!rr.ok) {
|
||||||
|
const list = document.createElement('ul');
|
||||||
|
list.className = 'issues';
|
||||||
|
for (const i of rr.issues) {
|
||||||
|
const item = document.createElement('li');
|
||||||
|
item.className = 'issue';
|
||||||
|
const line = i.line ? ' line ' + i.line + ':' : '';
|
||||||
|
item.innerHTML = '<span class="badge ' + i.severity + '">' + escapeText(i.severity) + '</span><span class="issue-title">' + escapeText(line + ' ' + i.title) + '</span><div class="issue-detail">' + escapeText(i.detail) + '</div>';
|
||||||
|
list.appendChild(item);
|
||||||
|
}
|
||||||
|
row.appendChild(list);
|
||||||
|
}
|
||||||
|
resultsEl.appendChild(row);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
`
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
_, _ = w.Write([]byte(Page("/mraid", "MRAID Checker", body)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func MraidPickFolder(w http.ResponseWriter, r *http.Request) {
|
||||||
|
writeJSON(w, map[string]any{"path": PickFolder("Select folder to scan", "")})
|
||||||
|
}
|
||||||
|
|
||||||
|
func MraidPickFiles(w http.ResponseWriter, r *http.Request) {
|
||||||
|
paths := PickFiles("Select HTML files", []FileFilter{{Name: "HTML", Extensions: []string{"html", "htm"}}}, true, "")
|
||||||
|
writeJSON(w, map[string]any{"paths": paths})
|
||||||
|
}
|
||||||
|
|
||||||
|
func MraidScan(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req struct {
|
||||||
|
Folder string `json:"folder"`
|
||||||
|
Files []string `json:"files"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
writeJSON(w, map[string]any{"results": []mraidResult{}, "error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var targets []string
|
||||||
|
var baseDir string
|
||||||
|
skipped := 0
|
||||||
|
if len(req.Files) > 0 {
|
||||||
|
var existing []string
|
||||||
|
for _, f := range req.Files {
|
||||||
|
if fileExists(f) {
|
||||||
|
existing = append(existing, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, f := range existing {
|
||||||
|
if isSupportedMraidBuild(f) {
|
||||||
|
targets = append(targets, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
skipped = len(existing) - len(targets)
|
||||||
|
if len(targets) > 0 {
|
||||||
|
baseDir = commonBaseDir(targets)
|
||||||
|
}
|
||||||
|
} else if req.Folder != "" && fileExists(req.Folder) {
|
||||||
|
all := collectHTMLFiles(req.Folder)
|
||||||
|
for _, f := range all {
|
||||||
|
if isSupportedMraidBuild(f) {
|
||||||
|
targets = append(targets, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
skipped = len(all) - len(targets)
|
||||||
|
baseDir = req.Folder
|
||||||
|
} else {
|
||||||
|
writeJSON(w, map[string]any{"results": []mraidResult{}, "error": "Pick a folder or files first"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(targets) == 0 {
|
||||||
|
writeJSON(w, map[string]any{"results": []mraidResult{}, "skipped": skipped, "error": "No AppLovin, ironSource, or Unity HTML builds were found."})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
results := []mraidResult{}
|
||||||
|
for _, file := range targets {
|
||||||
|
data, err := os.ReadFile(file)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
issues := checkMraid(string(data))
|
||||||
|
display := file
|
||||||
|
if baseDir != "" {
|
||||||
|
if rel, err := filepath.Rel(baseDir, file); err == nil && rel != "" {
|
||||||
|
display = rel
|
||||||
|
} else {
|
||||||
|
display = filepath.Base(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results = append(results, mraidResult{File: display, OK: len(issues) == 0, Issues: issues})
|
||||||
|
}
|
||||||
|
writeJSON(w, map[string]any{"results": results, "skipped": skipped})
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
mraidScriptRx = regexp.MustCompile(`(?is)<script\b[^>]*>(.*?)</script>`)
|
||||||
|
mraidCallRx = regexp.MustCompile(`(?i)\bmraid\s*\.\s*([a-zA-Z_$][\w$]*)\s*\(`)
|
||||||
|
mraidBuildNameRx = regexp.MustCompile(`(?i)(^|[/._ -])(al|applovin|app-lovin|is|iron.?source|un|unity|unityads|unity-ads)([/._ -]|$)`)
|
||||||
|
mraidReferenceRx = regexp.MustCompile(`(?i)\bmraid\b`)
|
||||||
|
mraidWindowOpenRx = regexp.MustCompile(`(?i)\bwindow\s*\.\s*open\s*\(`)
|
||||||
|
mraidLocationNavRx = regexp.MustCompile(`(?i)\blocation\s*\.\s*(href|assign|replace)\b`)
|
||||||
|
mraidCloseUiRx = regexp.MustCompile(`(?i)\b(close|dismiss|skip)\b`)
|
||||||
|
mraidLoadingLeftRx = regexp.MustCompile(`(?i)mraid\s*\.\s*getState\s*\(\s*\)\s*={0,2}\s*['"]loading['"]`)
|
||||||
|
mraidLoadingRightRx = regexp.MustCompile(`(?i)['"]loading['"]\s*={0,2}\s*mraid\s*\.\s*getState\s*\(\s*\)`)
|
||||||
|
)
|
||||||
|
|
||||||
|
const mraidReference = "MRAID 3.0 specification and MRAID 3.0 Best Practices Guide in mraidDocuments/"
|
||||||
|
|
||||||
|
func isSupportedMraidBuild(filePath string) bool {
|
||||||
|
normalized := strings.ReplaceAll(strings.ToLower(filePath), `\`, `/`)
|
||||||
|
return mraidBuildNameRx.MatchString(normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkMraid(htmlSrc string) []mraidIssue {
|
||||||
|
ctx := mraidContext{
|
||||||
|
HTML: htmlSrc,
|
||||||
|
ScriptText: extractMraidScriptText(htmlSrc),
|
||||||
|
MraidCallLines: collectMraidCallLines(htmlSrc),
|
||||||
|
}
|
||||||
|
var out []mraidIssue
|
||||||
|
for _, rule := range mraidRules {
|
||||||
|
if found := rule.Test(ctx, rule); found != nil {
|
||||||
|
out = append(out, *found)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if out == nil {
|
||||||
|
out = []mraidIssue{}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractMraidScriptText(htmlSrc string) string {
|
||||||
|
var parts []string
|
||||||
|
for _, m := range mraidScriptRx.FindAllStringSubmatch(htmlSrc, -1) {
|
||||||
|
parts = append(parts, m[1])
|
||||||
|
}
|
||||||
|
return strings.Join(parts, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectMraidCallLines(htmlSrc string) map[string]int {
|
||||||
|
out := map[string]int{}
|
||||||
|
for idx, line := range strings.Split(htmlSrc, "\n") {
|
||||||
|
for _, m := range mraidCallRx.FindAllStringSubmatch(line, -1) {
|
||||||
|
if _, ok := out[m[1]]; !ok {
|
||||||
|
out[m[1]] = idx + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func mraidHasReference(ctx mraidContext) bool { return mraidReferenceRx.MatchString(ctx.HTML) }
|
||||||
|
func mraidHasCall(ctx mraidContext, method string) bool {
|
||||||
|
_, ok := ctx.MraidCallLines[method]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
func mraidLine(ctx mraidContext, method string) int { return ctx.MraidCallLines[method] }
|
||||||
|
|
||||||
|
func mraidHasEventListener(ctx mraidContext, eventName string) bool {
|
||||||
|
rx := regexp.MustCompile(`(?i)mraid\s*\.\s*addEventListener\s*\(\s*['"]` + regexp.QuoteMeta(eventName) + `['"]`)
|
||||||
|
return rx.MatchString(ctx.ScriptText)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mraidHasSupportCheck(ctx mraidContext, feature string) bool {
|
||||||
|
rx := regexp.MustCompile(`(?i)mraid\s*\.\s*supports\s*\(\s*['"]` + regexp.QuoteMeta(feature) + `['"]`)
|
||||||
|
return rx.MatchString(ctx.ScriptText)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mraidHasReadyGate(ctx mraidContext) bool {
|
||||||
|
return mraidHasEventListener(ctx, "ready") || mraidLoadingLeftRx.MatchString(ctx.ScriptText) || mraidLoadingRightRx.MatchString(ctx.ScriptText)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mraidIssueFrom(rule mraidRule, line int) *mraidIssue {
|
||||||
|
found := mraidIssue{ID: rule.ID, Severity: rule.Severity, Title: rule.Title, Detail: rule.Detail, Reference: rule.Reference}
|
||||||
|
if line > 0 {
|
||||||
|
found.Line = line
|
||||||
|
}
|
||||||
|
return &found
|
||||||
|
}
|
||||||
|
|
||||||
|
var mraidRules = []mraidRule{
|
||||||
|
{ID: "mraid-reference", Severity: "requirement", Title: "MRAID API reference not found", Detail: "Playable HTML should use the injected mraid object when it depends on an MRAID container.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
|
||||||
|
if mraidHasReference(ctx) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return mraidIssueFrom(rule, 0)
|
||||||
|
}},
|
||||||
|
{ID: "ready-gate", Severity: "requirement", Title: "MRAID ready state is not guarded", Detail: "Gate ad startup behind mraid.ready, or call startup immediately only when mraid.getState() is not loading.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
|
||||||
|
if !mraidHasReference(ctx) || mraidHasReadyGate(ctx) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return mraidIssueFrom(rule, 0)
|
||||||
|
}},
|
||||||
|
{ID: "error-listener", Severity: "best-practice", Title: "No MRAID error listener found", Detail: "Listen for mraid error events so unsupported or failed MRAID calls can be logged or handled gracefully.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
|
||||||
|
if !mraidHasReference(ctx) || mraidHasEventListener(ctx, "error") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return mraidIssueFrom(rule, 0)
|
||||||
|
}},
|
||||||
|
{ID: "viewability", Severity: "best-practice", Title: "Viewability handling not found", Detail: "Use mraid.isViewable() and/or viewableChange before starting animation, audio, video, or timers.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
|
||||||
|
if !mraidHasReference(ctx) || mraidHasCall(ctx, "isViewable") || mraidHasEventListener(ctx, "viewableChange") || mraidHasEventListener(ctx, "exposureChange") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return mraidIssueFrom(rule, 0)
|
||||||
|
}},
|
||||||
|
{ID: "open-api", Severity: "requirement", Title: "Browser navigation used instead of mraid.open", Detail: "Use mraid.open(url) for click-through navigation inside an MRAID ad container.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
|
||||||
|
if (mraidWindowOpenRx.MatchString(ctx.ScriptText) || mraidLocationNavRx.MatchString(ctx.ScriptText)) && !mraidHasCall(ctx, "open") {
|
||||||
|
return mraidIssueFrom(rule, 0)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
|
{ID: "close-api", Severity: "best-practice", Title: "No mraid.close call found", Detail: "If the creative renders its own close control, wire that control to mraid.close().", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
|
||||||
|
if mraidCloseUiRx.MatchString(ctx.HTML) && !mraidHasCall(ctx, "close") {
|
||||||
|
return mraidIssueFrom(rule, 0)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
|
{ID: "expand-support", Severity: "requirement", Title: "mraid.expand used without supports check", Detail: "Check mraid.supports(\"expand\") before using expand behavior.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
|
||||||
|
if mraidHasCall(ctx, "expand") && !mraidHasSupportCheck(ctx, "expand") {
|
||||||
|
return mraidIssueFrom(rule, mraidLine(ctx, "expand"))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
|
{ID: "resize-support", Severity: "requirement", Title: "mraid.resize used without supports check", Detail: "Check mraid.supports(\"resize\") before using resize behavior.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
|
||||||
|
if mraidHasCall(ctx, "resize") && !mraidHasSupportCheck(ctx, "resize") {
|
||||||
|
return mraidIssueFrom(rule, mraidLine(ctx, "resize"))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
|
{ID: "orientation-support", Severity: "best-practice", Title: "Orientation properties set without supports check", Detail: "Check mraid.supports(\"orientation\") before requiring orientation behavior.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
|
||||||
|
if mraidHasCall(ctx, "setOrientationProperties") && !mraidHasSupportCheck(ctx, "orientation") {
|
||||||
|
return mraidIssueFrom(rule, mraidLine(ctx, "setOrientationProperties"))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
|
{ID: "size-change", Severity: "best-practice", Title: "No sizeChange listener found", Detail: "Listen for sizeChange when layout depends on container size, resize, expand, or orientation behavior.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
|
||||||
|
depends := mraidHasCall(ctx, "getMaxSize") || mraidHasCall(ctx, "getScreenSize") || mraidHasCall(ctx, "resize") || mraidHasCall(ctx, "expand") || mraidHasCall(ctx, "setOrientationProperties")
|
||||||
|
if depends && !mraidHasEventListener(ctx, "sizeChange") {
|
||||||
|
return mraidIssueFrom(rule, 0)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
|
{ID: "state-change", Severity: "best-practice", Title: "No stateChange listener found", Detail: "Listen for stateChange when using expand, resize, close, or other state-changing container behavior.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
|
||||||
|
changes := mraidHasCall(ctx, "expand") || mraidHasCall(ctx, "resize") || mraidHasCall(ctx, "close")
|
||||||
|
if changes && !mraidHasEventListener(ctx, "stateChange") {
|
||||||
|
return mraidIssueFrom(rule, 0)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
|
}
|
||||||
@@ -27,16 +27,12 @@ type SendToMobileConfig struct {
|
|||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DailyUpdateConfig struct {
|
|
||||||
LastProject string `json:"lastProject"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AppConfig struct {
|
type AppConfig struct {
|
||||||
Plec PlecConfig `json:"plec"`
|
Plec PlecConfig `json:"plec"`
|
||||||
Applovin ApplovinConfig `json:"applovin"`
|
Applovin ApplovinConfig `json:"applovin"`
|
||||||
SendToMobile SendToMobileConfig `json:"sendToMobile"`
|
SendToMobile SendToMobileConfig `json:"sendToMobile"`
|
||||||
DailyUpdate DailyUpdateConfig `json:"dailyUpdate"`
|
BetaToolsEnabled bool `json:"betaToolsEnabled"`
|
||||||
LastPickDir string `json:"lastPickDir,omitempty"`
|
LastPickDir string `json:"lastPickDir,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var configMu sync.Mutex
|
var configMu sync.Mutex
|
||||||
@@ -68,7 +64,6 @@ func defaultConfig() AppConfig {
|
|||||||
},
|
},
|
||||||
Applovin: ApplovinConfig{Cookie: "", DelayMs: 5000},
|
Applovin: ApplovinConfig{Cookie: "", DelayMs: 5000},
|
||||||
SendToMobile: SendToMobileConfig{Port: 0},
|
SendToMobile: SendToMobileConfig{Port: 0},
|
||||||
DailyUpdate: DailyUpdateConfig{LastProject: ""},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
385
standalone/playworks.go
Normal file
385
standalone/playworks.go
Normal file
@@ -0,0 +1,385 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type playworksConvertOptions struct {
|
||||||
|
SourcePath string `json:"sourcePath"`
|
||||||
|
OutputDir string `json:"outputDir"`
|
||||||
|
Networks []string `json:"networks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type playworksNetworkResult struct {
|
||||||
|
Network string `json:"network"`
|
||||||
|
File string `json:"file"`
|
||||||
|
OK bool `json:"ok"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func PlayworksPage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
body := `
|
||||||
|
<h2>Playworks Converter</h2>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.section { margin-bottom:14px; }
|
||||||
|
.section-label { font-size:11px; text-transform:uppercase; letter-spacing:0.5px; opacity:0.65; margin-bottom:5px; }
|
||||||
|
.row { display:flex; gap:6px; align-items:center; }
|
||||||
|
.net-grid { display:flex; flex-direction:column; gap:4px; }
|
||||||
|
.net-row { display:flex; align-items:center; gap:8px; padding:3px 0; cursor:pointer; user-select:none; }
|
||||||
|
.net-label { min-width:100px; font-weight:500; }
|
||||||
|
.net-tag { font-size:10px; opacity:0.6; font-weight:400; }
|
||||||
|
.net-note { font-size:11px; opacity:0.6; }
|
||||||
|
.toggle-row { display:flex; gap:8px; margin-bottom:6px; }
|
||||||
|
.toggle-row button { font-size:11px; padding:2px 8px; }
|
||||||
|
.actions { display:flex; gap:10px; align-items:center; margin-top:16px; }
|
||||||
|
#status { margin-top:12px; font-size:12px; opacity:0.75; min-height:16px; }
|
||||||
|
.results { margin-top:10px; display:flex; flex-direction:column; gap:3px; }
|
||||||
|
.res-row { display:flex; align-items:center; gap:8px; font-size:12px; font-family:Consolas, monospace; }
|
||||||
|
.mark { font-weight:bold; width:14px; flex-shrink:0; }
|
||||||
|
.ok { color:#3fb950; }
|
||||||
|
.bad { color:#f85149; }
|
||||||
|
.res-net { min-width:70px; font-weight:500; }
|
||||||
|
.res-file { opacity:0.65; word-break:break-all; }
|
||||||
|
.res-err { color:#f85149; opacity:0.9; }
|
||||||
|
hr { border:none; border-top:1px solid #333; margin:14px 0; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-label">Source HTML (Playworks export)</div>
|
||||||
|
<div class="row">
|
||||||
|
<input id="src" type="text" placeholder="Path to Playworks HTML..." readonly style="flex:1;" />
|
||||||
|
<button id="pickSrc">Browse...</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-label">Output Folder</div>
|
||||||
|
<div class="row">
|
||||||
|
<input id="outDir" type="text" placeholder="Output folder..." style="flex:1;" />
|
||||||
|
<button id="pickOut">Browse...</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-label">Networks to generate</div>
|
||||||
|
<div class="toggle-row">
|
||||||
|
<button id="selectAll">All</button>
|
||||||
|
<button id="selectNone">None</button>
|
||||||
|
</div>
|
||||||
|
<div class="net-grid">
|
||||||
|
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="al" checked /><span class="net-label">Applovin <span class="net-tag">al</span></span><span class="net-note">HTML + mraid.js in head</span></label>
|
||||||
|
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="is" checked /><span class="net-label">Ironsource <span class="net-tag">is</span></span><span class="net-note">HTML + mraid.js in head</span></label>
|
||||||
|
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="un" checked /><span class="net-label">Unity <span class="net-tag">un</span></span><span class="net-note">HTML (window.top to self)</span></label>
|
||||||
|
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="fb" checked /><span class="net-label">Facebook <span class="net-tag">fb</span></span><span class="net-note">HTML</span></label>
|
||||||
|
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="gg" checked /><span class="net-label">Google Ads <span class="net-tag">gg</span></span><span class="net-note">ZIP + exitapi.js in head</span></label>
|
||||||
|
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="mo" checked /><span class="net-label">Moloco <span class="net-tag">mo</span></span><span class="net-note">HTML</span></label>
|
||||||
|
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="vu" checked /><span class="net-label">Vungle <span class="net-tag">vu</span></span><span class="net-note">ZIP + __VUNGLE__ flag</span></label>
|
||||||
|
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="mtg" checked /><span class="net-label">Mintegral <span class="net-tag">mtg</span></span><span class="net-note">ZIP + onload gameReady</span></label>
|
||||||
|
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="tt" checked /><span class="net-label">TikTok <span class="net-tag">tt</span></span><span class="net-note">HTML + __TIKTOK__ flag</span></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button id="convert" disabled>Convert</button>
|
||||||
|
<button id="openOut" class="secondary" style="display:none">Open Output Folder</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="status"></div>
|
||||||
|
<div class="results" id="results"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let outputDir = '';
|
||||||
|
const srcEl = document.getElementById('src');
|
||||||
|
const outEl = document.getElementById('outDir');
|
||||||
|
const convertBtn = document.getElementById('convert');
|
||||||
|
const openOutBtn = document.getElementById('openOut');
|
||||||
|
const statusEl = document.getElementById('status');
|
||||||
|
const resultsEl = document.getElementById('results');
|
||||||
|
|
||||||
|
function updateConvertBtn() {
|
||||||
|
const hasNet = [...document.querySelectorAll('.net-cb')].some(c => c.checked);
|
||||||
|
convertBtn.disabled = !srcEl.value || !outEl.value || !hasNet;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('pickSrc').addEventListener('click', async () => {
|
||||||
|
const r = await fetch('/api/playworks/pickSource', { method:'POST' });
|
||||||
|
const j = await r.json();
|
||||||
|
if (j.path) {
|
||||||
|
srcEl.value = j.path;
|
||||||
|
if (!outEl.value && j.dir) { outEl.value = j.dir; outputDir = j.dir; }
|
||||||
|
updateConvertBtn();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.getElementById('pickOut').addEventListener('click', async () => {
|
||||||
|
const r = await fetch('/api/playworks/pickOutput', { method:'POST' });
|
||||||
|
const j = await r.json();
|
||||||
|
if (j.path) { outEl.value = j.path; outputDir = j.path; updateConvertBtn(); }
|
||||||
|
});
|
||||||
|
document.getElementById('selectAll').addEventListener('click', () => {
|
||||||
|
document.querySelectorAll('.net-cb').forEach(c => c.checked = true);
|
||||||
|
updateConvertBtn();
|
||||||
|
});
|
||||||
|
document.getElementById('selectNone').addEventListener('click', () => {
|
||||||
|
document.querySelectorAll('.net-cb').forEach(c => c.checked = false);
|
||||||
|
updateConvertBtn();
|
||||||
|
});
|
||||||
|
document.querySelectorAll('.net-cb').forEach(c => c.addEventListener('change', updateConvertBtn));
|
||||||
|
outEl.addEventListener('input', updateConvertBtn);
|
||||||
|
openOutBtn.addEventListener('click', () => {
|
||||||
|
if (outputDir) fetch('/api/open', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ url: outputDir }) });
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('convert').addEventListener('click', async () => {
|
||||||
|
const networks = [...document.querySelectorAll('.net-cb')].filter(c => c.checked).map(c => c.dataset.tag);
|
||||||
|
statusEl.textContent = 'Converting...';
|
||||||
|
resultsEl.innerHTML = '';
|
||||||
|
openOutBtn.style.display = 'none';
|
||||||
|
convertBtn.disabled = true;
|
||||||
|
const res = await fetch('/api/playworks/convert', {
|
||||||
|
method:'POST',
|
||||||
|
headers:{'Content-Type':'application/json'},
|
||||||
|
body: JSON.stringify({ sourcePath: srcEl.value, outputDir: outEl.value, networks })
|
||||||
|
});
|
||||||
|
const msg = await res.json();
|
||||||
|
convertBtn.disabled = false;
|
||||||
|
updateConvertBtn();
|
||||||
|
if (msg.error) { statusEl.textContent = 'Error: ' + msg.error; return; }
|
||||||
|
const results = msg.results || [];
|
||||||
|
const ok = results.filter(r => r.ok).length;
|
||||||
|
statusEl.textContent = ok + ' of ' + results.length + ' network(s) converted.';
|
||||||
|
resultsEl.innerHTML = '';
|
||||||
|
for (const r of results) {
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'res-row';
|
||||||
|
const mark = document.createElement('span');
|
||||||
|
mark.className = 'mark ' + (r.ok ? 'ok' : 'bad');
|
||||||
|
mark.textContent = r.ok ? '✓' : '✗';
|
||||||
|
const net = document.createElement('span');
|
||||||
|
net.className = 'res-net';
|
||||||
|
net.textContent = r.network;
|
||||||
|
const detail = document.createElement('span');
|
||||||
|
if (r.ok) {
|
||||||
|
detail.className = 'res-file';
|
||||||
|
detail.textContent = r.file.split(/[\\\\/]/).pop();
|
||||||
|
} else {
|
||||||
|
detail.className = 'res-err';
|
||||||
|
detail.textContent = r.error || 'Unknown error';
|
||||||
|
}
|
||||||
|
row.append(mark, net, detail);
|
||||||
|
resultsEl.appendChild(row);
|
||||||
|
}
|
||||||
|
if (ok > 0) { outputDir = outEl.value; openOutBtn.style.display = ''; }
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
`
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
_, _ = w.Write([]byte(Page("/playworks", "Playworks Converter", body)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func PlayworksPickSource(w http.ResponseWriter, r *http.Request) {
|
||||||
|
picked := PickFiles("Select Playworks HTML", []FileFilter{{Name: "HTML", Extensions: []string{"html", "htm"}}}, false, "")
|
||||||
|
if len(picked) == 0 {
|
||||||
|
writeJSON(w, map[string]any{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, map[string]any{"path": picked[0], "dir": filepath.Dir(picked[0])})
|
||||||
|
}
|
||||||
|
|
||||||
|
func PlayworksPickOutput(w http.ResponseWriter, r *http.Request) {
|
||||||
|
writeJSON(w, map[string]any{"path": PickFolder("Select Output Folder", "")})
|
||||||
|
}
|
||||||
|
|
||||||
|
func PlayworksConvert(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var opts playworksConvertOptions
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&opts); err != nil {
|
||||||
|
writeJSON(w, map[string]any{"results": []playworksNetworkResult{}, "error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !fileExists(opts.SourcePath) {
|
||||||
|
writeJSON(w, map[string]any{"results": []playworksNetworkResult{}, "error": "Source file not found."})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(opts.OutputDir, 0755); err != nil {
|
||||||
|
writeJSON(w, map[string]any{"results": []playworksNetworkResult{}, "error": "Could not create output folder."})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(opts.SourcePath)
|
||||||
|
if err != nil {
|
||||||
|
writeJSON(w, map[string]any{"results": []playworksNetworkResult{}, "error": "Could not read source: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
results := []playworksNetworkResult{}
|
||||||
|
for _, network := range opts.Networks {
|
||||||
|
filePath, err := convertPlayworksNetwork(string(data), opts, network)
|
||||||
|
if err != nil {
|
||||||
|
results = append(results, playworksNetworkResult{Network: network, OK: false, Error: err.Error()})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
results = append(results, playworksNetworkResult{Network: network, File: filePath, OK: true})
|
||||||
|
}
|
||||||
|
writeJSON(w, map[string]any{"results": results})
|
||||||
|
}
|
||||||
|
|
||||||
|
var playworksSuffixRx = regexp.MustCompile(`_(?:un|al|is|fb|gg|mo|vu|mtg|tt)$`)
|
||||||
|
|
||||||
|
func playworksSourceBaseName(sourcePath string) string {
|
||||||
|
base := strings.TrimSuffix(filepath.Base(sourcePath), filepath.Ext(sourcePath))
|
||||||
|
return playworksSuffixRx.ReplaceAllString(base, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertPlayworksNetwork(htmlSrc string, opts playworksConvertOptions, network string) (string, error) {
|
||||||
|
transformed := transformPlayworksHTML(htmlSrc, network)
|
||||||
|
baseName := playworksSourceBaseName(opts.SourcePath)
|
||||||
|
htmlFileName := fmt.Sprintf("%s_%s.html", baseName, network)
|
||||||
|
|
||||||
|
if playworksNeedsZip(network) {
|
||||||
|
zipPath := filepath.Join(opts.OutputDir, fmt.Sprintf("%s_%s.zip", baseName, network))
|
||||||
|
if err := createSingleFileZip([]byte(transformed), "index.html", zipPath); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return zipPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
outPath := filepath.Join(opts.OutputDir, htmlFileName)
|
||||||
|
if err := os.WriteFile(outPath, []byte(transformed), 0644); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return outPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func playworksNeedsZip(network string) bool {
|
||||||
|
return network == "gg" || network == "vu" || network == "mtg"
|
||||||
|
}
|
||||||
|
|
||||||
|
func transformPlayworksHTML(htmlSrc, network string) string {
|
||||||
|
result := htmlSrc
|
||||||
|
headInject := buildPlayworksLifecycleScript()
|
||||||
|
if network == "al" || network == "is" {
|
||||||
|
headInject += `<script src="mraid.js"></script>`
|
||||||
|
} else if network == "gg" {
|
||||||
|
headInject += `<script src="exitapi.js"></script>`
|
||||||
|
}
|
||||||
|
result = strings.Replace(result, "</head>", headInject+"\n</head>", 1)
|
||||||
|
|
||||||
|
if network == "vu" {
|
||||||
|
result = strings.Replace(result, "<body>", `<body>
|
||||||
|
<script>window.__VUNGLE__=true;</script>`, 1)
|
||||||
|
} else if network == "mtg" {
|
||||||
|
result = strings.Replace(result, "<body>", `<body onload="gameReady()">`, 1)
|
||||||
|
} else if network == "tt" {
|
||||||
|
result = strings.Replace(result, "<body>", `<body>
|
||||||
|
<script>window.__TIKTOK__=true;</script>`, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
result = replacePlayworksCTAScript(result, network)
|
||||||
|
if network == "un" {
|
||||||
|
result = strings.ReplaceAll(result, "window.top", "window.self")
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func replacePlayworksCTAScript(htmlSrc, network string) string {
|
||||||
|
marker := "Luna.Unity.Playable.InstallFullGame=function"
|
||||||
|
markerIdx := strings.LastIndex(htmlSrc, marker)
|
||||||
|
if markerIdx == -1 {
|
||||||
|
return htmlSrc
|
||||||
|
}
|
||||||
|
scriptOpenIdx := strings.LastIndex(htmlSrc[:markerIdx], "<script>")
|
||||||
|
if scriptOpenIdx == -1 {
|
||||||
|
return htmlSrc
|
||||||
|
}
|
||||||
|
scriptCloseRel := strings.Index(htmlSrc[markerIdx:], "</script>")
|
||||||
|
if scriptCloseRel == -1 {
|
||||||
|
return htmlSrc
|
||||||
|
}
|
||||||
|
scriptCloseIdx := markerIdx + scriptCloseRel
|
||||||
|
return htmlSrc[:scriptOpenIdx] + buildPlayworksConsoleRestoreScript() + buildPlayworksCTAScript(network) + htmlSrc[scriptCloseIdx+len("</script>"):]
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildPlayworksConsoleRestoreScript() string {
|
||||||
|
return strings.Join([]string{
|
||||||
|
`<script>(function(){`,
|
||||||
|
`try{`,
|
||||||
|
`var f=document.createElement("iframe");`,
|
||||||
|
`f.style.display="none";`,
|
||||||
|
`document.documentElement.appendChild(f);`,
|
||||||
|
`var nc=f.contentWindow.console;`,
|
||||||
|
`document.documentElement.removeChild(f);`,
|
||||||
|
`var methods=["log","warn","error","info","debug","dir","table","group","groupEnd","groupCollapsed","time","timeEnd","assert","count","countReset","trace"];`,
|
||||||
|
`methods.forEach(function(m){try{if(typeof nc[m]==="function")window.console[m]=nc[m].bind(nc);}catch(e){}});`,
|
||||||
|
`}catch(e){}`,
|
||||||
|
`})();</script>`,
|
||||||
|
}, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildPlayworksLifecycleScript() string {
|
||||||
|
return strings.Join([]string{
|
||||||
|
`<script>(function(){`,
|
||||||
|
`if(typeof window.gameReady!=="function")window.gameReady=function(){};`,
|
||||||
|
`if(typeof window.gameStart!=="function")window.gameStart=function(){};`,
|
||||||
|
`if(typeof window.gameEnd!=="function")window.gameEnd=function(){};`,
|
||||||
|
`if(typeof window.gameClose!=="function")window.gameClose=function(){};`,
|
||||||
|
`var _grOnce=false,_gsOnce=false;`,
|
||||||
|
`window.addEventListener("luna:build",function(){if(_grOnce)return;_grOnce=true;try{window.gameReady();}catch(e){}});`,
|
||||||
|
`window.addEventListener("luna:start",function(){if(_gsOnce)return;_gsOnce=true;try{window.gameStart();}catch(e){}});`,
|
||||||
|
`window.addEventListener("luna:ended",function(){try{window.gameEnd();}catch(e){}});`,
|
||||||
|
`})();</script>`,
|
||||||
|
}, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildPlayworksCTAScript(network string) string {
|
||||||
|
urlSetup := `n=n||window.$environment.packageConfig.iosLink,i=i||window.$environment.packageConfig.androidLink;var o=/iphone|ipad|ipod|macintosh/i.test(window.navigator.userAgent.toLowerCase())?n:i;`
|
||||||
|
closeCall := `try{window.gameClose();}catch(e){}`
|
||||||
|
ctaLogic := closeCall + `window.open(o,"_blank");`
|
||||||
|
switch network {
|
||||||
|
case "al", "is":
|
||||||
|
ctaLogic = closeCall + `if(typeof mraid!=="undefined"&&typeof mraid.open==="function"){if(typeof mraid.getState==="function"&&mraid.getState()==="loading"){mraid.addEventListener("ready",function(){mraid.open(o)});}else{mraid.open(o);}}else{window.open(o,"_blank");}`
|
||||||
|
case "un":
|
||||||
|
ctaLogic = closeCall + `if(typeof mraid!=="undefined"&&typeof mraid.open==="function"){mraid.open(o);}else{window.open(o,"_blank");}`
|
||||||
|
case "fb":
|
||||||
|
ctaLogic = closeCall + `if(typeof FbPlayableAd!=="undefined"&&typeof FbPlayableAd.onCTAClick==="function"){FbPlayableAd.onCTAClick();return;}window.open(o,"_blank");`
|
||||||
|
case "gg":
|
||||||
|
ctaLogic = closeCall + `if(typeof ExitApi!=="undefined"&&typeof ExitApi.exit==="function"){ExitApi.exit();return;}window.open(o,"_blank");`
|
||||||
|
case "mo":
|
||||||
|
ctaLogic = closeCall + `if(typeof window.clickTag==="string"&&window.clickTag){window.open(window.clickTag,"_blank");return;}window.open(o,"_blank");`
|
||||||
|
case "vu":
|
||||||
|
ctaLogic = closeCall + `try{parent.postMessage("download","*");}catch(e){}`
|
||||||
|
case "mtg":
|
||||||
|
ctaLogic = closeCall + `if(typeof window.install==="function"){window.install();return;}window.open(o,"_blank");`
|
||||||
|
case "tt":
|
||||||
|
ctaLogic = closeCall + `if(typeof window.openAppStore==="function"){window.openAppStore();return;}window.open(o,"_blank");`
|
||||||
|
}
|
||||||
|
return `<script>window.addEventListener("luna:build",(function(){Bridge.ready((function(){Luna.Unity.Playable.InstallFullGame=function(n,i){window.pi.logCta(),` + urlSetup + ctaLogic + `}}))}));</script>`
|
||||||
|
}
|
||||||
|
|
||||||
|
func createSingleFileZip(data []byte, nameInZip, destZipPath string) error {
|
||||||
|
_ = os.Remove(destZipPath)
|
||||||
|
out, err := os.Create(destZipPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
zw := zip.NewWriter(out)
|
||||||
|
entry, err := zw.Create(nameInZip)
|
||||||
|
if err != nil {
|
||||||
|
_ = zw.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := entry.Write(data); err != nil {
|
||||||
|
_ = zw.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return zw.Close()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user