From b0dd87cad383c820863eb51b9901971481898985 Mon Sep 17 00:00:00 2001 From: ari melody Date: Tue, 21 Oct 2025 18:39:38 +0100 Subject: [PATCH] add artists/tracks pages; more components; css cleanup --- admin/artisthttp.go | 48 +++++-- admin/http.go | 20 ++- admin/releasehttp.go | 40 +++--- admin/static/admin.css | 32 +++-- admin/static/artists.css | 23 ++++ admin/static/artists.js | 7 + admin/static/edit-release.css | 69 +--------- admin/static/index.css | 84 ------------ admin/static/index.js | 8 -- .../{release-list-item.css => releases.css} | 14 +- admin/static/tracks.css | 127 ++++++++++++++++++ admin/templates/html/artists.html | 20 ++- .../html/components/artist/artist.html | 6 + .../{credits => credit}/addcredit.html | 0 .../{credits => credit}/editcredits.html | 0 .../{credits => credit}/newcredit.html | 0 .../components/{links => link}/editlinks.html | 0 .../{release-list-item.html => release.html} | 0 .../{tracks => track}/addtrack.html | 0 .../{tracks => track}/edittracks.html | 0 .../{tracks => track}/newtrack.html | 0 .../html/components/track/track.html | 24 ++++ admin/templates/html/edit-artist.html | 1 + admin/templates/html/edit-release.html | 23 +--- admin/templates/html/edit-track.html | 2 + admin/templates/html/index.html | 41 ++---- admin/templates/html/logs.html | 2 +- admin/templates/html/releases.html | 17 ++- admin/templates/html/tracks.html | 51 ++++--- admin/templates/templates.go | 123 +++++++++++------ admin/trackhttp.go | 48 +++++-- controller/artist.go | 5 + controller/release.go | 2 +- controller/track.go | 5 + model/release.go | 2 +- model/track.go | 6 +- public/style/music-gateway.css | 2 +- 37 files changed, 498 insertions(+), 354 deletions(-) create mode 100644 admin/static/artists.css create mode 100644 admin/static/artists.js delete mode 100644 admin/static/index.css rename admin/static/{release-list-item.css => releases.css} (83%) create mode 100644 admin/static/tracks.css create mode 100644 admin/templates/html/components/artist/artist.html rename admin/templates/html/components/{credits => credit}/addcredit.html (100%) rename admin/templates/html/components/{credits => credit}/editcredits.html (100%) rename admin/templates/html/components/{credits => credit}/newcredit.html (100%) rename admin/templates/html/components/{links => link}/editlinks.html (100%) rename admin/templates/html/components/release/{release-list-item.html => release.html} (100%) rename admin/templates/html/components/{tracks => track}/addtrack.html (100%) rename admin/templates/html/components/{tracks => track}/edittracks.html (100%) rename admin/templates/html/components/{tracks => track}/newtrack.html (100%) create mode 100644 admin/templates/html/components/track/track.html diff --git a/admin/artisthttp.go b/admin/artisthttp.go index 9d99fd4..f151ddd 100644 --- a/admin/artisthttp.go +++ b/admin/artisthttp.go @@ -12,22 +12,57 @@ import ( func serveArtists(app *model.AppState) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value("session").(*model.Session) + slices := strings.Split(strings.TrimPrefix(r.URL.Path, "/artists")[1:], "/") - id := slices[0] - artist, err := controller.GetArtist(app.DB, id) + artistID := slices[0] + + if len(artistID) > 0 { + serveArtist(app, artistID).ServeHTTP(w, r) + return + } + + artists, err := controller.GetAllArtists(app.DB) + if err != nil { + fmt.Printf("WARN: Failed to fetch artists: %s\n", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + type ArtistsResponse struct { + adminPageData + Artists []*model.Artist + } + + err = templates.ArtistsTemplate.Execute(w, ArtistsResponse{ + adminPageData: adminPageData{ Path: r.URL.Path, Session: session }, + Artists: artists, + }) + if err != nil { + fmt.Printf("WARN: Failed to serve admin artists page: %s\n", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } + }) +} + +func serveArtist(app *model.AppState, artistID string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value("session").(*model.Session) + + artist, err := controller.GetArtist(app.DB, artistID) if err != nil { if artist == nil { http.NotFound(w, r) return } - fmt.Printf("Error rendering admin artist page for %s: %s\n", id, err) + fmt.Printf("WARN: Failed to fetch artist %s: %s\n", artistID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } credits, err := controller.GetArtistCredits(app.DB, artist.ID, true) if err != nil { - fmt.Printf("Error rendering admin track page for %s: %s\n", id, err) + fmt.Printf("WARN: Failed to serve admin artist page for %s: %s\n", artistID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } @@ -38,17 +73,14 @@ func serveArtists(app *model.AppState) http.Handler { Credits []*model.Credit } - session := r.Context().Value("session").(*model.Session) - err = templates.EditArtistTemplate.Execute(w, ArtistResponse{ adminPageData: adminPageData{ Path: r.URL.Path, Session: session }, Artist: artist, Credits: credits, }) if err != nil { - fmt.Printf("Error rendering admin track page for %s: %s\n", id, err) + fmt.Printf("WARN: Failed to serve admin artist page for %s: %s\n", artistID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) } - diff --git a/admin/http.go b/admin/http.go index 1094095..f65d8b9 100644 --- a/admin/http.go +++ b/admin/http.go @@ -53,7 +53,9 @@ func Handler(app *model.AppState) http.Handler { mux.Handle("/releases", requireAccount(serveReleases(app))) mux.Handle("/releases/", requireAccount(serveReleases(app))) + mux.Handle("/artists", requireAccount(serveArtists(app))) mux.Handle("/artists/", requireAccount(serveArtists(app))) + mux.Handle("/tracks", requireAccount(serveTracks(app))) mux.Handle("/tracks/", requireAccount(serveTracks(app))) mux.Handle("/static/", staticHandler()) @@ -79,7 +81,7 @@ func AdminIndexHandler(app *model.AppState) http.Handler { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - releaseCount, err := controller.GetReleasesCount(app.DB, false) + releaseCount, err := controller.GetReleaseCount(app.DB, false) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to pull releases count: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -92,6 +94,12 @@ func AdminIndexHandler(app *model.AppState) http.Handler { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } + artistCount, err := controller.GetArtistCount(app.DB) + if err != nil { + fmt.Fprintf(os.Stderr, "WARN: Failed to pull artist count: %s\n", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } tracks, err := controller.GetOrphanTracks(app.DB) if err != nil { @@ -99,13 +107,21 @@ func AdminIndexHandler(app *model.AppState) http.Handler { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } + trackCount, err := controller.GetTrackCount(app.DB) + if err != nil { + fmt.Fprintf(os.Stderr, "WARN: Failed to pull track count: %s\n", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } type IndexData struct { adminPageData Releases []*model.Release ReleaseCount int Artists []*model.Artist + ArtistCount int Tracks []*model.Track + TrackCount int } err = templates.IndexTemplate.Execute(w, IndexData{ @@ -113,7 +129,9 @@ func AdminIndexHandler(app *model.AppState) http.Handler { Releases: releases, ReleaseCount: releaseCount, Artists: artists, + ArtistCount: artistCount, Tracks: tracks, + TrackCount: trackCount, }) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to render admin index: %s\n", err) diff --git a/admin/releasehttp.go b/admin/releasehttp.go index 5484609..028ad2f 100644 --- a/admin/releasehttp.go +++ b/admin/releasehttp.go @@ -13,6 +13,8 @@ import ( func serveReleases(app *model.AppState) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value("session").(*model.Session) + slices := strings.Split(strings.TrimPrefix(r.URL.Path, "/releases")[1:], "/") releaseID := slices[0] @@ -26,8 +28,6 @@ func serveReleases(app *model.AppState) http.Handler { return } - session := r.Context().Value("session").(*model.Session) - type ReleasesData struct { adminPageData Releases []*model.Release @@ -35,7 +35,7 @@ func serveReleases(app *model.AppState) http.Handler { releases, err := controller.GetAllReleases(app.DB, false, 0, true) if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to pull releases: %s\n", err) + fmt.Fprintf(os.Stderr, "WARN: Failed to fetch releases: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } @@ -48,7 +48,7 @@ func serveReleases(app *model.AppState) http.Handler { Releases: releases, }) if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to render releases page: %s\n", err) + fmt.Fprintf(os.Stderr, "WARN: Failed to serve releases page: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } @@ -65,7 +65,7 @@ func serveRelease(app *model.AppState, releaseID string, action string) http.Han http.NotFound(w, r) return } - fmt.Printf("WARN: Failed to pull full release data for %s: %s\n", releaseID, err) + fmt.Printf("WARN: Failed to fetch full release data for %s: %s\n", releaseID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } @@ -103,12 +103,16 @@ func serveRelease(app *model.AppState, releaseID string, action string) http.Han Release *model.Release } + for i, track := range release.Tracks { + track.Number = i + 1 + } + err = templates.EditReleaseTemplate.Execute(w, ReleaseResponse{ adminPageData: adminPageData{ Path: r.URL.Path, Session: session }, Release: release, }) if err != nil { - fmt.Printf("Error rendering admin release page for %s: %s\n", release.ID, err) + fmt.Printf("WARN: Failed to serve admin release page for %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) @@ -119,7 +123,7 @@ func serveEditCredits(release *model.Release) http.Handler { w.Header().Set("Content-Type", "text/html") err := templates.EditCreditsTemplate.Execute(w, release) if err != nil { - fmt.Printf("Error rendering edit credits component for %s: %s\n", release.ID, err) + fmt.Printf("WARN: Failed to serve edit credits component for %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) @@ -129,7 +133,7 @@ func serveAddCredit(app *model.AppState, release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { artists, err := controller.GetArtistsNotOnRelease(app.DB, release.ID) if err != nil { - fmt.Printf("WARN: Failed to pull artists not on %s: %s\n", release.ID, err) + fmt.Printf("WARN: Failed to fetch artists not on %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } @@ -145,7 +149,7 @@ func serveAddCredit(app *model.AppState, release *model.Release) http.Handler { Artists: artists, }) if err != nil { - fmt.Printf("Error rendering add credits component for %s: %s\n", release.ID, err) + fmt.Printf("WARN: Failed to serve add credits component for %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) @@ -156,7 +160,7 @@ func serveNewCredit(app *model.AppState) http.Handler { artistID := strings.Split(r.URL.Path, "/")[3] artist, err := controller.GetArtist(app.DB, artistID) if err != nil { - fmt.Printf("WARN: Failed to pull artists %s: %s\n", artistID, err) + fmt.Printf("WARN: Failed to fetch artist %s: %s\n", artistID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } @@ -168,7 +172,7 @@ func serveNewCredit(app *model.AppState) http.Handler { w.Header().Set("Content-Type", "text/html") err = templates.NewCreditTemplate.Execute(w, artist) if err != nil { - fmt.Printf("Error rendering new credit component for %s: %s\n", artist.ID, err) + fmt.Printf("WARN: Failed to serve new credit component for %s: %s\n", artist.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) @@ -177,9 +181,9 @@ func serveNewCredit(app *model.AppState) http.Handler { func serveEditLinks(release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") - err := templates.EditCreditsTemplate.Execute(w, release) + err := templates.EditLinksTemplate.Execute(w, release) if err != nil { - fmt.Printf("Error rendering edit links component for %s: %s\n", release.ID, err) + fmt.Printf("WARN: Failed to serve edit links component for %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) @@ -199,7 +203,7 @@ func serveEditTracks(release *model.Release) http.Handler { Add: func(a, b int) int { return a + b }, }) if err != nil { - fmt.Printf("Error rendering edit tracks component for %s: %s\n", release.ID, err) + fmt.Printf("WARN: Failed to serve edit tracks component for %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) @@ -209,7 +213,7 @@ func serveAddTrack(app *model.AppState, release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { tracks, err := controller.GetTracksNotOnRelease(app.DB, release.ID) if err != nil { - fmt.Printf("WARN: Failed to pull tracks not on %s: %s\n", release.ID, err) + fmt.Printf("WARN: Failed to fetch tracks not on %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } @@ -225,7 +229,7 @@ func serveAddTrack(app *model.AppState, release *model.Release) http.Handler { Tracks: tracks, }) if err != nil { - fmt.Printf("Error rendering add tracks component for %s: %s\n", release.ID, err) + fmt.Printf("WARN: Failed to add tracks component for %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) @@ -236,7 +240,7 @@ func serveNewTrack(app *model.AppState) http.Handler { trackID := strings.Split(r.URL.Path, "/")[3] track, err := controller.GetTrack(app.DB, trackID) if err != nil { - fmt.Printf("Error rendering new track component for %s: %s\n", trackID, err) + fmt.Printf("WARN: Failed to fetch track %s: %s\n", trackID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } @@ -248,7 +252,7 @@ func serveNewTrack(app *model.AppState) http.Handler { w.Header().Set("Content-Type", "text/html") err = templates.NewTrackTemplate.Execute(w, track) if err != nil { - fmt.Printf("Error rendering new track component for %s: %s\n", track.ID, err) + fmt.Printf("WARN: Failed to serve new track component for %s: %s\n", track.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) diff --git a/admin/static/admin.css b/admin/static/admin.css index ab736f2..7e94798 100644 --- a/admin/static/admin.css +++ b/admin/static/admin.css @@ -95,23 +95,26 @@ h1, h2, h3, h4, h5, h6 { } header { - position: fixed; - left: 0; - height: 100vh; display: flex; - flex-direction: column; - width: 180px; - background-color: var(--bg-1); - box-shadow: var(--shadow-md); - - transition: background .1s ease-out, color .1s ease-out; + justify-content: space-between; + align-items: center; } nav { - height: 100%; - margin: 1em 0; + position: fixed; + top: 0; + left: 0; + width: 180px; + height: calc(100vh - 2em); + margin: 0; + padding: 1em 0; display: flex; flex-direction: column; justify-content: left; + + background-color: var(--bg-1); + box-shadow: var(--shadow-md); + transition: background .1s ease-out, color .1s ease-out; + user-select: none; } nav .icon { @@ -134,17 +137,18 @@ nav .icon img { color: var(--fg-2); line-height: 2em; font-weight: 500; - transition: color .1s, background-color .1s; + transition: color .1s ease-out, background-color .1s ease-out; } .nav-item:hover { - background: var(--bg-2); + color: var(--bg-2); + background-color: var(--fg-2); text-decoration: none; } .nav-item.active { border-left: 4px solid var(--fg-2); } .nav-item.active a { - padding-left: calc(1em - 4px); + padding-left: calc(1em - 3.5px); } nav a { padding: .2em 1em; diff --git a/admin/static/artists.css b/admin/static/artists.css new file mode 100644 index 0000000..3074874 --- /dev/null +++ b/admin/static/artists.css @@ -0,0 +1,23 @@ +.artist { + padding: .5em; + + color: var(--fg-3); + background: var(--bg-2); + box-shadow: var(--shadow-md); + border-radius: 16px; + text-align: center; + + cursor: pointer; + transition: background .1s ease-out, color .1s ease-out; +} + +.artist:hover { + background: var(--bg-1); + text-decoration: hover; +} + +.artist .artist-avatar { + width: 100%; + object-fit: cover; + border-radius: 8px; +} diff --git a/admin/static/artists.js b/admin/static/artists.js new file mode 100644 index 0000000..29eab22 --- /dev/null +++ b/admin/static/artists.js @@ -0,0 +1,7 @@ +import { hijackClickEvent } from "./admin.js"; + +document.addEventListener("readystatechange", () => { + document.querySelectorAll(".artists-group .artist").forEach(el => { + hijackClickEvent(el, el.querySelector("a.artist-name")) + }); +}); diff --git a/admin/static/edit-release.css b/admin/static/edit-release.css index b8817e0..62dbda1 100644 --- a/admin/static/edit-release.css +++ b/admin/static/edit-release.css @@ -133,10 +133,13 @@ input[type="text"] { dialog { width: min(720px, calc(100% - 2em)); padding: 2em; - border: 1px solid #101010; + border: none; border-radius: 16px; color: var(--fg-0); - background: var(--bg-0); + background-color: var(--bg-0); + box-shadow: var(--shadow-lg); + + transition: color .1s ease-out, background-color .1s ease-out; } dialog header { @@ -433,68 +436,6 @@ dialog div.dialog-actions { outline: 1px solid #808080; } -/* - * RELEASE TRACKS - */ - -#tracks .track { - margin-bottom: 1em; - padding: 1em; - display: flex; - flex-direction: column; - gap: .5em; - - border-radius: 16px; - background: var(--bg-2); - box-shadow: var(--shadow-md); - - transition: background .1s ease-out, color .1s ease-out; -} - -#tracks .track h3, -#tracks .track p { - margin: 0; -} - -#tracks h2.track-title { - margin: 0; - display: flex; - gap: .5em; -} - -#tracks h2.track-title .track-number { - opacity: .5; -} - -#tracks a:hover { - text-decoration: underline; -} - -#tracks .track-album { - margin-left: auto; - font-style: italic; - font-size: .75em; - opacity: .5; -} - -#tracks .track-album.empty { - color: #ff2020; - opacity: 1; -} - -#tracks .track-description { - font-style: italic; -} - -#tracks .track-lyrics { - max-height: 10em; - overflow-y: scroll; -} - -#tracks .track .empty { - opacity: 0.75; -} - #edittracks ul { padding: 0; list-style: none; diff --git a/admin/static/index.css b/admin/static/index.css deleted file mode 100644 index 5f64a2f..0000000 --- a/admin/static/index.css +++ /dev/null @@ -1,84 +0,0 @@ -@import url("/admin/static/release-list-item.css"); - -.artist { - padding: .5em; - - color: var(--fg-3); - background: var(--bg-2); - box-shadow: var(--shadow-md); - border-radius: 16px; - text-align: center; - - cursor: pointer; - transition: background .1s ease-out, color .1s ease-out; -} - -.artist:hover { - background: var(--bg-1); - text-decoration: hover; -} - -.artist-avatar { - width: 100%; - object-fit: cover; - border-radius: 8px; -} - -.track { - margin-bottom: 1em; - padding: 1em; - display: flex; - flex-direction: column; - gap: .5em; - - border-radius: 8px; - background: #f8f8f8f8; - border: 1px solid #808080; - - transition: background .1s ease-out, color .1s ease-out; -} - -.track p { - margin: 0; -} - -.card h2.track-title { - margin: 0; - display: flex; - flex-direction: row; - justify-content: space-between; -} - -.track-id { - width: fit-content; - font-family: "Monaspace Argon", monospace; - font-size: .8em; - font-style: italic; - line-height: 1em; - user-select: all; -} - -.track-album { - margin-left: auto; - font-style: italic; - font-size: .75em; - opacity: .5; -} - -.track-album.empty { - color: #ff2020; - opacity: 1; -} - -.track-description { - font-style: italic; -} - -.track-lyrics { - max-height: 10em; - overflow-y: scroll; -} - -.track .empty { - opacity: 0.75; -} diff --git a/admin/static/index.js b/admin/static/index.js index c6f6b58..60bdfd0 100644 --- a/admin/static/index.js +++ b/admin/static/index.js @@ -1,5 +1,3 @@ -import { hijackClickEvent } from "./admin.js"; - const newReleaseBtn = document.getElementById("create-release"); const newArtistBtn = document.getElementById("create-artist"); const newTrackBtn = document.getElementById("create-track"); @@ -74,9 +72,3 @@ newTrackBtn.addEventListener("click", event => { console.error(err); }); }); - -document.addEventListener("readystatechange", () => { - document.querySelectorAll("#artists .artist").forEach(el => { - hijackClickEvent(el, el.querySelector("a.artist-name")) - }); -}); diff --git a/admin/static/release-list-item.css b/admin/static/releases.css similarity index 83% rename from admin/static/release-list-item.css rename to admin/static/releases.css index aedd121..08dcd20 100644 --- a/admin/static/release-list-item.css +++ b/admin/static/releases.css @@ -17,7 +17,7 @@ margin: 0; } -.release-artwork { +.release .release-artwork { margin: auto 0; width: 96px; @@ -29,16 +29,16 @@ box-shadow: var(--shadow-sm); } -.release-artwork img { +.release .release-artwork img { width: 100%; aspect-ratio: 1; } -.release-title small { +.release .release-title small { opacity: .75; } -.release-links { +.release .release-links { margin: .5em 0; padding: 0; display: flex; @@ -48,13 +48,13 @@ gap: .5em; } -.release-actions { +.release .release-actions { margin-top: .5em; user-select: none; color: var(--fg-3); } -.release-actions a { +.release .release-actions a { margin-right: .3em; padding: .3em .5em; display: inline-block; @@ -66,7 +66,7 @@ transition: color .1s ease-out, background .1s ease-out; } -.release-actions a:hover { +.release .release-actions a:hover { background: var(--bg-0); color: var(--fg-3); text-decoration: none; diff --git a/admin/static/tracks.css b/admin/static/tracks.css new file mode 100644 index 0000000..c36c1b1 --- /dev/null +++ b/admin/static/tracks.css @@ -0,0 +1,127 @@ +#tracks h2.track-title { + margin: 0; + display: flex; + gap: .5em; +} + +#tracks .track { + margin-bottom: 1em; + padding: 1em; + display: flex; + flex-direction: column; + gap: .5em; + + border-radius: 16px; + background: var(--bg-2); + box-shadow: var(--shadow-md); + + transition: background .1s ease-out, color .1s ease-out; +} + +#tracks .track h3, +#tracks .track p { + margin: 0; +} + +#tracks h2.track-title { + margin: 0; + display: flex; + gap: .5em; +} + +#tracks h2.track-title .track-number { + opacity: .5; +} + +#tracks a:hover { + text-decoration: underline; +} + +#tracks .track-album { + margin-left: auto; + font-style: italic; + font-size: .75em; + opacity: .5; +} + +#tracks .track-album.empty { + color: #ff2020; + opacity: 1; +} + +#tracks .track-description { + font-style: italic; +} + +#tracks .track-lyrics { + max-height: 10em; + overflow-y: scroll; +} + +#tracks .track .empty { + opacity: 0.75; +} + + +.card h2.track-title { + margin: 0; + display: flex; + flex-direction: row; + /* + justify-content: space-between; + */ +} + +/* +.track { + margin-bottom: 1em; + padding: 1em; + display: flex; + flex-direction: column; + gap: .5em; + + border-radius: 8px; + background-color: var(--bg-2); + box-shadow: var(--shadow-md); + + transition: color .1s ease-out, background-color .1s ease-out; +} + +.track p { + margin: 0; +} + +.track-id { + width: fit-content; + font-family: "Monaspace Argon", monospace; + font-size: .8em; + font-style: italic; + line-height: 1em; + user-select: all; +} + +.track-album { + margin-left: auto; + font-style: italic; + font-size: .75em; + opacity: .5; +} + +.track-album.empty { + color: #ff2020; + opacity: 1; +} + +.track-description { + font-style: italic; +} + +.track-lyrics { + max-height: 10em; + overflow-y: scroll; +} + +.track .empty { + opacity: 0.75; +} +*/ diff --git a/admin/templates/html/artists.html b/admin/templates/html/artists.html index 7b2a464..fe21153 100644 --- a/admin/templates/html/artists.html +++ b/admin/templates/html/artists.html @@ -1,24 +1,21 @@ {{define "head"}} Artists - ari melody 💫 - + + {{end}} {{define "content"}}
-

Artists

- - + + {{if .Artists}}
- {{range $Artist := .Artists}} - + {{range .Artists}} + {{block "artist" .}}{{end}} {{end}}
{{else}} @@ -27,4 +24,5 @@
+ {{end}} diff --git a/admin/templates/html/components/artist/artist.html b/admin/templates/html/components/artist/artist.html new file mode 100644 index 0000000..86ac4cc --- /dev/null +++ b/admin/templates/html/components/artist/artist.html @@ -0,0 +1,6 @@ +{{define "artist"}} +
+ + {{.Name}} +
+{{end}} diff --git a/admin/templates/html/components/credits/addcredit.html b/admin/templates/html/components/credit/addcredit.html similarity index 100% rename from admin/templates/html/components/credits/addcredit.html rename to admin/templates/html/components/credit/addcredit.html diff --git a/admin/templates/html/components/credits/editcredits.html b/admin/templates/html/components/credit/editcredits.html similarity index 100% rename from admin/templates/html/components/credits/editcredits.html rename to admin/templates/html/components/credit/editcredits.html diff --git a/admin/templates/html/components/credits/newcredit.html b/admin/templates/html/components/credit/newcredit.html similarity index 100% rename from admin/templates/html/components/credits/newcredit.html rename to admin/templates/html/components/credit/newcredit.html diff --git a/admin/templates/html/components/links/editlinks.html b/admin/templates/html/components/link/editlinks.html similarity index 100% rename from admin/templates/html/components/links/editlinks.html rename to admin/templates/html/components/link/editlinks.html diff --git a/admin/templates/html/components/release/release-list-item.html b/admin/templates/html/components/release/release.html similarity index 100% rename from admin/templates/html/components/release/release-list-item.html rename to admin/templates/html/components/release/release.html diff --git a/admin/templates/html/components/tracks/addtrack.html b/admin/templates/html/components/track/addtrack.html similarity index 100% rename from admin/templates/html/components/tracks/addtrack.html rename to admin/templates/html/components/track/addtrack.html diff --git a/admin/templates/html/components/tracks/edittracks.html b/admin/templates/html/components/track/edittracks.html similarity index 100% rename from admin/templates/html/components/tracks/edittracks.html rename to admin/templates/html/components/track/edittracks.html diff --git a/admin/templates/html/components/tracks/newtrack.html b/admin/templates/html/components/track/newtrack.html similarity index 100% rename from admin/templates/html/components/tracks/newtrack.html rename to admin/templates/html/components/track/newtrack.html diff --git a/admin/templates/html/components/track/track.html b/admin/templates/html/components/track/track.html new file mode 100644 index 0000000..4db20a3 --- /dev/null +++ b/admin/templates/html/components/track/track.html @@ -0,0 +1,24 @@ +{{define "track"}} +
+

+ {{if .Number}} + {{.Number}} + {{end}} + {{.Title}} +

+ +

Description

+ {{if .Description}} +

{{.GetDescriptionHTML}}

+ {{else}} +

No description provided.

+ {{end}} + +

Lyrics

+ {{if .Lyrics}} +

{{.GetLyricsHTML}}

+ {{else}} +

There are no lyrics.

+ {{end}} +
+{{end}} diff --git a/admin/templates/html/edit-artist.html b/admin/templates/html/edit-artist.html index 9bb5a83..e9b829a 100644 --- a/admin/templates/html/edit-artist.html +++ b/admin/templates/html/edit-artist.html @@ -2,6 +2,7 @@ Editing {{.Artist.Name}} - ari melody 💫 + {{end}} {{define "content"}} diff --git a/admin/templates/html/edit-release.html b/admin/templates/html/edit-release.html index 9a16f65..238dd75 100644 --- a/admin/templates/html/edit-release.html +++ b/admin/templates/html/edit-release.html @@ -2,6 +2,8 @@ Editing {{.Release.Title}} - ari melody 💫 + + {{end}} {{define "content"}} @@ -154,26 +156,7 @@ >Edit {{range $i, $track := .Release.Tracks}} -
-

- {{.Add $i 1}} - {{$track.Title}} -

- -

Description

- {{if $track.Description}} -

{{$track.GetDescriptionHTML}}

- {{else}} -

No description provided.

- {{end}} - -

Lyrics

- {{if $track.Lyrics}} -

{{$track.GetLyricsHTML}}

- {{else}} -

There are no lyrics.

- {{end}} -
+ {{block "track" .}}{{end}} {{end}} diff --git a/admin/templates/html/edit-track.html b/admin/templates/html/edit-track.html index 871db6f..14f389b 100644 --- a/admin/templates/html/edit-track.html +++ b/admin/templates/html/edit-track.html @@ -2,6 +2,8 @@ Editing Track - ari melody 💫 + + {{end}} {{define "content"}} diff --git a/admin/templates/html/index.html b/admin/templates/html/index.html index 581c691..4387c31 100644 --- a/admin/templates/html/index.html +++ b/admin/templates/html/index.html @@ -1,7 +1,10 @@ {{define "head"}} Admin - ari melody 💫 - + + + + {{end}} {{define "content"}} @@ -14,26 +17,24 @@

Releases ({{.ReleaseCount}} total)

Create New + {{if .Artists}} {{range .Releases}} {{block "release" .}}{{end}} {{end}} - {{if not .Releases}} + {{else}}

There are no releases.

{{end}}
-

Artists

+

Artists ({{.ArtistCount}} total)

Create New
{{if .Artists}}
- {{range $Artist := .Artists}} - + {{range .Artists}} + {{block "artist" .}}{{end}} {{end}}
{{else}} @@ -43,30 +44,13 @@
-

Tracks

+

Tracks ({{.TrackCount}} total)

Create New

"Orphaned" tracks that have not yet been bound to a release.


- {{range $Track := .Tracks}} -
-

- {{$Track.Title}} -

- {{if $Track.Description}} -

{{$Track.GetDescriptionHTML}}

- {{else}} -

No description provided.

- {{end}} - {{if $Track.Lyrics}} -

{{$Track.GetLyricsHTML}}

- {{else}} -

There are no lyrics.

- {{end}} -
- {{end}} - {{if not .Artists}} -

There are no artists.

+ {{range .Tracks}} + {{block "track" .}}{{end}} {{end}}
@@ -74,5 +58,6 @@ + {{end}} diff --git a/admin/templates/html/logs.html b/admin/templates/html/logs.html index 94146a7..1faf1f1 100644 --- a/admin/templates/html/logs.html +++ b/admin/templates/html/logs.html @@ -55,7 +55,7 @@ {{range .Logs}} - + {{prettyTime .CreatedAt}} {{parseLevel .Level}} {{titleCase .Type}} diff --git a/admin/templates/html/releases.html b/admin/templates/html/releases.html index ec4443f..cf32614 100644 --- a/admin/templates/html/releases.html +++ b/admin/templates/html/releases.html @@ -1,19 +1,24 @@ {{define "head"}} Releases - ari melody 💫 - + + {{end}} {{define "content"}}
-
+

Releases

Create New +
+ + {{if .Releases}} +
+ {{range .Releases}} + {{block "release" .}}{{end}} + {{end}}
- {{range .Releases}} - {{block "release" .}}{{end}} - {{end}} - {{if not .Releases}} + {{else}}

There are no releases.

{{end}}
diff --git a/admin/templates/html/tracks.html b/admin/templates/html/tracks.html index 196b41c..8e9496b 100644 --- a/admin/templates/html/tracks.html +++ b/admin/templates/html/tracks.html @@ -1,39 +1,36 @@ {{define "head"}} Releases - ari melody 💫 - + + {{end}} {{define "content"}}
-

Releases

- -
-

Tracks

+
+

Tracks

Create New +
+ +
+ {{range $Track := .Tracks}} +
+

+ {{$Track.Title}} +

+ {{if $Track.Description}} +

{{$Track.GetDescriptionHTML}}

+ {{else}} +

No description provided.

+ {{end}} + {{if $Track.Lyrics}} +

{{$Track.GetLyricsHTML}}

+ {{else}} +

There are no lyrics.

+ {{end}} +
+ {{end}}
-

"Orphaned" tracks that have not yet been bound to a release.

-
- {{range $Track := .Tracks}} -
-

- {{$Track.Title}} -

- {{if $Track.Description}} -

{{$Track.GetDescriptionHTML}}

- {{else}} -

No description provided.

- {{end}} - {{if $Track.Lyrics}} -

{{$Track.GetLyricsHTML}}

- {{else}} -

There are no lyrics.

- {{end}} -
- {{end}} - {{if not .Artists}} -

There are no artists.

- {{end}}
diff --git a/admin/templates/templates.go b/admin/templates/templates.go index 10005ca..51b1376 100644 --- a/admin/templates/templates.go +++ b/admin/templates/templates.go @@ -50,33 +50,34 @@ var editArtistHTML string //go:embed "html/edit-track.html" var editTrackHTML string -//go:embed "html/components/credits/newcredit.html" +//go:embed "html/components/credit/newcredit.html" var componentNewCreditHTML string -//go:embed "html/components/credits/addcredit.html" +//go:embed "html/components/credit/addcredit.html" var componentAddCreditHTML string -//go:embed "html/components/credits/editcredits.html" +//go:embed "html/components/credit/editcredits.html" var componentEditCreditsHTML string -//go:embed "html/components/links/editlinks.html" +//go:embed "html/components/link/editlinks.html" var componentEditLinksHTML string -//go:embed "html/components/release/release-list-item.html" -var componentReleaseListItemHTML string +//go:embed "html/components/release/release.html" +var componentReleaseHTML string +//go:embed "html/components/artist/artist.html" +var componentArtistHTML string +//go:embed "html/components/track/track.html" +var componentTrackHTML string -//go:embed "html/components/tracks/newtrack.html" +//go:embed "html/components/track/newtrack.html" var componentNewTrackHTML string -//go:embed "html/components/tracks/addtrack.html" +//go:embed "html/components/track/addtrack.html" var componentAddTrackHTML string -//go:embed "html/components/tracks/edittracks.html" +//go:embed "html/components/track/edittracks.html" var componentEditTracksHTML string var BaseTemplate = template.Must( template.New("base").Funcs( template.FuncMap{ - "hasPrefix": func(s string, prefix string) bool { - fmt.Printf("does \"%s\" start with \"%s\"?\n", s, prefix) - return strings.HasPrefix(s, prefix) - }, + "hasPrefix": strings.HasPrefix, }, ).Parse(strings.Join([]string{ layoutHTML, @@ -86,10 +87,14 @@ var BaseTemplate = template.Must( var IndexTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse( strings.Join([]string{ indexHTML, - componentReleaseListItemHTML, + componentReleaseHTML, + componentArtistHTML, + componentTrackHTML, }, "\n"), )) + + var LoginTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(loginHTML)) var LoginTOTPTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(loginTotpHTML)) var RegisterTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(registerHTML)) @@ -98,51 +103,54 @@ var AccountTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(ed var TOTPSetupTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(totpSetupHTML)) var TOTPConfirmTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(totpConfirmHTML)) + + var LogsTemplate = template.Must(template.Must(BaseTemplate.Clone()).Funcs(template.FuncMap{ - "parseLevel": func(level log.LogLevel) string { - switch level { - case log.LEVEL_INFO: - return "INFO" - case log.LEVEL_WARN: - return "WARN" - } - return fmt.Sprintf("%d?", level) - }, - "titleCase": func(logType string) string { - runes := []rune(logType) - for i, r := range runes { - if (i == 0 || runes[i - 1] == ' ') && r >= 'a' && r <= 'z' { - runes[i] = r + ('A' - 'a') - } - } - return string(runes) - }, - "lower": func(str string) string { return strings.ToLower(str) }, - "prettyTime": func(t time.Time) string { - // return t.Format("2006-01-02 15:04:05") - // return t.Format("15:04:05, 2 Jan 2006") - return t.Format("02 Jan 2006, 15:04:05") - }, + "parseLevel": parseLevel, + "titleCase": titleCase, + "toLower": toLower, + "prettyTime": prettyTime, }).Parse(logsHTML)) + + var ReleasesTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse( strings.Join([]string{ releasesHTML, - componentReleaseListItemHTML, + componentReleaseHTML, + }, "\n"), +)) +var ArtistsTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse( + strings.Join([]string{ + artistsHTML, + componentArtistHTML, + }, "\n"), +)) +var TracksTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse( + strings.Join([]string{ + tracksHTML, + componentTrackHTML, }, "\n"), )) -var ArtistsTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(artistsHTML)) -var TracksTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(tracksHTML)) -var EditReleaseTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(editReleaseHTML)) + + +var EditReleaseTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse( + strings.Join([]string{ + editReleaseHTML, + componentTrackHTML, + }, "\n"), +)) var EditArtistTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(editArtistHTML)) var EditTrackTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse( strings.Join([]string{ editTrackHTML, - componentReleaseListItemHTML, + componentReleaseHTML, }, "\n"), )) + + var EditCreditsTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentEditCreditsHTML)) var AddCreditTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentAddCreditHTML)) var NewCreditTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentNewCreditHTML)) @@ -152,3 +160,32 @@ var EditLinksTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse( var EditTracksTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentEditTracksHTML)) var AddTrackTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentAddTrackHTML)) var NewTrackTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentNewTrackHTML)) + + + +func parseLevel(level log.LogLevel) string { + switch level { + case log.LEVEL_INFO: + return "INFO" + case log.LEVEL_WARN: + return "WARN" + } + return fmt.Sprintf("%d?", level) +} +func titleCase(logType string) string { + runes := []rune(logType) + for i, r := range runes { + if (i == 0 || runes[i - 1] == ' ') && r >= 'a' && r <= 'z' { + runes[i] = r + ('A' - 'a') + } + } + return string(runes) +} +func toLower(str string) string { + return strings.ToLower(str) +} +func prettyTime(t time.Time) string { + // return t.Format("2006-01-02 15:04:05") + // return t.Format("15:04:05, 2 Jan 2006") + return t.Format("02 Jan 2006, 15:04:05") +} diff --git a/admin/trackhttp.go b/admin/trackhttp.go index 9dfd119..bcb5220 100644 --- a/admin/trackhttp.go +++ b/admin/trackhttp.go @@ -12,11 +12,46 @@ import ( func serveTracks(app *model.AppState) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value("session").(*model.Session) + slices := strings.Split(strings.TrimPrefix(r.URL.Path, "/tracks")[1:], "/") - id := slices[0] - track, err := controller.GetTrack(app.DB, id) + trackID := slices[0] + + if len(trackID) > 0 { + serveTrack(app, trackID).ServeHTTP(w, r) + return + } + + tracks, err := controller.GetAllTracks(app.DB) if err != nil { - fmt.Printf("Error rendering admin track page for %s: %s\n", id, err) + fmt.Printf("WARN: Failed to fetch tracks: %s\n", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + type TracksResponse struct { + adminPageData + Tracks []*model.Track + } + + err = templates.TracksTemplate.Execute(w, TracksResponse{ + adminPageData: adminPageData{ Path: r.URL.Path, Session: session }, + Tracks: tracks, + }) + if err != nil { + fmt.Printf("WARN: Failed to serve admin tracks page: %s\n", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } + }) +} + +func serveTrack(app *model.AppState, trackID string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value("session").(*model.Session) + + track, err := controller.GetTrack(app.DB, trackID) + if err != nil { + fmt.Printf("WARN: Failed to serve admin track page for %s: %s\n", trackID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } @@ -27,7 +62,7 @@ func serveTracks(app *model.AppState) http.Handler { releases, err := controller.GetTrackReleases(app.DB, track.ID, true) if err != nil { - fmt.Printf("FATAL: Failed to pull releases for %s: %s\n", id, err) + fmt.Printf("WARN: Failed to fetch releases for track %s: %s\n", trackID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } @@ -38,17 +73,14 @@ func serveTracks(app *model.AppState) http.Handler { Releases []*model.Release } - session := r.Context().Value("session").(*model.Session) - err = templates.EditTrackTemplate.Execute(w, TrackResponse{ adminPageData: adminPageData{ Path: r.URL.Path, Session: session }, Track: track, Releases: releases, }) if err != nil { - fmt.Printf("Error rendering admin track page for %s: %s\n", id, err) + fmt.Printf("WARN: Failed to serve admin track page for %s: %s\n", trackID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) } - diff --git a/controller/artist.go b/controller/artist.go index f086778..adcdbc5 100644 --- a/controller/artist.go +++ b/controller/artist.go @@ -29,6 +29,11 @@ func GetAllArtists(db *sqlx.DB) ([]*model.Artist, error) { return artists, nil } +func GetArtistCount(db *sqlx.DB) (int, error) { + var count int + err := db.Get(&count, "SELECT count(*) FROM artist") + return count, err +} func GetArtistsNotOnRelease(db *sqlx.DB, releaseID string) ([]*model.Artist, error) { var artists = []*model.Artist{} diff --git a/controller/release.go b/controller/release.go index c729c4f..a22d157 100644 --- a/controller/release.go +++ b/controller/release.go @@ -99,7 +99,7 @@ func GetAllReleases(db *sqlx.DB, onlyVisible bool, limit int, full bool) ([]*mod return releases, nil } -func GetReleasesCount(db *sqlx.DB, onlyVisible bool) (int, error) { +func GetReleaseCount(db *sqlx.DB, onlyVisible bool) (int, error) { query := "SELECT count(*) FROM musicrelease" if onlyVisible { query += " WHERE visible=true" diff --git a/controller/track.go b/controller/track.go index ee7581c..27f4afc 100644 --- a/controller/track.go +++ b/controller/track.go @@ -29,6 +29,11 @@ func GetAllTracks(db *sqlx.DB) ([]*model.Track, error) { return tracks, nil } +func GetTrackCount(db *sqlx.DB) (int, error) { + var count int + err := db.Get(&count, "SELECT count(*) FROM musictrack") + return count, err +} func GetOrphanTracks(db *sqlx.DB) ([]*model.Track, error) { var tracks = []*model.Track{} diff --git a/model/release.go b/model/release.go index e64317b..46f4460 100644 --- a/model/release.go +++ b/model/release.go @@ -39,7 +39,7 @@ const ( // GETTERS func (release Release) GetDescriptionHTML() template.HTML { - return template.HTML(strings.Replace(release.Description, "\n", "
", -1)) + return template.HTML(strings.ReplaceAll(release.Description, "\n", "
")) } func (release Release) TextReleaseDate() string { diff --git a/model/track.go b/model/track.go index deaf086..b94117e 100644 --- a/model/track.go +++ b/model/track.go @@ -13,16 +13,16 @@ type ( Lyrics string `json:"lyrics" db:"lyrics"` PreviewURL string `json:"previewURL" db:"preview_url"` - Number int + Number int `json:"-"` } ) func (track Track) GetDescriptionHTML() template.HTML { - return template.HTML(strings.Replace(track.Description, "\n", "
", -1)) + return template.HTML(strings.ReplaceAll(track.Description, "\n", "
")) } func (track Track) GetLyricsHTML() template.HTML { - return template.HTML(strings.Replace(track.Lyrics, "\n", "
", -1)) + return template.HTML(strings.ReplaceAll(track.Lyrics, "\n", "
")) } // this function is stupid and i hate that i need it diff --git a/public/style/music-gateway.css b/public/style/music-gateway.css index 1c10e0b..1f15f74 100644 --- a/public/style/music-gateway.css +++ b/public/style/music-gateway.css @@ -333,7 +333,7 @@ ul#links a { background-color: #fff; text-align: center; text-decoration: none; - transition: filter .1s,-webkit-filter .1s + transition: filter .1s ease-out, -webkit-filter .1s ease-out; } #buylink {