0.1.6 updates and add changelog

This commit is contained in:
2026-05-28 01:49:33 +08:00
parent 16847a78ca
commit afbf504957
14 changed files with 385 additions and 21 deletions

26
CHANGELOG.md Normal file
View File

@@ -0,0 +1,26 @@
# CHANGELOG
## v0.1.2:
- Added local hosting for html files
- Created standalone version
## v0.1.3
- Updated Applovin Demo Upload
- Changed file selection workflow
- Added "Regenerate QRs" if in case they don't render
- Added "Save QR" and "Save All QRs"
## v0.1.5
- Better file selection for PLEC Uoload
- Multi file selection
- Auto detects iteration name (it tries its best)
Standalone .exe
- Move temp files into user temp folder
## v0.1.6
- Simplify UI
- Added beta tools (needs further testing)
- Playworks Converter
- Upload UnityAds HTML from Playworks to add injections for the other networks
- MRAID Checker
- Based on Official MRAID Document, scans the HTML files if they adhere to the documentation requirements and best practices

30
README.md Normal file
View File

@@ -0,0 +1,30 @@
# HPL Toolbox
Bundled VS Code extension and standalone tools for playable ad workflows.
# CHANGELOG
## v0.1.2:
- Added local hosting for html files
- Created standalone version
## v0.1.3
- Updated Applovin Demo Upload
- Changed file selection workflow
- Added "Regenerate QRs" if in case they don't render
- Added "Save QR" and "Save All QRs"
## v0.1.5
- Better file selection for PLEC Uoload
- Multi file selection
- Auto detects iteration name (it tries its best)
Standalone .exe
- Move temp files into user temp folder
## v0.1.6
- Simplify UI
- Added beta tools (needs further testing)
- Playworks Converter
- Upload UnityAds HTML from Playworks to add injections for the other networks
- MRAID Checker
- Based on Official MRAID Document, scans the HTML files if they adhere to the documentation requirements and best practices

Binary file not shown.

Binary file not shown.

View File

@@ -123,7 +123,8 @@
]
},
"scripts": {
"compile": "tsc -p ./",
"sync-readme": "node scripts/sync-readme.js",
"compile": "npm run sync-readme && tsc -p ./",
"watch": "tsc -watch -p ./",
"vscode:prepublish": "npm run compile"
},

20
scripts/sync-readme.js Normal file
View File

@@ -0,0 +1,20 @@
const fs = require('fs');
const path = require('path');
const root = path.resolve(__dirname, '..');
const changelogPath = path.join(root, 'CHANGELOG.md');
const readmePath = path.join(root, 'README.md');
let changelog = '# CHANGELOG\n\nNo changelog entries yet.';
if (fs.existsSync(changelogPath)) {
changelog = fs.readFileSync(changelogPath, 'utf8').trim();
}
const readme = `# HPL Toolbox
Bundled VS Code extension and standalone tools for playable ad workflows.
${changelog}
`;
fs.writeFileSync(readmePath, readme.replace(/\r?\n/g, '\n'), 'utf8');

122
src/changelogView.ts Normal file
View File

@@ -0,0 +1,122 @@
import * as fs from 'fs';
import * as path from 'path';
import * as vscode from 'vscode';
export function openChangelog(context: vscode.ExtensionContext) {
const panel = vscode.window.createWebviewPanel(
'hplToolbox.changelog',
'HPL Toolbox Changelog',
vscode.ViewColumn.One,
{ enableScripts: false }
);
const changelogPath = path.join(context.extensionPath, 'CHANGELOG.md');
let markdown = '# Changelog\n\nCHANGELOG.md was not found.';
try {
markdown = fs.readFileSync(changelogPath, 'utf8');
} catch {
// Keep the fallback text readable in packaged or development installs.
}
panel.webview.html = getHtml(renderMarkdown(markdown));
}
function getHtml(content: string): string {
return `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<style>
* { box-sizing: border-box; }
body {
margin: 0;
padding: 18px;
font-family: var(--vscode-font-family);
font-size: var(--vscode-font-size);
background: var(--vscode-editor-background);
color: var(--vscode-foreground);
}
main {
max-width: 840px;
margin: 0 auto;
}
h1 {
margin: 0 0 14px;
font-size: 20px;
line-height: 1.25;
}
h2 {
margin: 18px 0 8px;
padding-top: 12px;
border-top: 1px solid var(--vscode-panel-border);
font-size: 15px;
line-height: 1.3;
}
ul {
margin: 6px 0 12px;
padding-left: 20px;
}
li {
margin: 4px 0;
line-height: 1.45;
}
p {
margin: 8px 0;
line-height: 1.5;
}
</style>
</head>
<body>
<main>${content}</main>
</body>
</html>`;
}
function renderMarkdown(markdown: string): string {
const lines = markdown.replace(/\r\n/g, '\n').split('\n');
const html: string[] = [];
let inList = false;
const closeList = () => {
if (inList) {
html.push('</ul>');
inList = false;
}
};
for (const rawLine of lines) {
const line = rawLine.trim();
if (!line) {
closeList();
continue;
}
const heading = line.match(/^(#{1,3})\s+(.+)$/);
if (heading) {
closeList();
const level = Math.min(heading[1].length, 2);
html.push(`<h${level}>${escapeHtml(heading[2])}</h${level}>`);
continue;
}
const listItem = line.match(/^-\s+(.+)$/);
if (listItem) {
if (!inList) {
html.push('<ul>');
inList = true;
}
html.push(`<li>${escapeHtml(listItem[1])}</li>`);
continue;
}
closeList();
html.push(`<p>${escapeHtml(line)}</p>`);
}
closeList();
return html.join('\n');
}
function escapeHtml(value: string): string {
return value.replace(/[&<>"']/g, ch => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
}[ch] ?? ch));
}

View File

@@ -6,6 +6,7 @@ import { openBase64Scanner } from './tools/base64Scanner';
import { openMraidChecker } from './tools/mraidChecker';
import { openSendToMobile } from './tools/sendToMobile';
import { openPlayworksConverter } from './tools/playworksConverter';
import { openChangelog } from './changelogView';
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
@@ -15,6 +16,7 @@ export function activate(context: vscode.ExtensionContext) {
vscode.commands.registerCommand('hplToolbox.openMraidChecker', () => openMraidChecker(context)),
vscode.commands.registerCommand('hplToolbox.openSendToMobile', () => openSendToMobile(context)),
vscode.commands.registerCommand('hplToolbox.openPlayworksConverter', () => openPlayworksConverter(context)),
vscode.commands.registerCommand('hplToolbox.openChangelog', () => openChangelog(context)),
vscode.window.registerWebviewViewProvider('hplToolbox.launcher', new LauncherViewProvider(context))
);
}

View File

@@ -55,6 +55,8 @@ export class LauncherViewProvider implements vscode.WebviewViewProvider {
view.webview.onDidReceiveMessage(async (msg) => {
if (msg?.type === 'open' && typeof msg.command === 'string') {
vscode.commands.executeCommand(msg.command);
} else if (msg?.type === 'openChangelog') {
vscode.commands.executeCommand('hplToolbox.openChangelog');
} else if (msg?.type === 'setBetaToolsEnabled' && typeof msg.enabled === 'boolean') {
await this.context.globalState.update(BETA_TOOLS_ENABLED_KEY, msg.enabled);
view.webview.html = getHtml(version, msg.enabled);
@@ -142,15 +144,15 @@ function getHtml(version: string, betaToolsEnabled: boolean): string {
.footer {
margin-top: auto;
padding-top: 8px;
display: flex;
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
justify-content: space-between;
gap: 8px;
color: var(--vscode-descriptionForeground);
font-size: 8px;
border-top: 1px solid var(--vscode-panel-border);
}
.beta-toggle {
.footer-action {
padding: 0;
background: transparent;
color: var(--vscode-descriptionForeground);
@@ -161,12 +163,20 @@ function getHtml(version: string, betaToolsEnabled: boolean): string {
font-size: 10px;
opacity: 0.72;
}
.beta-toggle:hover {
#betaToggle {
justify-self: start;
}
.footer-action:hover {
opacity: 1;
text-decoration: underline;
}
.footer-version {
opacity: 0.75;
text-align: center;
white-space: nowrap;
}
.changelog-link {
justify-self: end;
text-align: right;
}
</style>
@@ -176,8 +186,9 @@ function getHtml(version: string, betaToolsEnabled: boolean): string {
${toolButtons}
</div>
<div class="footer">
<button id="betaToggle" class="beta-toggle" data-enabled="${betaToolsEnabled ? 'true' : 'false'}">${betaToolsEnabled ? 'Hide beta' : 'Show beta'}</button>
<button id="betaToggle" class="footer-action" data-enabled="${betaToolsEnabled ? 'true' : 'false'}">${betaToolsEnabled ? 'Hide beta' : 'Show beta'}</button>
<span class="footer-version">${version} - JJGC 00784</span>
<button id="changelogLink" class="footer-action changelog-link">Changelog</button>
</div>
<script>
const vscode = acquireVsCodeApi();
@@ -190,6 +201,9 @@ ${toolButtons}
const enabled = event.currentTarget.dataset.enabled === 'true';
vscode.postMessage({ type: 'setBetaToolsEnabled', enabled: !enabled });
});
document.getElementById('changelogLink').addEventListener('click', () => {
vscode.postMessage({ type: 'openChangelog' });
});
</script>
</body>
</html>`;

116
standalone/changelog.go Normal file
View File

@@ -0,0 +1,116 @@
package main
import (
"html"
"net/http"
"os"
"path/filepath"
"strings"
)
func ChangelogPage(w http.ResponseWriter, r *http.Request) {
markdown := readChangelog()
body := `
<header class="tool-header">
<h2 class="tool-title">Changelog</h2>
<p class="tool-description">Loaded from CHANGELOG.md.</p>
</header>
<section class="tool-panel changelog-panel">
<div class="panel-body changelog-content">
` + renderChangelogMarkdown(markdown) + `
</div>
</section>
<style>
.changelog-panel { max-width: 840px; }
.changelog-content h1 { margin: 0 0 14px; font-size: 20px; line-height: 1.25; }
.changelog-content h2 { margin: 18px 0 8px; padding-top: 12px; border-top: 1px solid #333; font-size: 15px; line-height: 1.3; }
.changelog-content ul { margin: 6px 0 12px; padding-left: 20px; }
.changelog-content li { margin: 4px 0; line-height: 1.45; }
.changelog-content p { margin: 8px 0; line-height: 1.5; }
</style>`
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = w.Write([]byte(Page("/changelog", "Changelog", body)))
}
func readChangelog() string {
for _, start := range changelogSearchRoots() {
for _, candidate := range changelogCandidates(start) {
data, err := os.ReadFile(candidate)
if err == nil {
return string(data)
}
}
}
return "# Changelog\n\nCHANGELOG.md was not found."
}
func changelogSearchRoots() []string {
roots := []string{}
if wd, err := os.Getwd(); err == nil {
roots = append(roots, wd)
}
roots = append(roots, appDir())
return roots
}
func changelogCandidates(start string) []string {
candidates := []string{}
dir := filepath.Clean(start)
for i := 0; i < 5; i++ {
candidates = append(candidates, filepath.Join(dir, "CHANGELOG.md"))
parent := filepath.Dir(dir)
if parent == dir {
break
}
dir = parent
}
return candidates
}
func renderChangelogMarkdown(markdown string) string {
lines := strings.Split(strings.ReplaceAll(markdown, "\r\n", "\n"), "\n")
var out strings.Builder
inList := false
closeList := func() {
if inList {
out.WriteString("</ul>")
inList = false
}
}
for _, rawLine := range lines {
line := strings.TrimSpace(rawLine)
if line == "" {
closeList()
continue
}
if strings.HasPrefix(line, "### ") {
closeList()
out.WriteString("<h2>" + html.EscapeString(strings.TrimSpace(line[4:])) + "</h2>")
continue
}
if strings.HasPrefix(line, "## ") {
closeList()
out.WriteString("<h2>" + html.EscapeString(strings.TrimSpace(line[3:])) + "</h2>")
continue
}
if strings.HasPrefix(line, "# ") {
closeList()
out.WriteString("<h1>" + html.EscapeString(strings.TrimSpace(line[2:])) + "</h1>")
continue
}
if strings.HasPrefix(line, "- ") {
if !inList {
out.WriteString("<ul>")
inList = true
}
out.WriteString("<li>" + html.EscapeString(strings.TrimSpace(line[2:])) + "</li>")
continue
}
closeList()
out.WriteString("<p>" + html.EscapeString(line) + "</p>")
}
closeList()
return out.String()
}

View File

@@ -16,7 +16,7 @@ func HomePage(w http.ResponseWriter, r *http.Request) {
if n.Beta {
badge = ` <span class="beta-pill">Beta</span>`
}
items.WriteString(`<a class="home-tool" href="` + n.Path + `"><span class="home-tool-title">` + n.Label + badge + `</span><span class="home-tool-desc">` + n.Description + `</span></a>`)
items.WriteString(`<button type="button" class="home-tool" data-path="` + n.Path + `"><span class="home-tool-title">` + n.Label + badge + `</span><span class="home-tool-desc">` + n.Description + `</span></button>`)
}
body := `
@@ -31,13 +31,16 @@ func HomePage(w http.ResponseWriter, r *http.Request) {
.home-tools { display:flex; flex-direction:column; gap:8px; max-width:620px; }
.home-tool {
display:block;
width:100%;
min-height:52px;
padding:9px 11px;
color:#ddd;
text-decoration:none;
text-align:left;
background:#2a2d2e;
border:1px solid #333;
border-radius:5px;
cursor:pointer;
font:inherit;
}
.home-tool:hover { background:#33373a; border-color:#007fd4; }
.home-tool-title { display:flex; align-items:center; justify-content:space-between; gap:8px; font-size:12px; font-weight:600; }

View File

@@ -31,9 +31,17 @@ const SharedCSS = `
padding: 8px 16px; background: #252526; border-bottom: 1px solid #333;
font-size: 13px;
}
.topbar a { color: #ddd; text-decoration: none; padding: 5px 10px; border-radius: 4px; }
.topbar a:hover { background: #2a2d2e; }
.topbar a.active { background: #094771; color: #fff; }
.topbar .nav-link {
background: transparent;
color: #ddd;
border: 0;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
font: inherit;
}
.topbar .nav-link:hover { background: #2a2d2e; }
.topbar .nav-link.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-spacer { margin-left: auto; }
.content { flex: 1; width: 100%; padding: 18px; max-width: 1100px; margin: 0 auto; }
@@ -42,15 +50,16 @@ const SharedCSS = `
max-width: 1100px;
margin: auto auto 0;
padding: 8px 18px 14px;
display: flex;
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
justify-content: space-between;
gap: 8px;
color: #a7a7a7;
font-size: 10px;
border-top: 1px solid #333;
}
.beta-tools-toggle {
justify-self: start;
padding: 0;
background: transparent;
color: #a7a7a7;
@@ -58,9 +67,12 @@ const SharedCSS = `
cursor: pointer;
font: inherit;
opacity: 0.72;
text-align: left;
}
.beta-tools-toggle:hover { background: transparent; opacity: 1; text-decoration: underline; }
.app-footer-version { opacity: 0.75; text-align: right; }
.app-footer-version { opacity: 0.75; text-align: center; white-space: nowrap; }
.app-footer-link { justify-self: end; padding: 0; background: transparent; color: #a7a7a7; border: 0; cursor: pointer; font: inherit; opacity: 0.72; }
.app-footer-link:hover { background: transparent; opacity: 1; text-decoration: underline; }
h2 { margin: 0; font-size: 18px; line-height: 1.25; font-weight: 600; }
.tool-header { margin-bottom: var(--tool-gap-lg); }
.tool-title { margin: 0; font-size: 18px; line-height: 1.25; font-weight: 600; }
@@ -176,13 +188,13 @@ func Page(activePath, title, body string) string {
for _, n := range visibleNavItems(betaToolsEnabled) {
cls := ""
if n.Path == activePath {
cls = " class=\"active\""
cls = " active"
}
label := n.Label
if n.Beta {
label += `<span class="beta-pill">Beta</span>`
}
tabs.WriteString(`<a href="` + n.Path + `"` + cls + `>` + label + `</a>`)
tabs.WriteString(`<button type="button" class="nav-link` + cls + `" data-path="` + n.Path + `">` + label + `</button>`)
}
betaButtonLabel := "Show beta"
@@ -201,8 +213,15 @@ func Page(activePath, title, body string) string {
<div class="app-footer">
<button id="betaToolsToggle" class="beta-tools-toggle" data-enabled="` + boolAttr(betaToolsEnabled) + `">` + betaButtonLabel + `</button>
<span class="app-footer-version">` + AppVersion + ` - JJGC 00784</span>
<button type="button" class="app-footer-link" data-path="/changelog">Changelog</button>
</div>
<script>
document.querySelectorAll('[data-path]').forEach((element) => {
element.addEventListener('click', () => {
const path = element.dataset.path;
if (path) window.location.href = path;
});
});
document.getElementById('betaToolsToggle').addEventListener('click', async (event) => {
const enabled = event.currentTarget.dataset.enabled === 'true';
await fetch('/api/betaTools', {

View File

@@ -113,6 +113,7 @@ func buildMux() *http.ServeMux {
mux.HandleFunc("GET /mraid", MraidPage)
mux.HandleFunc("GET /mobile", MobilePage)
mux.HandleFunc("GET /playworks", PlayworksPage)
mux.HandleFunc("GET /changelog", ChangelogPage)
mux.HandleFunc("GET /assets/qrcode.min.js", MobileQrScript)
// Shared API

View File

@@ -8,6 +8,7 @@ import (
"path/filepath"
"strings"
"sync"
"syscall"
)
type PlecConfig struct {
@@ -114,12 +115,12 @@ func SaveConfig(cfg AppConfig) error {
func OpenInBrowser(url string) {
// `start` is a cmd builtin. Quoted empty arg is the window title.
cmd := exec.Command("cmd", "/c", "start", "", url)
cmd := hiddenCommand("cmd", "/c", "start", "", url)
_ = cmd.Start()
}
func CopyToClipboard(text string) error {
cmd := exec.Command("clip")
cmd := hiddenCommand("clip")
cmd.Stdin = strings.NewReader(text)
return cmd.Run()
}
@@ -131,6 +132,15 @@ type FileFilter struct {
func psEscape(s string) string { return strings.ReplaceAll(s, "'", "''") }
func hiddenCommand(name string, args ...string) *exec.Cmd {
cmd := exec.Command(name, args...)
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: true,
CreationFlags: 0x08000000,
}
return cmd
}
func PickFiles(title string, filters []FileFilter, multi bool, initialDir string) []string {
filterStr := "All files|*.*"
if len(filters) > 0 {
@@ -159,7 +169,7 @@ if ($dlg.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
$dlg.FileNames | ForEach-Object { Write-Output $_ }
}
`, psEscape(title), psEscape(filterStr), multiStr, psEscape(initialDir), psEscape(initialDir))
out, err := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-STA", "-Command", script).Output()
out, err := hiddenCommand("powershell", "-NoProfile", "-NonInteractive", "-STA", "-Command", script).Output()
if err != nil {
return nil
}
@@ -198,7 +208,7 @@ if ($dlg.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
Write-Output $dlg.FileName
}
`, psEscape(title), psEscape(filterStr), psEscape(defaultName), psEscape(initialDir), psEscape(initialDir))
out, err := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-STA", "-Command", script).Output()
out, err := hiddenCommand("powershell", "-NoProfile", "-NonInteractive", "-STA", "-Command", script).Output()
if err != nil {
return ""
}
@@ -215,7 +225,7 @@ if ($dlg.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
Write-Output $dlg.SelectedPath
}
`, psEscape(title), psEscape(initialDir), psEscape(initialDir))
out, err := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-STA", "-Command", script).Output()
out, err := hiddenCommand("powershell", "-NoProfile", "-NonInteractive", "-STA", "-Command", script).Output()
if err != nil {
return ""
}