Release 2026.08.01-1
Lyrics, versioning, and the song page's fact line. - Lyrics are suggested at submission and never imposed: the worker makes one LRCLIB lookup, and Hae sanoitukset re-queries with whatever title and artist are typed. Neither overwrites what the submitter wrote. They live on the submission, travel to the song at publish, and stay editable after the song locks — the lock freezes what a song claims to be, and nobody reviewed the lyrics - The review strip reads and writes side by side: lyrics left, review right. Synced LRC highlights the playing line and seeks on click; plain text scrolls continuously with a nudge knob. Following can be turned off - CalVer YYYY.MM.DD-N, injected from the git tag with -ldflags, shown in the footer, the startup log and /healthz - The song page's metadata became four labelled cells instead of one flat run of five different kinds of fact Fixes: publishing wiped lyrics the worker had just fetched (a request that omitted a field cleared it), lyric auto-scroll landed in the wrong place, and the fader shifted the deck sideways at score 100.
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<script src="/static/htmx.min.js" defer></script>
|
||||
<script src="/static/player.js" defer></script>
|
||||
<script src="/static/lyrics.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
|
||||
@@ -1,3 +1,30 @@
|
||||
{{/* Two shapes, because timed lyrics and guessed lyrics deserve different treatment.
|
||||
Synced: one element per line with its own timestamp, highlighted as it comes.
|
||||
Plain: one block that scrolls continuously, with a nudge knob, because a highlight on evenly
|
||||
guessed timings turns guaranteed drift into what looks like a bug. */}}
|
||||
{{define "lyricsview"}}
|
||||
<span class="cap">Sanoitukset</span>
|
||||
{{$lines := .LyricLines}}
|
||||
{{if $lines}}
|
||||
<div class="lyricsbox synced" data-song="{{.ID}}">
|
||||
{{range $lines}}<p class="lline" data-t="{{.At}}">{{if .Text}}{{.Text}}{{else}} {{end}}</p>{{end}}
|
||||
</div>
|
||||
<label class="follow">
|
||||
<input type="checkbox" checked> Seuraa kappaletta
|
||||
<span class="muted small">— korostus jatkuu, sivu ei vieri</span>
|
||||
</label>
|
||||
{{else}}
|
||||
<div class="lyricsbox plain" data-duration="{{.Duration}}" data-song="{{.ID}}">
|
||||
<div class="lscroll">{{lyricstext .Lyrics}}</div>
|
||||
</div>
|
||||
<label class="nudge">
|
||||
Ajoitus
|
||||
<input type="range" min="-10" max="10" step="0.5" value="0" aria-label="Ajoituksen siirto sekunteina">
|
||||
<output>0 s</output>
|
||||
</label>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{define "player"}}
|
||||
<!-- Ships with native controls; player.js removes them and drives the same element. No JS means
|
||||
the browser's own player, which is plain but complete. -->
|
||||
|
||||
+36
-8
@@ -49,14 +49,23 @@
|
||||
<div class="deck">
|
||||
{{template "player" $s}}
|
||||
{{with $s.Description}}<p class="intro">{{.}}</p>{{end}}
|
||||
<label class="grow">Arvostelu
|
||||
<textarea name="text" rows="8" maxlength="5000" required
|
||||
placeholder="Mitä kuulit?"></textarea>
|
||||
</label>
|
||||
<div class="deckfoot">
|
||||
<button type="submit">Tallenna arvostelu</button>
|
||||
<span class="muted small">Muiden pisteet paljastuvat kun tallennat omasi. Voit muokata
|
||||
tai poistaa arvostelusi 30 minuutin ajan.</span>
|
||||
|
||||
<!-- Two panes when the song has lyrics: read on the left, write on the right, so following
|
||||
the words costs no scrolling. Without lyrics the pane is absent, not empty. -->
|
||||
<div class="panes{{if not $s.Lyrics}} solo{{end}}">
|
||||
{{if $s.Lyrics}}
|
||||
<div class="lyricspane">{{template "lyricsview" $s}}</div>
|
||||
{{end}}
|
||||
<div class="writepane">
|
||||
<label class="grow">Arvostelu
|
||||
<textarea name="text" maxlength="5000" required placeholder="Mitä kuulit?"></textarea>
|
||||
</label>
|
||||
<div class="deckfoot">
|
||||
<button type="submit">Tallenna arvostelu</button>
|
||||
<span class="muted small">Muiden pisteet paljastuvat kun tallennat omasi. Voit muokata
|
||||
tai poistaa arvostelusi 30 minuutin ajan.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -67,6 +76,25 @@
|
||||
|
||||
{{with $s.SourceURL}}<p class="muted small"><a href="{{.}}" rel="noreferrer">Kuuntele YouTubessa</a></p>{{end}}
|
||||
|
||||
{{if and (not $s.CanReview) (or $s.Lyrics $s.Own)}}
|
||||
<!-- Only when the review strip is not already showing them: while reviewing, the lyrics live in
|
||||
the left pane. This panel is for reading afterwards and for the submitter's edits. -->
|
||||
<details class="lyrics">
|
||||
<summary>Sanoitukset{{if not $s.Lyrics}} <span class="muted small">— ei vielä lisätty</span>{{end}}</summary>
|
||||
{{if $s.Lyrics}}<pre class="lyricstext">{{lyricstext $s.Lyrics}}</pre>{{end}}
|
||||
{{if $s.Own}}
|
||||
<form method="post" action="/songs/{{$s.ID}}/lyrics" class="stack">
|
||||
<label>Muokkaa sanoituksia
|
||||
<textarea name="lyrics" rows="10" maxlength="20000"
|
||||
placeholder="Liitä sanoitukset tähän.">{{$s.Lyrics}}</textarea>
|
||||
</label>
|
||||
<button type="submit">Tallenna sanoitukset</button>
|
||||
</form>
|
||||
<p class="muted small">Sanoituksia voi muokata vielä arvostelujenkin jälkeen.</p>
|
||||
{{end}}
|
||||
</details>
|
||||
{{end}}
|
||||
|
||||
{{if $s.ViewerReview}}
|
||||
<section>
|
||||
<h2>Oma arvostelusi</h2>
|
||||
|
||||
@@ -31,6 +31,28 @@
|
||||
|
||||
{{define "saved"}}<span id="saved" class="saved">{{if .}}Tallennettu {{.}}{{end}}</span>{{end}}
|
||||
|
||||
<!-- Included by the page and returned alone by the suggestion button, so the markup exists once.
|
||||
hx-include sends the current title and artist, which is the whole point: the lookup uses what
|
||||
the submitter just fixed, not what the tags claimed. -->
|
||||
{{define "lyricsfield"}}
|
||||
<div id="lyricsfield">
|
||||
<label>Sanoitukset <span class="muted small">(vapaaehtoinen)</span>
|
||||
<textarea name="lyrics" rows="8" maxlength="20000"
|
||||
placeholder="Liitä sanoitukset tähän tai hae ne alta.">{{.Lyrics}}</textarea>
|
||||
</label>
|
||||
<div class="lyricsbar">
|
||||
<button type="button" class="ghost"
|
||||
hx-post="/submit/{{.ID}}/lyrics"
|
||||
hx-include="[name='title'], [name='artist']"
|
||||
hx-target="#lyricsfield" hx-swap="outerHTML">Hae sanoitukset</button>
|
||||
{{if .Found}}<span class="muted small">Löytyi — tarkista ja muokkaa tarvittaessa.</span>{{end}}
|
||||
{{if .Searched}}{{if not .Found}}
|
||||
<span class="muted small">Ei löytynyt. Tarkista nimi ja esittäjä tai liitä sanoitukset itse.</span>
|
||||
{{end}}{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<h1>Lähetys</h1>
|
||||
|
||||
@@ -58,6 +80,7 @@
|
||||
<label>Esittely <span class="muted small">(vapaaehtoinen)</span>
|
||||
<textarea name="description" rows="5" maxlength="2000">{{.Data.Description}}</textarea>
|
||||
</label>
|
||||
{{template "lyricsfield" dict "ID" .Data.ID "Lyrics" .Data.Lyrics}}
|
||||
{{template "saved" ""}}
|
||||
</form>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user