add artists/tracks pages; more components; css cleanup

This commit is contained in:
ari melody 2025-10-21 18:39:38 +01:00
parent 065a34a744
commit b0dd87cad3
Signed by: ari
GPG key ID: CF99829C92678188
37 changed files with 498 additions and 354 deletions

View file

@ -12,22 +12,57 @@ import (
func serveArtists(app *model.AppState) http.Handler { func serveArtists(app *model.AppState) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 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:], "/") slices := strings.Split(strings.TrimPrefix(r.URL.Path, "/artists")[1:], "/")
id := slices[0] artistID := slices[0]
artist, err := controller.GetArtist(app.DB, id)
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 err != nil {
if artist == nil { if artist == nil {
http.NotFound(w, r) http.NotFound(w, r)
return 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) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
credits, err := controller.GetArtistCredits(app.DB, artist.ID, true) credits, err := controller.GetArtistCredits(app.DB, artist.ID, true)
if err != nil { 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) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
@ -38,17 +73,14 @@ func serveArtists(app *model.AppState) http.Handler {
Credits []*model.Credit Credits []*model.Credit
} }
session := r.Context().Value("session").(*model.Session)
err = templates.EditArtistTemplate.Execute(w, ArtistResponse{ err = templates.EditArtistTemplate.Execute(w, ArtistResponse{
adminPageData: adminPageData{ Path: r.URL.Path, Session: session }, adminPageData: adminPageData{ Path: r.URL.Path, Session: session },
Artist: artist, Artist: artist,
Credits: credits, Credits: credits,
}) })
if err != nil { 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) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
} }
}) })
} }

View file

@ -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("/releases/", requireAccount(serveReleases(app))) mux.Handle("/releases/", requireAccount(serveReleases(app)))
mux.Handle("/artists", requireAccount(serveArtists(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("/tracks/", requireAccount(serveTracks(app)))
mux.Handle("/static/", staticHandler()) mux.Handle("/static/", staticHandler())
@ -79,7 +81,7 @@ func AdminIndexHandler(app *model.AppState) http.Handler {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
releaseCount, err := controller.GetReleasesCount(app.DB, false) releaseCount, err := controller.GetReleaseCount(app.DB, false)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to pull releases count: %s\n", err) fmt.Fprintf(os.Stderr, "WARN: Failed to pull releases count: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 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) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return 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) tracks, err := controller.GetOrphanTracks(app.DB)
if err != nil { if err != nil {
@ -99,13 +107,21 @@ func AdminIndexHandler(app *model.AppState) http.Handler {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return 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 { type IndexData struct {
adminPageData adminPageData
Releases []*model.Release Releases []*model.Release
ReleaseCount int ReleaseCount int
Artists []*model.Artist Artists []*model.Artist
ArtistCount int
Tracks []*model.Track Tracks []*model.Track
TrackCount int
} }
err = templates.IndexTemplate.Execute(w, IndexData{ err = templates.IndexTemplate.Execute(w, IndexData{
@ -113,7 +129,9 @@ func AdminIndexHandler(app *model.AppState) http.Handler {
Releases: releases, Releases: releases,
ReleaseCount: releaseCount, ReleaseCount: releaseCount,
Artists: artists, Artists: artists,
ArtistCount: artistCount,
Tracks: tracks, Tracks: tracks,
TrackCount: trackCount,
}) })
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to render admin index: %s\n", err) fmt.Fprintf(os.Stderr, "WARN: Failed to render admin index: %s\n", err)

View file

@ -13,6 +13,8 @@ import (
func serveReleases(app *model.AppState) http.Handler { func serveReleases(app *model.AppState) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 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:], "/") slices := strings.Split(strings.TrimPrefix(r.URL.Path, "/releases")[1:], "/")
releaseID := slices[0] releaseID := slices[0]
@ -26,8 +28,6 @@ func serveReleases(app *model.AppState) http.Handler {
return return
} }
session := r.Context().Value("session").(*model.Session)
type ReleasesData struct { type ReleasesData struct {
adminPageData adminPageData
Releases []*model.Release Releases []*model.Release
@ -35,7 +35,7 @@ func serveReleases(app *model.AppState) http.Handler {
releases, err := controller.GetAllReleases(app.DB, false, 0, true) releases, err := controller.GetAllReleases(app.DB, false, 0, true)
if err != nil { 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) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
@ -48,7 +48,7 @@ func serveReleases(app *model.AppState) http.Handler {
Releases: releases, Releases: releases,
}) })
if err != nil { 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) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
@ -65,7 +65,7 @@ func serveRelease(app *model.AppState, releaseID string, action string) http.Han
http.NotFound(w, r) http.NotFound(w, r)
return 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) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
@ -103,12 +103,16 @@ func serveRelease(app *model.AppState, releaseID string, action string) http.Han
Release *model.Release Release *model.Release
} }
for i, track := range release.Tracks {
track.Number = i + 1
}
err = templates.EditReleaseTemplate.Execute(w, ReleaseResponse{ err = templates.EditReleaseTemplate.Execute(w, ReleaseResponse{
adminPageData: adminPageData{ Path: r.URL.Path, Session: session }, adminPageData: adminPageData{ Path: r.URL.Path, Session: session },
Release: release, Release: release,
}) })
if err != nil { 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) 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") w.Header().Set("Content-Type", "text/html")
err := templates.EditCreditsTemplate.Execute(w, release) err := templates.EditCreditsTemplate.Execute(w, release)
if err != nil { 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) 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) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
artists, err := controller.GetArtistsNotOnRelease(app.DB, release.ID) artists, err := controller.GetArtistsNotOnRelease(app.DB, release.ID)
if err != nil { 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) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
@ -145,7 +149,7 @@ func serveAddCredit(app *model.AppState, release *model.Release) http.Handler {
Artists: artists, Artists: artists,
}) })
if err != nil { 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) 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] artistID := strings.Split(r.URL.Path, "/")[3]
artist, err := controller.GetArtist(app.DB, artistID) artist, err := controller.GetArtist(app.DB, artistID)
if err != nil { 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) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
@ -168,7 +172,7 @@ func serveNewCredit(app *model.AppState) http.Handler {
w.Header().Set("Content-Type", "text/html") w.Header().Set("Content-Type", "text/html")
err = templates.NewCreditTemplate.Execute(w, artist) err = templates.NewCreditTemplate.Execute(w, artist)
if err != nil { 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) 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 { func serveEditLinks(release *model.Release) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html") w.Header().Set("Content-Type", "text/html")
err := templates.EditCreditsTemplate.Execute(w, release) err := templates.EditLinksTemplate.Execute(w, release)
if err != nil { 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) 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 }, Add: func(a, b int) int { return a + b },
}) })
if err != nil { 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) 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) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tracks, err := controller.GetTracksNotOnRelease(app.DB, release.ID) tracks, err := controller.GetTracksNotOnRelease(app.DB, release.ID)
if err != nil { 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) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
@ -225,7 +229,7 @@ func serveAddTrack(app *model.AppState, release *model.Release) http.Handler {
Tracks: tracks, Tracks: tracks,
}) })
if err != nil { 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) 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] trackID := strings.Split(r.URL.Path, "/")[3]
track, err := controller.GetTrack(app.DB, trackID) track, err := controller.GetTrack(app.DB, trackID)
if err != nil { 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) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
@ -248,7 +252,7 @@ func serveNewTrack(app *model.AppState) http.Handler {
w.Header().Set("Content-Type", "text/html") w.Header().Set("Content-Type", "text/html")
err = templates.NewTrackTemplate.Execute(w, track) err = templates.NewTrackTemplate.Execute(w, track)
if err != nil { 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) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
} }
}) })

View file

@ -95,23 +95,26 @@ h1, h2, h3, h4, h5, h6 {
} }
header { header {
position: fixed;
left: 0;
height: 100vh;
display: flex; display: flex;
flex-direction: column; justify-content: space-between;
width: 180px; align-items: center;
background-color: var(--bg-1);
box-shadow: var(--shadow-md);
transition: background .1s ease-out, color .1s ease-out;
} }
nav { nav {
height: 100%; position: fixed;
margin: 1em 0; top: 0;
left: 0;
width: 180px;
height: calc(100vh - 2em);
margin: 0;
padding: 1em 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: left; 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; user-select: none;
} }
nav .icon { nav .icon {
@ -134,17 +137,18 @@ nav .icon img {
color: var(--fg-2); color: var(--fg-2);
line-height: 2em; line-height: 2em;
font-weight: 500; font-weight: 500;
transition: color .1s, background-color .1s; transition: color .1s ease-out, background-color .1s ease-out;
} }
.nav-item:hover { .nav-item:hover {
background: var(--bg-2); color: var(--bg-2);
background-color: var(--fg-2);
text-decoration: none; text-decoration: none;
} }
.nav-item.active { .nav-item.active {
border-left: 4px solid var(--fg-2); border-left: 4px solid var(--fg-2);
} }
.nav-item.active a { .nav-item.active a {
padding-left: calc(1em - 4px); padding-left: calc(1em - 3.5px);
} }
nav a { nav a {
padding: .2em 1em; padding: .2em 1em;

23
admin/static/artists.css Normal file
View file

@ -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;
}

7
admin/static/artists.js Normal file
View file

@ -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"))
});
});

View file

@ -133,10 +133,13 @@ input[type="text"] {
dialog { dialog {
width: min(720px, calc(100% - 2em)); width: min(720px, calc(100% - 2em));
padding: 2em; padding: 2em;
border: 1px solid #101010; border: none;
border-radius: 16px; border-radius: 16px;
color: var(--fg-0); 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 { dialog header {
@ -433,68 +436,6 @@ dialog div.dialog-actions {
outline: 1px solid #808080; 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 { #edittracks ul {
padding: 0; padding: 0;
list-style: none; list-style: none;

View file

@ -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;
}

View file

@ -1,5 +1,3 @@
import { hijackClickEvent } from "./admin.js";
const newReleaseBtn = document.getElementById("create-release"); const newReleaseBtn = document.getElementById("create-release");
const newArtistBtn = document.getElementById("create-artist"); const newArtistBtn = document.getElementById("create-artist");
const newTrackBtn = document.getElementById("create-track"); const newTrackBtn = document.getElementById("create-track");
@ -74,9 +72,3 @@ newTrackBtn.addEventListener("click", event => {
console.error(err); console.error(err);
}); });
}); });
document.addEventListener("readystatechange", () => {
document.querySelectorAll("#artists .artist").forEach(el => {
hijackClickEvent(el, el.querySelector("a.artist-name"))
});
});

View file

@ -17,7 +17,7 @@
margin: 0; margin: 0;
} }
.release-artwork { .release .release-artwork {
margin: auto 0; margin: auto 0;
width: 96px; width: 96px;
@ -29,16 +29,16 @@
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
} }
.release-artwork img { .release .release-artwork img {
width: 100%; width: 100%;
aspect-ratio: 1; aspect-ratio: 1;
} }
.release-title small { .release .release-title small {
opacity: .75; opacity: .75;
} }
.release-links { .release .release-links {
margin: .5em 0; margin: .5em 0;
padding: 0; padding: 0;
display: flex; display: flex;
@ -48,13 +48,13 @@
gap: .5em; gap: .5em;
} }
.release-actions { .release .release-actions {
margin-top: .5em; margin-top: .5em;
user-select: none; user-select: none;
color: var(--fg-3); color: var(--fg-3);
} }
.release-actions a { .release .release-actions a {
margin-right: .3em; margin-right: .3em;
padding: .3em .5em; padding: .3em .5em;
display: inline-block; display: inline-block;
@ -66,7 +66,7 @@
transition: color .1s ease-out, background .1s ease-out; transition: color .1s ease-out, background .1s ease-out;
} }
.release-actions a:hover { .release .release-actions a:hover {
background: var(--bg-0); background: var(--bg-0);
color: var(--fg-3); color: var(--fg-3);
text-decoration: none; text-decoration: none;

127
admin/static/tracks.css Normal file
View file

@ -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;
}
*/

View file

@ -1,24 +1,21 @@
{{define "head"}} {{define "head"}}
<title>Artists - ari melody 💫</title> <title>Artists - ari melody 💫</title>
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon"> <link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
<link rel="stylesheet" href="/admin/static/index.css"> <link rel="stylesheet" href="/admin/static/admin.css">
<link rel="stylesheet" href="/admin/static/artists.css">
{{end}} {{end}}
{{define "content"}} {{define "content"}}
<main> <main>
<h1>Artists</h1> <header>
<h1><a href="/admin/artists/">Artists</a></h2>
<div class="card-header">
<h2><a href="/admin/artists/">Artists</a></h2>
<a class="button new" id="create-artist">Create New</a> <a class="button new" id="create-artist">Create New</a>
</div> </header>
{{if .Artists}} {{if .Artists}}
<div class="artists-group"> <div class="artists-group">
{{range $Artist := .Artists}} {{range .Artists}}
<div class="artist"> {{block "artist" .}}{{end}}
<img src="{{$Artist.GetAvatar}}" alt="" width="64" loading="lazy" class="artist-avatar">
<a href="/admin/artists/{{$Artist.ID}}" class="artist-name">{{$Artist.Name}}</a>
</div>
{{end}} {{end}}
</div> </div>
{{else}} {{else}}
@ -27,4 +24,5 @@
</main> </main>
<script type="module" src="/admin/static/admin.js"></script> <script type="module" src="/admin/static/admin.js"></script>
<script type="module" src="/admin/static/artists.js"></script>
{{end}} {{end}}

View file

@ -0,0 +1,6 @@
{{define "artist"}}
<div class="artist">
<img src="{{.GetAvatar}}" alt="" width="64" loading="lazy" class="artist-avatar">
<a href="/admin/artists/{{.ID}}" class="artist-name">{{.Name}}</a>
</div>
{{end}}

View file

@ -0,0 +1,24 @@
{{define "track"}}
<div class="track" data-id="{{.ID}}">
<h2 class="track-title">
{{if .Number}}
<span class="track-number">{{.Number}}</span>
{{end}}
<a href="/admin/tracks/{{.ID}}">{{.Title}}</a>
</h2>
<h3>Description</h3>
{{if .Description}}
<p class="track-description">{{.GetDescriptionHTML}}</p>
{{else}}
<p class="track-description empty">No description provided.</p>
{{end}}
<h3>Lyrics</h3>
{{if .Lyrics}}
<p class="track-lyrics">{{.GetLyricsHTML}}</p>
{{else}}
<p class="track-lyrics empty">There are no lyrics.</p>
{{end}}
</div>
{{end}}

View file

@ -2,6 +2,7 @@
<title>Editing {{.Artist.Name}} - ari melody 💫</title> <title>Editing {{.Artist.Name}} - ari melody 💫</title>
<link rel="shortcut icon" href="{{.Artist.GetAvatar}}" type="image/x-icon"> <link rel="shortcut icon" href="{{.Artist.GetAvatar}}" type="image/x-icon">
<link rel="stylesheet" href="/admin/static/edit-artist.css"> <link rel="stylesheet" href="/admin/static/edit-artist.css">
<link rel="stylesheet" href="/admin/static/artists.css">
{{end}} {{end}}
{{define "content"}} {{define "content"}}

View file

@ -2,6 +2,8 @@
<title>Editing {{.Release.Title}} - ari melody 💫</title> <title>Editing {{.Release.Title}} - ari melody 💫</title>
<link rel="shortcut icon" href="{{.Release.GetArtwork}}" type="image/x-icon"> <link rel="shortcut icon" href="{{.Release.GetArtwork}}" type="image/x-icon">
<link rel="stylesheet" href="/admin/static/edit-release.css"> <link rel="stylesheet" href="/admin/static/edit-release.css">
<link rel="stylesheet" href="/admin/static/releases.css">
<link rel="stylesheet" href="/admin/static/tracks.css">
{{end}} {{end}}
{{define "content"}} {{define "content"}}
@ -154,26 +156,7 @@
>Edit</a> >Edit</a>
</div> </div>
{{range $i, $track := .Release.Tracks}} {{range $i, $track := .Release.Tracks}}
<div class="track" data-id="{{$track.ID}}"> {{block "track" .}}{{end}}
<h2 class="track-title">
<span class="track-number">{{.Add $i 1}}</span>
<a href="/admin/tracks/{{$track.ID}}">{{$track.Title}}</a>
</h2>
<h3>Description</h3>
{{if $track.Description}}
<p class="track-description">{{$track.GetDescriptionHTML}}</p>
{{else}}
<p class="track-description empty">No description provided.</p>
{{end}}
<h3>Lyrics</h3>
{{if $track.Lyrics}}
<p class="track-lyrics">{{$track.GetLyricsHTML}}</p>
{{else}}
<p class="track-lyrics empty">There are no lyrics.</p>
{{end}}
</div>
{{end}} {{end}}
</div> </div>

View file

@ -2,6 +2,8 @@
<title>Editing Track - ari melody 💫</title> <title>Editing Track - ari melody 💫</title>
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon"> <link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
<link rel="stylesheet" href="/admin/static/edit-track.css"> <link rel="stylesheet" href="/admin/static/edit-track.css">
<link rel="stylesheet" href="/admin/static/tracks.css">
<link rel="stylesheet" href="/admin/static/releases.css">
{{end}} {{end}}
{{define "content"}} {{define "content"}}

View file

@ -1,7 +1,10 @@
{{define "head"}} {{define "head"}}
<title>Admin - ari melody 💫</title> <title>Admin - ari melody 💫</title>
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon"> <link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
<link rel="stylesheet" href="/admin/static/index.css"> <link rel="stylesheet" href="/admin/static/admin.css">
<link rel="stylesheet" href="/admin/static/releases.css">
<link rel="stylesheet" href="/admin/static/artists.css">
<link rel="stylesheet" href="/admin/static/tracks.css">
{{end}} {{end}}
{{define "content"}} {{define "content"}}
@ -14,26 +17,24 @@
<h2><a href="/admin/releases/">Releases</a> <small>({{.ReleaseCount}} total)</small></h2> <h2><a href="/admin/releases/">Releases</a> <small>({{.ReleaseCount}} total)</small></h2>
<a class="button new" id="create-release">Create New</a> <a class="button new" id="create-release">Create New</a>
</div> </div>
{{if .Artists}}
{{range .Releases}} {{range .Releases}}
{{block "release" .}}{{end}} {{block "release" .}}{{end}}
{{end}} {{end}}
{{if not .Releases}} {{else}}
<p>There are no releases.</p> <p>There are no releases.</p>
{{end}} {{end}}
</div> </div>
<div class="card" id="artists"> <div class="card" id="artists">
<div class="card-header"> <div class="card-header">
<h2><a href="/admin/artists/">Artists</a></h2> <h2><a href="/admin/artists/">Artists</a> <small>({{.ArtistCount}} total)</small></h2>
<a class="button new" id="create-artist">Create New</a> <a class="button new" id="create-artist">Create New</a>
</div> </div>
{{if .Artists}} {{if .Artists}}
<div class="artists-group"> <div class="artists-group">
{{range $Artist := .Artists}} {{range .Artists}}
<div class="artist"> {{block "artist" .}}{{end}}
<img src="{{$Artist.GetAvatar}}" alt="" width="64" loading="lazy" class="artist-avatar">
<a href="/admin/artists/{{$Artist.ID}}" class="artist-name">{{$Artist.Name}}</a>
</div>
{{end}} {{end}}
</div> </div>
{{else}} {{else}}
@ -43,30 +44,13 @@
<div class="card" id="tracks"> <div class="card" id="tracks">
<div class="card-header"> <div class="card-header">
<h2><a href="/admin/tracks/">Tracks</a></h2> <h2><a href="/admin/tracks/">Tracks</a> <small>({{.TrackCount}} total)</small></h2>
<a class="button new" id="create-track">Create New</a> <a class="button new" id="create-track">Create New</a>
</div> </div>
<p><em>"Orphaned" tracks that have not yet been bound to a release.</em></p> <p><em>"Orphaned" tracks that have not yet been bound to a release.</em></p>
<br> <br>
{{range $Track := .Tracks}} {{range .Tracks}}
<div class="track"> {{block "track" .}}{{end}}
<h2 class="track-title">
<a href="/admin/tracks/{{$Track.ID}}">{{$Track.Title}}</a>
</h2>
{{if $Track.Description}}
<p class="track-description">{{$Track.GetDescriptionHTML}}</p>
{{else}}
<p class="track-description empty">No description provided.</p>
{{end}}
{{if $Track.Lyrics}}
<p class="track-lyrics">{{$Track.GetLyricsHTML}}</p>
{{else}}
<p class="track-lyrics empty">There are no lyrics.</p>
{{end}}
</div>
{{end}}
{{if not .Artists}}
<p>There are no artists.</p>
{{end}} {{end}}
</div> </div>
</div> </div>
@ -74,5 +58,6 @@
</main> </main>
<script type="module" src="/admin/static/admin.js"></script> <script type="module" src="/admin/static/admin.js"></script>
<script type="module" src="/admin/static/artists.js"></script>
<script type="module" src="/admin/static/index.js"></script> <script type="module" src="/admin/static/index.js"></script>
{{end}} {{end}}

View file

@ -55,7 +55,7 @@
</thead> </thead>
<tbody> <tbody>
{{range .Logs}} {{range .Logs}}
<tr class="log {{lower (parseLevel .Level)}}"> <tr class="log {{toLower (parseLevel .Level)}}">
<td class="log-time">{{prettyTime .CreatedAt}}</td> <td class="log-time">{{prettyTime .CreatedAt}}</td>
<td class="log-level">{{parseLevel .Level}}</td> <td class="log-level">{{parseLevel .Level}}</td>
<td class="log-type">{{titleCase .Type}}</td> <td class="log-type">{{titleCase .Type}}</td>

View file

@ -1,19 +1,24 @@
{{define "head"}} {{define "head"}}
<title>Releases - ari melody 💫</title> <title>Releases - ari melody 💫</title>
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon"> <link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
<link rel="stylesheet" href="/admin/static/index.css"> <link rel="stylesheet" href="/admin/static/admin.css">
<link rel="stylesheet" href="/admin/static/releases.css">
{{end}} {{end}}
{{define "content"}} {{define "content"}}
<main> <main>
<div class="card-header"> <header>
<h1><a href="/admin/releases/">Releases</a></h1> <h1><a href="/admin/releases/">Releases</a></h1>
<a class="button new" id="create-release">Create New</a> <a class="button new" id="create-release">Create New</a>
</header>
{{if .Releases}}
<div id="releases">
{{range .Releases}}
{{block "release" .}}{{end}}
{{end}}
</div> </div>
{{range .Releases}} {{else}}
{{block "release" .}}{{end}}
{{end}}
{{if not .Releases}}
<p>There are no releases.</p> <p>There are no releases.</p>
{{end}} {{end}}
</main> </main>

View file

@ -1,39 +1,36 @@
{{define "head"}} {{define "head"}}
<title>Releases - ari melody 💫</title> <title>Releases - ari melody 💫</title>
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon"> <link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
<link rel="stylesheet" href="/admin/static/index.css"> <link rel="stylesheet" href="/admin/static/admin.css">
<link rel="stylesheet" href="/admin/static/tracks.css">
{{end}} {{end}}
{{define "content"}} {{define "content"}}
<main> <main>
<h1>Releases</h1> <header>
<h1><a href="/admin/tracks/">Tracks</a></h1>
<div class="card-header">
<h2><a href="/admin/tracks/">Tracks</a></h2>
<a class="button new" id="create-track">Create New</a> <a class="button new" id="create-track">Create New</a>
</header>
<div id="tracks">
{{range $Track := .Tracks}}
<div class="track">
<h2 class="track-title">
<a href="/admin/tracks/{{$Track.ID}}">{{$Track.Title}}</a>
</h2>
{{if $Track.Description}}
<p class="track-description">{{$Track.GetDescriptionHTML}}</p>
{{else}}
<p class="track-description empty">No description provided.</p>
{{end}}
{{if $Track.Lyrics}}
<p class="track-lyrics">{{$Track.GetLyricsHTML}}</p>
{{else}}
<p class="track-lyrics empty">There are no lyrics.</p>
{{end}}
</div>
{{end}}
</div> </div>
<p><em>"Orphaned" tracks that have not yet been bound to a release.</em></p>
<br>
{{range $Track := .Tracks}}
<div class="track">
<h2 class="track-title">
<a href="/admin/tracks/{{$Track.ID}}">{{$Track.Title}}</a>
</h2>
{{if $Track.Description}}
<p class="track-description">{{$Track.GetDescriptionHTML}}</p>
{{else}}
<p class="track-description empty">No description provided.</p>
{{end}}
{{if $Track.Lyrics}}
<p class="track-lyrics">{{$Track.GetLyricsHTML}}</p>
{{else}}
<p class="track-lyrics empty">There are no lyrics.</p>
{{end}}
</div>
{{end}}
{{if not .Artists}}
<p>There are no artists.</p>
{{end}}
</main> </main>
<script type="module" src="/admin/static/admin.js"></script> <script type="module" src="/admin/static/admin.js"></script>

View file

@ -50,33 +50,34 @@ var editArtistHTML string
//go:embed "html/edit-track.html" //go:embed "html/edit-track.html"
var editTrackHTML string var editTrackHTML string
//go:embed "html/components/credits/newcredit.html" //go:embed "html/components/credit/newcredit.html"
var componentNewCreditHTML string var componentNewCreditHTML string
//go:embed "html/components/credits/addcredit.html" //go:embed "html/components/credit/addcredit.html"
var componentAddCreditHTML string var componentAddCreditHTML string
//go:embed "html/components/credits/editcredits.html" //go:embed "html/components/credit/editcredits.html"
var componentEditCreditsHTML string var componentEditCreditsHTML string
//go:embed "html/components/links/editlinks.html" //go:embed "html/components/link/editlinks.html"
var componentEditLinksHTML string var componentEditLinksHTML string
//go:embed "html/components/release/release-list-item.html" //go:embed "html/components/release/release.html"
var componentReleaseListItemHTML string 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 var componentNewTrackHTML string
//go:embed "html/components/tracks/addtrack.html" //go:embed "html/components/track/addtrack.html"
var componentAddTrackHTML string var componentAddTrackHTML string
//go:embed "html/components/tracks/edittracks.html" //go:embed "html/components/track/edittracks.html"
var componentEditTracksHTML string var componentEditTracksHTML string
var BaseTemplate = template.Must( var BaseTemplate = template.Must(
template.New("base").Funcs( template.New("base").Funcs(
template.FuncMap{ template.FuncMap{
"hasPrefix": func(s string, prefix string) bool { "hasPrefix": strings.HasPrefix,
fmt.Printf("does \"%s\" start with \"%s\"?\n", s, prefix)
return strings.HasPrefix(s, prefix)
},
}, },
).Parse(strings.Join([]string{ ).Parse(strings.Join([]string{
layoutHTML, layoutHTML,
@ -86,10 +87,14 @@ var BaseTemplate = template.Must(
var IndexTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse( var IndexTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
strings.Join([]string{ strings.Join([]string{
indexHTML, indexHTML,
componentReleaseListItemHTML, componentReleaseHTML,
componentArtistHTML,
componentTrackHTML,
}, "\n"), }, "\n"),
)) ))
var LoginTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(loginHTML)) var LoginTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(loginHTML))
var LoginTOTPTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(loginTotpHTML)) var LoginTOTPTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(loginTotpHTML))
var RegisterTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(registerHTML)) 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 TOTPSetupTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(totpSetupHTML))
var TOTPConfirmTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(totpConfirmHTML)) var TOTPConfirmTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(totpConfirmHTML))
var LogsTemplate = template.Must(template.Must(BaseTemplate.Clone()).Funcs(template.FuncMap{ var LogsTemplate = template.Must(template.Must(BaseTemplate.Clone()).Funcs(template.FuncMap{
"parseLevel": func(level log.LogLevel) string { "parseLevel": parseLevel,
switch level { "titleCase": titleCase,
case log.LEVEL_INFO: "toLower": toLower,
return "INFO" "prettyTime": prettyTime,
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")
},
}).Parse(logsHTML)) }).Parse(logsHTML))
var ReleasesTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse( var ReleasesTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
strings.Join([]string{ strings.Join([]string{
releasesHTML, 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"), }, "\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 EditArtistTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(editArtistHTML))
var EditTrackTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse( var EditTrackTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
strings.Join([]string{ strings.Join([]string{
editTrackHTML, editTrackHTML,
componentReleaseListItemHTML, componentReleaseHTML,
}, "\n"), }, "\n"),
)) ))
var EditCreditsTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentEditCreditsHTML)) var EditCreditsTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentEditCreditsHTML))
var AddCreditTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentAddCreditHTML)) var AddCreditTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentAddCreditHTML))
var NewCreditTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentNewCreditHTML)) 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 EditTracksTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentEditTracksHTML))
var AddTrackTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentAddTrackHTML)) var AddTrackTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentAddTrackHTML))
var NewTrackTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentNewTrackHTML)) 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")
}

View file

@ -12,11 +12,46 @@ import (
func serveTracks(app *model.AppState) http.Handler { func serveTracks(app *model.AppState) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 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:], "/") slices := strings.Split(strings.TrimPrefix(r.URL.Path, "/tracks")[1:], "/")
id := slices[0] trackID := slices[0]
track, err := controller.GetTrack(app.DB, id)
if len(trackID) > 0 {
serveTrack(app, trackID).ServeHTTP(w, r)
return
}
tracks, err := controller.GetAllTracks(app.DB)
if err != nil { 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) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
@ -27,7 +62,7 @@ func serveTracks(app *model.AppState) http.Handler {
releases, err := controller.GetTrackReleases(app.DB, track.ID, true) releases, err := controller.GetTrackReleases(app.DB, track.ID, true)
if err != nil { 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) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
@ -38,17 +73,14 @@ func serveTracks(app *model.AppState) http.Handler {
Releases []*model.Release Releases []*model.Release
} }
session := r.Context().Value("session").(*model.Session)
err = templates.EditTrackTemplate.Execute(w, TrackResponse{ err = templates.EditTrackTemplate.Execute(w, TrackResponse{
adminPageData: adminPageData{ Path: r.URL.Path, Session: session }, adminPageData: adminPageData{ Path: r.URL.Path, Session: session },
Track: track, Track: track,
Releases: releases, Releases: releases,
}) })
if err != nil { 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) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
} }
}) })
} }

View file

@ -29,6 +29,11 @@ func GetAllArtists(db *sqlx.DB) ([]*model.Artist, error) {
return artists, nil 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) { func GetArtistsNotOnRelease(db *sqlx.DB, releaseID string) ([]*model.Artist, error) {
var artists = []*model.Artist{} var artists = []*model.Artist{}

View file

@ -99,7 +99,7 @@ func GetAllReleases(db *sqlx.DB, onlyVisible bool, limit int, full bool) ([]*mod
return releases, nil 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" query := "SELECT count(*) FROM musicrelease"
if onlyVisible { if onlyVisible {
query += " WHERE visible=true" query += " WHERE visible=true"

View file

@ -29,6 +29,11 @@ func GetAllTracks(db *sqlx.DB) ([]*model.Track, error) {
return tracks, nil 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) { func GetOrphanTracks(db *sqlx.DB) ([]*model.Track, error) {
var tracks = []*model.Track{} var tracks = []*model.Track{}

View file

@ -39,7 +39,7 @@ const (
// GETTERS // GETTERS
func (release Release) GetDescriptionHTML() template.HTML { func (release Release) GetDescriptionHTML() template.HTML {
return template.HTML(strings.Replace(release.Description, "\n", "<br>", -1)) return template.HTML(strings.ReplaceAll(release.Description, "\n", "<br>"))
} }
func (release Release) TextReleaseDate() string { func (release Release) TextReleaseDate() string {

View file

@ -13,16 +13,16 @@ type (
Lyrics string `json:"lyrics" db:"lyrics"` Lyrics string `json:"lyrics" db:"lyrics"`
PreviewURL string `json:"previewURL" db:"preview_url"` PreviewURL string `json:"previewURL" db:"preview_url"`
Number int Number int `json:"-"`
} }
) )
func (track Track) GetDescriptionHTML() template.HTML { func (track Track) GetDescriptionHTML() template.HTML {
return template.HTML(strings.Replace(track.Description, "\n", "<br>", -1)) return template.HTML(strings.ReplaceAll(track.Description, "\n", "<br>"))
} }
func (track Track) GetLyricsHTML() template.HTML { func (track Track) GetLyricsHTML() template.HTML {
return template.HTML(strings.Replace(track.Lyrics, "\n", "<br>", -1)) return template.HTML(strings.ReplaceAll(track.Lyrics, "\n", "<br>"))
} }
// this function is stupid and i hate that i need it // this function is stupid and i hate that i need it

View file

@ -333,7 +333,7 @@ ul#links a {
background-color: #fff; background-color: #fff;
text-align: center; text-align: center;
text-decoration: none; text-decoration: none;
transition: filter .1s,-webkit-filter .1s transition: filter .1s ease-out, -webkit-filter .1s ease-out;
} }
#buylink { #buylink {