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

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 ""
}