0.1.6 updates and add changelog
This commit is contained in:
116
standalone/changelog.go
Normal file
116
standalone/changelog.go
Normal 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()
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user