Add lyrics: paste, fetch, and read them while reviewing
Lyrics are suggested at submission and never imposed. The conversion worker makes one LRCLIB lookup with whatever metadata exists, and the waiting page has a Hae sanoitukset button that re-queries with whatever title and artist are currently typed — which is the case that matters, since our metadata comes from ID3 tags and YouTube uploaders. Neither path overwrites typed text. - lyrics text on both submissions and songs, copied across at publish. Nothing has launched, so the column goes into 001_init.sql rather than a migration - The lock does not cover lyrics: it freezes what the song claims to be, and nobody reviewed the lyrics. So the submitter can still fix them afterwards, or paste them for an old song a year later - The review strip gained a second pane: lyrics on the left, review on the right, so following the words costs no scrolling. No lyrics means no pane, not an empty one. Below 1024px the panes stack - LRC timestamps are stored but stripped for reading — they belong to the player, not the reader - The lyrics box is capped and scrolls inside itself, so a long song cannot stretch the strip past the screen Fixes a real bug found on the way: saveMetadata cleared any field the request did not carry, so publishing wiped the lyrics the worker had just fetched. Fields absent from a request now keep their stored value. The client identifies itself to LRCLIB as "levyraati" and nothing more. Tests cover cleanLyrics keeping line breaks, and fetchLyrics against a local server: synced beats plain, instrumentals and wrong-length takes are skipped, and a miss is empty with no error.
This commit is contained in:
@@ -330,6 +330,35 @@ footer.sitefooter {
|
||||
|
||||
.average { font-family: var(--font-display); font-size: 1.2rem; color: var(--gold-1); }
|
||||
|
||||
.lyrics {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: var(--radius);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
margin-bottom: var(--space-5);
|
||||
}
|
||||
|
||||
.lyrics > summary {
|
||||
cursor: pointer;
|
||||
font-family: var(--font-display);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
/* Lyrics are typed with intent: line breaks and indentation are the content. */
|
||||
.lyricstext {
|
||||
font-family: inherit;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.7;
|
||||
white-space: pre-wrap;
|
||||
margin: var(--space-4) 0 0;
|
||||
max-height: 26rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.lyrics form { margin-top: var(--space-4); }
|
||||
|
||||
.review {
|
||||
padding: var(--space-4);
|
||||
border-left: 3px solid var(--input-border);
|
||||
@@ -424,6 +453,39 @@ button.link:hover { background: none; color: var(--primary-hover); }
|
||||
|
||||
.deck { display: flex; flex-direction: column; gap: var(--space-3); min-width: 0; }
|
||||
.deck .grow { flex: 1; }
|
||||
|
||||
/* Read on the left, write on the right. One column when the song has no lyrics — the pane is
|
||||
absent rather than empty. */
|
||||
.panes { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); flex: 1; min-height: 0; }
|
||||
.panes.solo { grid-template-columns: 1fr; }
|
||||
|
||||
.lyricspane, .writepane { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
|
||||
.writepane .grow { display: flex; flex-direction: column; gap: var(--space-1); }
|
||||
.writepane textarea { flex: 1; min-height: 12rem; }
|
||||
|
||||
.cap {
|
||||
font-family: var(--font-display);
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.lyricsbox {
|
||||
/* Grows with the pane but stops before it can push the page: long lyrics scroll inside the box
|
||||
rather than stretching the strip past the screen. */
|
||||
flex: 1 1 auto;
|
||||
min-height: 10rem;
|
||||
max-height: 24rem;
|
||||
overflow-y: auto;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
background: var(--bar);
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: var(--radius);
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.7;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.deck .player { margin: 0; }
|
||||
.deckfoot { display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap; }
|
||||
|
||||
@@ -740,6 +802,9 @@ code { background: var(--surface-raised); padding: 0.1rem var(--space-1);
|
||||
|
||||
/* A 200px fader on a phone is worse than a horizontal one. */
|
||||
.strip { grid-template-columns: 1fr; gap: var(--space-3); padding: var(--space-4); }
|
||||
/* Side by side needs width it does not have here, so reading stacks above writing. */
|
||||
.panes { grid-template-columns: 1fr; }
|
||||
.lyricsbox { max-height: 14rem; }
|
||||
.fader { grid-template-columns: 1fr auto; grid-template-rows: auto; align-items: center; }
|
||||
.fader input[type="range"] { writing-mode: horizontal-tb; direction: ltr;
|
||||
width: 100%; height: auto; min-height: 0; }
|
||||
@@ -980,3 +1045,6 @@ img.avatar { object-fit: cover; }
|
||||
|
||||
.version { color: var(--muted); font-family: var(--font-display); }
|
||||
.version::before { content: "·"; margin: 0 var(--space-2); }
|
||||
|
||||
.lyricsbar { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap;
|
||||
margin-top: var(--space-2); }
|
||||
|
||||
Reference in New Issue
Block a user