3d7be594c6e8cd6e5b73a5cb37f47ec88ac50962

Author
TheEdgeOfRage <git@theedgeofrage.com>
Committer
TheEdgeOfRage <git@theedgeofrage.com>
Date

Message

Add web UI polish: progress dots, now-playing equalizer, htmx fade transitions, README docs

- Round progress dots in the HUD (filled per completed round)
- Animated equalizer 'now playing' indicator during playback
- Smooth fade-in on htmx state swaps (.htmx-added)
- Cap displayed round number at total when game is over
- Document -web / -port flags and web mode in README

Diff

  1diff --git a/README.md b/README.md
  2index ac062bdc50b67ef4c5b742c26570fbd90f6a60e7..752bb35082405889b625a62caaed0e337664ee98 100644
  3--- a/README.md
  4+++ b/README.md
  5@@ -50,6 +50,27 @@ museguessr -dir /path/to/music [-blacklist blacklist.txt] [-rounds 10]
  6 | `-blacklist` | `blacklist`        | File with one directory per line; every file under it is excluded. |
  7 | `-rounds`    | `10`               | Number of songs to play. `0` means one per song in the catalog.    |
  8 
  9+## Web UI
 10+
 11+Run the game as a web app instead of the TUI with `-web`:
 12+
 13+```sh
 14+museguessr -dir /path/to/music -web [-port 8080]
 15+```
 16+
 17+This starts an HTTP server (default port `8080`) that serves a single-page
 18+interface built with [Go templates](https://pkg.go.dev/html/template) and
 19+[htmx](https://htmx.org). Audio clips are generated on the fly with `ffmpeg`
 20+and streamed to the browser as WAV, so no local audio playback is required —
 21+just `ffmpeg` and `ffprobe` on `PATH`. Open <http://localhost:8080> in a
 22+browser. Type your guess in the three fields and submit; use **Next song** to
 23+advance and **Play again** to restart.
 24+
 25+| Flag    | Default | Description                        |
 26+| ------- | ------- | ---------------------------------- |
 27+| `-web`  | `false` | Serve a web UI instead of the TUI. |
 28+| `-port` | `8080`  | Port for the web server.           |
 29+
 30 ### Blacklist file
 31 
 32 One **directory** per line — every file under that directory is excluded.
 33diff --git a/server.go b/server.go
 34index 33023444c5d91f53b7284dcaaa5ee37645a97533..4e092daed5584465c1206ac0ec0995f90b227c9b 100644
 35--- a/server.go
 36+++ b/server.go
 37@@ -166,6 +166,7 @@ type viewData struct {
 38 	MaxScore    int
 39 	Round       int
 40 	TotalRounds int
 41+	Dots        []bool
 42 	Answer      Song
 43 	Hinted      bool
 44 	Done        bool
 45@@ -177,11 +178,16 @@ func (g *Game) viewData() viewData {
 46 	if round > g.rounds {
 47 		round = g.rounds
 48 	}
 49+	dots := make([]bool, g.rounds)
 50+	for i := 0; i < g.played && i < g.rounds; i++ {
 51+		dots[i] = true
 52+	}
 53 	return viewData{
 54 		Score:       g.score,
 55 		MaxScore:    g.rounds * pointsPerRound,
 56 		Round:       round,
 57 		TotalRounds: g.rounds,
 58+		Dots:        dots,
 59 		Answer:      g.answer,
 60 		Hinted:      g.hinted,
 61 		Done:        g.done,
 62diff --git a/web/static/style.css b/web/static/style.css
 63index c6b52ecb7fb2e9fae26767594f6d8162e0d6a626..8bea289f52973a99e8fd36e15deef703990a3ecd 100644
 64--- a/web/static/style.css
 65+++ b/web/static/style.css
 66@@ -168,3 +168,44 @@ button:active { transform: translateY(1px); }
 67   line-height: 1;
 68 }
 69 .final-sub { color: var(--dim); margin: 0; }
 70+
 71+/* Round progress dots */
 72+.progress { display: flex; gap: 6px; margin-bottom: 18px; }
 73+.dot {
 74+  width: 9px;
 75+  height: 9px;
 76+  border-radius: 50%;
 77+  background: var(--line);
 78+  transition: background 0.2s, box-shadow 0.2s;
 79+}
 80+.dot.done { background: var(--accent); box-shadow: 0 0 6px rgba(224, 164, 88, 0.5); }
 81+
 82+/* Now-playing equalizer */
 83+.now-playing {
 84+  display: flex;
 85+  align-items: center;
 86+  gap: 8px;
 87+  color: var(--dim);
 88+  font-size: 0.85rem;
 89+  margin-bottom: 14px;
 90+}
 91+.eq { display: inline-flex; align-items: flex-end; gap: 2px; height: 14px; }
 92+.eq i {
 93+  width: 3px;
 94+  background: var(--accent);
 95+  border-radius: 2px;
 96+  animation: eq 0.9s ease-in-out infinite;
 97+}
 98+.eq i:nth-child(2) { animation-delay: 0.25s; }
 99+.eq i:nth-child(3) { animation-delay: 0.5s; }
100+@keyframes eq {
101+  0%, 100% { height: 4px; }
102+  50% { height: 14px; }
103+}
104+
105+/* Smooth htmx state swaps */
106+#game .htmx-added { animation: fadeIn 0.28s ease; }
107+@keyframes fadeIn {
108+  from { opacity: 0; transform: translateY(5px); }
109+  to { opacity: 1; transform: none; }
110+}
111diff --git a/web/templates/game.html b/web/templates/game.html
112index 642bb80a3f80a926a77787fc9d5350259319d107..f35176be78ce9b30be61b359c19b3ac25078d566 100644
113--- a/web/templates/game.html
114+++ b/web/templates/game.html
115@@ -4,6 +4,9 @@
116   <div class="score">Score: {{.Score}}/{{.MaxScore}}</div>
117 </header>
118 <p class="round">Round {{.Round}} of {{.TotalRounds}}</p>
119+<div class="progress">
120+  {{range .Dots}}<span class="dot {{if .}}done{{end}}"></span>{{end}}
121+</div>
122 
123 {{if .Done}}{{template "gameover" .}}{{else if .Result}}{{template "result" .}}{{else}}{{template "playing" .}}{{end}}
124 {{end}}
125diff --git a/web/templates/playing.html b/web/templates/playing.html
126index 9466a0807baeb2cd4e6e26280731902c4ce118e5..566d8fc88c10b7140fc662749bcd9e62aa81b8ae 100644
127--- a/web/templates/playing.html
128+++ b/web/templates/playing.html
129@@ -1,4 +1,7 @@
130 {{define "playing"}}
131+<div class="now-playing">
132+  <span class="eq"><i></i><i></i><i></i></span> Now playing
133+</div>
134 <audio id="clip" src="/audio" autoplay></audio>
135 <script>
136   (function () {