lyrics

lyrics
git clone git://brookjeynes.dev/bjeynes/lyrics.git
Log | Files | Refs

commit 5083fe684fd6ce8612d58c10ce22546533dff606
parent 32de8e817e22882b59bef51d493f031935dd84fc
Author: brookjeynes <me@brookjeynes.dev>
Date:   Mon, 22 Jun 2026 08:06:40 +1000

feat: order songs via input type number

Task-id: rrpptxupmmsrnrsz
Signed-off-by: brookjeynes <me@brookjeynes.dev>

Diffstat:
Minternal/server/edit-release.go | 35+++++++++++++++++++++++++++--------
Minternal/server/ui/picker-list.css | 4++++
Minternal/server/ui/views/edit-release/edit-release.go | 10++++++----
Minternal/server/ui/views/edit-release/edit-release.js | 38++++++++++++++++++++++++++++++++++++++
Minternal/server/ui/views/edit-release/edit-release.templ | 28+++++++++-------------------
Minternal/server/ui/views/release/release.templ | 2+-
Rtasks/order_songs_via_input_type_number.txt -> tasks/done/order_songs_via_input_type_number.txt | 0
7 files changed, 85 insertions(+), 32 deletions(-)

diff --git a/internal/server/edit-release.go b/internal/server/edit-release.go @@ -20,6 +20,7 @@ type EditReleaseForm struct { LatinTitle string `schema:"latin-title"` ArtistID []domain.ArtistID `schema:"artist-id"` SongID []domain.SongID `schema:"song-id"` + TrackNumber []int `schema:"track-number"` ReleaseDate string `schema:"release-date"` MusicBrainzReleaseID domain.MusicBrainzID `schema:"musicbrainz-release-id"` } @@ -87,22 +88,40 @@ func parseEditReleaseForm(form EditReleaseForm) (domain.EditRelease, error) { minDate.Format("2006-01-02"), maxDate.Format("2006-01-02")) } + if len(form.SongID) != len(form.TrackNumber) { + return release, fmt.Errorf("Song ID and track number counts do not match", err) + } + songs := make([]domain.EditReleaseSong, 0, len(form.SongID)) - seenSongs := make(map[int64]struct{}, len(form.SongID)) + seenTracknumbers := make(map[int]domain.SongID, len(form.SongID)) + seenSongs := make(map[domain.SongID]int, len(form.SongID)) - for i, id := range form.SongID { - if id < 0 { + for i := 0; i < len(form.SongID); i++ { + songID := form.SongID[i] + trackNumber := form.TrackNumber[i] + + if songID < 0 { return release, fmt.Errorf("Invalid song selection") } - if _, exists := seenSongs[int64(id)]; exists { + + if trackNumber < 1 { + return release, fmt.Errorf("Track number can not be less that 1") + } + + if _, exists := seenTracknumbers[trackNumber]; exists { + return release, fmt.Errorf("Songs may not share the same track number") + } + + if _, exists := seenSongs[songID]; exists { return release, fmt.Errorf("A song cannot be added more than once") } - seenSongs[int64(id)] = struct{}{} + + seenTracknumbers[trackNumber] = songID + seenSongs[songID] = trackNumber songs = append(songs, domain.EditReleaseSong{ - SongID: id, - // Track numbers should start at 1 - TrackNumber: i + 1, + SongID: songID, + TrackNumber: trackNumber, }) } diff --git a/internal/server/ui/picker-list.css b/internal/server/ui/picker-list.css @@ -5,6 +5,10 @@ display: flex; align-items: center; gap: 0.75rem; + + input[type="number"] { + width: 3em; + } } .picker-main { diff --git a/internal/server/ui/views/edit-release/edit-release.go b/internal/server/ui/views/edit-release/edit-release.go @@ -17,8 +17,9 @@ type editReleaseArtist struct { } type editReleaseSong struct { - ID domain.SongID `json:"id"` - Title string `json:"title"` + ID domain.SongID `json:"id"` + Title string `json:"title"` + TrackNumber int `json:"trackNumber"` } func editReleaseArtistsJSON(artists []domain.ReleaseArtist) string { @@ -50,8 +51,9 @@ func editReleaseSongsJSON(songs []domain.ReleaseSong) string { title += " (" + *as.Song.LatinTitle + ")" } jsonSongs = append(jsonSongs, editReleaseSong{ - ID: as.Song.ID, - Title: title, + ID: as.Song.ID, + Title: title, + TrackNumber: as.TrackNumber, }) } diff --git a/internal/server/ui/views/edit-release/edit-release.js b/internal/server/ui/views/edit-release/edit-release.js @@ -7,3 +7,41 @@ form.addEventListener("htmx:config:request", function(evt) { ) return; evt.detail.ctx.request.body.delete("artist"); }); + +/** + * @typedef {Object} EditReleaseSong + * @property {number} id + * @property {string} title + * @property {number} trackNumber + */ + +/** + * @param {Event} e + * @param {EditReleaseSong[]} items + * @param {number} a Current track number + * @param {string} b Desired track number + */ +function changeTrackOrder(e, items, a, b) { + if (b <= 0) return; + if (isNaN(b)) return; + + if (items.length === 1) { + items[a].trackNumber = b; + return; + } + + const swapTo = items.findIndex(item => item.trackNumber == b); + + if (swapTo !== -1) { + [items[a].trackNumber, items[swapTo].trackNumber] = [items[swapTo].trackNumber, items[a].trackNumber]; + [items[a], items[swapTo]] = [items[swapTo], items[a]]; + return; + } + + const item = items[a]; + item.trackNumber = b; + items.push(item); + items.splice(a, 1); + + items.sort((a, b) => a.trackNumber - b.trackNumber); +} diff --git a/internal/server/ui/views/edit-release/edit-release.templ b/internal/server/ui/views/edit-release/edit-release.templ @@ -183,31 +183,21 @@ templ EditRelease(params EditReleaseParams) { } else { <ol class="picker-list"> <template x-for="(item, i) in items" :key="item.id"> - <li x-init="htmx.process($el); initLucideIcons()"> + <li :value="item.trackNumber" x-init="htmx.process($el); initLucideIcons()"> <div class="picker-row"> + <input + type="number" + :value="item.trackNumber" + min="1" + @change="changeTrackOrder($event, items, i, $event.target.value)" + required + /> <div class="picker-main"> <strong x-text="item.title"></strong> </div> - <div class="picker-actions"> - <button - type="button" - aria-label="Move up" - x-show="i >= 1" - @click="[items[i - 1], items[i]] = [items[i], items[i - 1]]" - > - <i data-lucide="chevron-up"></i> - </button> - <button - type="button" - aria-label="Move down" - x-show="i + 1 < items.length" - @click="[items[i + 1], items[i]] = [items[i], items[i + 1]]" - > - <i data-lucide="chevron-down"></i> - </button> - </div> </div> <input type="hidden" name="song-id" :value="item.id"/> + <input type="hidden" name="track-number" :value="item.trackNumber"/> </li> </template> </ol> diff --git a/internal/server/ui/views/release/release.templ b/internal/server/ui/views/release/release.templ @@ -24,7 +24,7 @@ templ Release(params ReleaseParams) { } else { <ol> for _, as := range params.Release.Songs { - <li> + <li value={ as.TrackNumber }> <a href={ routes.Song(as.Song) }> { as.Song.Title } if as.Song.LatinTitle != nil { diff --git a/tasks/order_songs_via_input_type_number.txt b/tasks/done/order_songs_via_input_type_number.txt